Search: introduce weighted reordering (#272)
* Database: backend for weights & timestamps - Migration 21->22 - SearchableLaunchTimestampEntity.kt - `weight`-column in SavedSearchableEntity - Queries for sorting by and adjusting weight - Queries for sorting by recent use * move weightfactor access to favoritesRepository * reorder calls in SearchVM * no more datahoarding * add settings screen for ordering * ui fix, animations * move ordering settings out of own screen * remove unused localization * weight factors * larger factor for WeightFactor.High * cleanup * sort favorites by weight * icons for favorite screen * I hate coming up with descriptive strings * add default weight factor to settings migration * Add default values for search result ordering preferences * Favorites settings: change order of preferences * Change strings * Replace favorites variability slider with list preference * migration initial weight * Change labels * Include searchable weight column in backup --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
+57
-12
@@ -25,7 +25,8 @@ fun SliderPreference(
|
||||
max: Float = 1f,
|
||||
step: Float? = null,
|
||||
onValueChanged: (Float) -> Unit,
|
||||
enabled: Boolean = true
|
||||
enabled: Boolean = true,
|
||||
label: (@Composable (Float) -> Unit)? = null
|
||||
) {
|
||||
var sliderValue by remember(value) { mutableStateOf(value) }
|
||||
Row(
|
||||
@@ -72,16 +73,20 @@ fun SliderPreference(
|
||||
onValueChanged(sliderValue)
|
||||
}
|
||||
)
|
||||
val decimalPlaces = -log(step ?: 0.01f, 10f)
|
||||
val format = remember { DecimalFormat().apply {
|
||||
maximumFractionDigits = floor(decimalPlaces).toInt()
|
||||
minimumFractionDigits = 0
|
||||
} }
|
||||
Text(
|
||||
modifier = Modifier.width(56.dp).padding(start = 24.dp),
|
||||
text = format.format(sliderValue),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
if (label != null) {
|
||||
label(sliderValue)
|
||||
} else {
|
||||
val decimalPlaces = -log(step ?: 0.01f, 10f)
|
||||
val format = remember { DecimalFormat().apply {
|
||||
maximumFractionDigits = floor(decimalPlaces).toInt()
|
||||
minimumFractionDigits = 0
|
||||
} }
|
||||
Text(
|
||||
modifier = Modifier.width(56.dp).padding(start = 24.dp),
|
||||
text = format.format(sliderValue),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +101,8 @@ fun SliderPreference(
|
||||
max: Int = 100,
|
||||
step: Int = 1,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
enabled: Boolean = true
|
||||
enabled: Boolean = true,
|
||||
label: (@Composable (Int) -> Unit)? = null
|
||||
) {
|
||||
SliderPreference(
|
||||
title = title,
|
||||
@@ -108,6 +114,45 @@ fun SliderPreference(
|
||||
step = step.toFloat(),
|
||||
onValueChanged = {
|
||||
onValueChanged(it.roundToInt())
|
||||
},
|
||||
label = if (label == null) null else {
|
||||
{ label(it.roundToInt()) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
inline fun <reified T: Enum<T>> SliderPreference(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
value: T,
|
||||
enabled: Boolean = true,
|
||||
labels: List<EnumLocalization<T>>? = null,
|
||||
crossinline onValueChanged: (T) -> Unit
|
||||
) {
|
||||
val values = labels?.map { it.value }?.toTypedArray() ?: enumValues()
|
||||
SliderPreference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
value = values.indexOf(value),
|
||||
min = 0,
|
||||
max = values.size - 1,
|
||||
step = 1,
|
||||
onValueChanged = {
|
||||
onValueChanged(values[it])
|
||||
},
|
||||
enabled = enabled,
|
||||
label = if (labels == null) null else {
|
||||
{
|
||||
val idx = labels.indexOfFirst { l -> l.value == values[it] }
|
||||
Text(
|
||||
modifier = Modifier.width(68.dp).padding(start = 12.dp),
|
||||
text = if (idx != -1) labels[idx].label else "",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
typealias EnumLocalization<T> = ListPreferenceItem<T>
|
||||
|
||||
@@ -10,7 +10,8 @@ import de.mm20.launcher2.favorites.SavedSearchableRankInfo
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchResultOrdering
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.Ordering
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
@@ -136,14 +137,23 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
.sortedBy { (it as? SavableSearchable) }
|
||||
}
|
||||
|
||||
|
||||
val relevance =
|
||||
if (query.isNotEmpty() && settings.searchBar.searchResultOrdering == SearchResultOrdering.Relevance) {
|
||||
favoritesRepository.sortByRelevance(
|
||||
resultsList.mapNotNull { (it as? SavableSearchable)?.key }
|
||||
).first()
|
||||
} else {
|
||||
if (query.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
val keys = resultsList.mapNotNull { (it as? SavableSearchable)?.key }
|
||||
when (settings.resultOrdering.ordering) {
|
||||
|
||||
Ordering.LaunchCount -> favoritesRepository.sortByRelevance(
|
||||
keys
|
||||
).first()
|
||||
|
||||
Ordering.Weighted -> favoritesRepository.sortByWeight(
|
||||
keys
|
||||
).first()
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
resultsList = resultsList.sortedWith { a, b ->
|
||||
|
||||
@@ -10,10 +10,13 @@ import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
import de.mm20.launcher2.icons.IconRepository
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
|
||||
+27
-3
@@ -1,5 +1,11 @@
|
||||
package de.mm20.launcher2.ui.settings.favorites
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Insights
|
||||
import androidx.compose.material.icons.rounded.Sort
|
||||
import androidx.compose.material.icons.rounded.SwapVert
|
||||
import androidx.compose.material.icons.rounded.TableRows
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@@ -8,7 +14,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.preferences.ListPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
@@ -28,6 +36,7 @@ fun FavoritesSettingsScreen() {
|
||||
Preference(
|
||||
title = stringResource(R.string.menu_item_edit_favs),
|
||||
summary = stringResource(R.string.preference_edit_favorites_summary),
|
||||
icon = Icons.Rounded.Sort,
|
||||
onClick = {
|
||||
showEditSheet = true
|
||||
}
|
||||
@@ -43,7 +52,8 @@ fun FavoritesSettingsScreen() {
|
||||
value = frequentlyUsed == true,
|
||||
onValueChanged = {
|
||||
viewModel.setFrequentlyUsed(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.Insights
|
||||
)
|
||||
val frequentlyUsedRows by viewModel.frequentlyUsedRows.observeAsState(1)
|
||||
SliderPreference(
|
||||
@@ -54,7 +64,20 @@ fun FavoritesSettingsScreen() {
|
||||
max = 4,
|
||||
onValueChanged = {
|
||||
viewModel.setFrequentlyUsedRows(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.TableRows
|
||||
)
|
||||
val searchResultWeightFactor by viewModel.searchResultWeightFactor.observeAsState(WeightFactor.Default)
|
||||
ListPreference(
|
||||
title = stringResource(R.string.preference_search_result_ordering_weight_factor),
|
||||
icon = Icons.Rounded.SwapVert,
|
||||
value = searchResultWeightFactor,
|
||||
items = listOf(
|
||||
stringResource(R.string.preference_search_result_ordering_weight_factor_low) to WeightFactor.Low,
|
||||
stringResource(R.string.preference_search_result_ordering_weight_factor_default) to WeightFactor.Default,
|
||||
stringResource(R.string.preference_search_result_ordering_weight_factor_high) to WeightFactor.High
|
||||
),
|
||||
onValueChanged = { viewModel.setSearchResultWeightFactor(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -67,7 +90,8 @@ fun FavoritesSettingsScreen() {
|
||||
value = editButton == true,
|
||||
onValueChanged = {
|
||||
viewModel.setEditButton(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.Edit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -53,4 +54,18 @@ class FavoritesSettingsScreenVM: ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchResultWeightFactor = dataStore.data.map { it.resultOrdering.weightFactor }.asLiveData()
|
||||
fun setSearchResultWeightFactor(searchResultWeightFactor: WeightFactor) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setResultOrdering(
|
||||
it.resultOrdering.toBuilder()
|
||||
.setWeightFactor(searchResultWeightFactor)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,16 +207,17 @@ fun SearchSettingsScreen() {
|
||||
)
|
||||
val searchResultOrdering by viewModel.searchResultOrdering.observeAsState()
|
||||
ListPreference(
|
||||
title = stringResource(R.string.preference_search_bar_ordering),
|
||||
value = searchResultOrdering,
|
||||
icon = Icons.Rounded.Sort,
|
||||
title = stringResource(R.string.preference_search_result_ordering),
|
||||
items = listOf(
|
||||
stringResource(R.string.preference_search_bar_ordering_alphabetic) to Settings.SearchBarSettings.SearchResultOrdering.Alphabetic,
|
||||
stringResource(R.string.preference_search_bar_ordering_relevance) to Settings.SearchBarSettings.SearchResultOrdering.Relevance
|
||||
stringResource(R.string.preference_search_result_ordering_alphabetic) to Settings.SearchResultOrderingSettings.Ordering.Alphabetic,
|
||||
stringResource(R.string.preference_search_result_ordering_launch_count) to Settings.SearchResultOrderingSettings.Ordering.LaunchCount,
|
||||
stringResource(R.string.preference_search_result_ordering_weighted) to Settings.SearchResultOrderingSettings.Ordering.Weighted
|
||||
),
|
||||
value = searchResultOrdering,
|
||||
onValueChanged = {
|
||||
if (it != null) viewModel.setSearchResultOrdering(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.Sort
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+49
-88
@@ -7,7 +7,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchResultOrdering
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -21,29 +22,22 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setFavorites(favorites: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setFavorites(
|
||||
it.favorites.toBuilder()
|
||||
.setEnabled(favorites)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setFavorites(
|
||||
it.favorites.toBuilder().setEnabled(favorites)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val hasContactsPermission =
|
||||
permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
|
||||
val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
|
||||
val contacts = dataStore.data.map { it.contactsSearch.enabled }.asLiveData()
|
||||
fun setContacts(contacts: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setContactsSearch(
|
||||
it.contactsSearch.toBuilder()
|
||||
.setEnabled(contacts)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setContactsSearch(
|
||||
it.contactsSearch.toBuilder().setEnabled(contacts)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,18 +46,14 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
|
||||
}
|
||||
|
||||
val hasCalendarPermission =
|
||||
permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
|
||||
val calendar = dataStore.data.map { it.calendarSearch.enabled }.asLiveData()
|
||||
fun setCalendar(calendar: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setCalendarSearch(
|
||||
it.calendarSearch.toBuilder()
|
||||
.setEnabled(calendar)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setCalendarSearch(
|
||||
it.calendarSearch.toBuilder().setEnabled(calendar)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,12 +66,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setCalculator(calculator: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setCalculatorSearch(
|
||||
it.calculatorSearch.toBuilder()
|
||||
.setEnabled(calculator)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setCalculatorSearch(
|
||||
it.calculatorSearch.toBuilder().setEnabled(calculator)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,12 +77,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setUnitConverter(unitConverter: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setUnitConverterSearch(
|
||||
it.unitConverterSearch.toBuilder()
|
||||
.setEnabled(unitConverter)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setUnitConverterSearch(
|
||||
it.unitConverterSearch.toBuilder().setEnabled(unitConverter)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,12 +88,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setWikipedia(wikipedia: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setWikipediaSearch(
|
||||
it.wikipediaSearch.toBuilder()
|
||||
.setEnabled(wikipedia)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setWikipediaSearch(
|
||||
it.wikipediaSearch.toBuilder().setEnabled(wikipedia)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,12 +99,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setWebsites(websites: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setWebsiteSearch(
|
||||
it.websiteSearch.toBuilder()
|
||||
.setEnabled(websites)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setWebsiteSearch(
|
||||
it.websiteSearch.toBuilder().setEnabled(websites)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,12 +110,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setWebSearch(webSearch: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setWebSearch(
|
||||
it.webSearch.toBuilder()
|
||||
.setEnabled(webSearch)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setWebSearch(
|
||||
it.webSearch.toBuilder().setEnabled(webSearch)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,12 +121,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setAutoFocus(autoFocus: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchBar(
|
||||
it.searchBar.toBuilder()
|
||||
.setAutoFocus(autoFocus)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setSearchBar(
|
||||
it.searchBar.toBuilder().setAutoFocus(autoFocus)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,43 +132,32 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setLaunchOnEnter(launchOnEnter: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchBar(
|
||||
it.searchBar.toBuilder()
|
||||
.setLaunchOnEnter(launchOnEnter)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setSearchBar(
|
||||
it.searchBar.toBuilder().setLaunchOnEnter(launchOnEnter)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchResultOrdering = dataStore.data.map { it.searchBar.searchResultOrdering }.asLiveData()
|
||||
fun setSearchResultOrdering(searchResultOrdering: SearchResultOrdering) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchBar(
|
||||
it.searchBar.toBuilder()
|
||||
.setSearchResultOrdering(searchResultOrdering)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val hasAppShortcutPermission =
|
||||
permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData()
|
||||
val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData()
|
||||
val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData()
|
||||
fun setAppShortcuts(appShortcuts: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setAppShortcutSearch(
|
||||
it.appShortcutSearch.toBuilder()
|
||||
.setEnabled(appShortcuts)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setAppShortcutSearch(
|
||||
it.appShortcutSearch.toBuilder().setEnabled(appShortcuts)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchResultOrdering = dataStore.data.map { it.resultOrdering.ordering }.asLiveData()
|
||||
fun setSearchResultOrdering(searchResultOrdering: Settings.SearchResultOrderingSettings.Ordering) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder().setResultOrdering(
|
||||
it.resultOrdering.toBuilder().setOrdering(searchResultOrdering)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user