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:
Christoph
2023-03-10 22:17:03 +01:00
committed by GitHub
co-authored by MM20
parent 003789f310
commit 2c9cba72a9
21 changed files with 838 additions and 163 deletions
@@ -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
@@ -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
)
}
}
@@ -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
)
}
}
@@ -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()
}
}
}