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
parent 003789f310
commit 2c9cba72a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 838 additions and 163 deletions

View File

@ -25,7 +25,8 @@ fun SliderPreference(
max: Float = 1f, max: Float = 1f,
step: Float? = null, step: Float? = null,
onValueChanged: (Float) -> Unit, onValueChanged: (Float) -> Unit,
enabled: Boolean = true enabled: Boolean = true,
label: (@Composable (Float) -> Unit)? = null
) { ) {
var sliderValue by remember(value) { mutableStateOf(value) } var sliderValue by remember(value) { mutableStateOf(value) }
Row( Row(
@ -72,16 +73,20 @@ fun SliderPreference(
onValueChanged(sliderValue) onValueChanged(sliderValue)
} }
) )
val decimalPlaces = -log(step ?: 0.01f, 10f) if (label != null) {
val format = remember { DecimalFormat().apply { label(sliderValue)
maximumFractionDigits = floor(decimalPlaces).toInt() } else {
minimumFractionDigits = 0 val decimalPlaces = -log(step ?: 0.01f, 10f)
} } val format = remember { DecimalFormat().apply {
Text( maximumFractionDigits = floor(decimalPlaces).toInt()
modifier = Modifier.width(56.dp).padding(start = 24.dp), minimumFractionDigits = 0
text = format.format(sliderValue), } }
style = MaterialTheme.typography.titleSmall 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, max: Int = 100,
step: Int = 1, step: Int = 1,
onValueChanged: (Int) -> Unit, onValueChanged: (Int) -> Unit,
enabled: Boolean = true enabled: Boolean = true,
label: (@Composable (Int) -> Unit)? = null
) { ) {
SliderPreference( SliderPreference(
title = title, title = title,
@ -108,6 +114,45 @@ fun SliderPreference(
step = step.toFloat(), step = step.toFloat(),
onValueChanged = { onValueChanged = {
onValueChanged(it.roundToInt()) 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>

View File

@ -10,7 +10,8 @@ import de.mm20.launcher2.favorites.SavedSearchableRankInfo
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore 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.SavableSearchable
import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
@ -136,14 +137,23 @@ class SearchVM : ViewModel(), KoinComponent {
.sortedBy { (it as? SavableSearchable) } .sortedBy { (it as? SavableSearchable) }
} }
val relevance = val relevance =
if (query.isNotEmpty() && settings.searchBar.searchResultOrdering == SearchResultOrdering.Relevance) { if (query.isEmpty()) {
favoritesRepository.sortByRelevance(
resultsList.mapNotNull { (it as? SavableSearchable)?.key }
).first()
} else {
emptyList() 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 -> resultsList = resultsList.sortedWith { a, b ->

View File

@ -10,10 +10,13 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel 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.SavableSearchable
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject

View File

@ -1,5 +1,11 @@
package de.mm20.launcher2.ui.settings.favorites 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@ -8,7 +14,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel 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.R
import de.mm20.launcher2.ui.component.preferences.ListPreference
import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@ -28,6 +36,7 @@ fun FavoritesSettingsScreen() {
Preference( Preference(
title = stringResource(R.string.menu_item_edit_favs), title = stringResource(R.string.menu_item_edit_favs),
summary = stringResource(R.string.preference_edit_favorites_summary), summary = stringResource(R.string.preference_edit_favorites_summary),
icon = Icons.Rounded.Sort,
onClick = { onClick = {
showEditSheet = true showEditSheet = true
} }
@ -43,7 +52,8 @@ fun FavoritesSettingsScreen() {
value = frequentlyUsed == true, value = frequentlyUsed == true,
onValueChanged = { onValueChanged = {
viewModel.setFrequentlyUsed(it) viewModel.setFrequentlyUsed(it)
} },
icon = Icons.Rounded.Insights
) )
val frequentlyUsedRows by viewModel.frequentlyUsedRows.observeAsState(1) val frequentlyUsedRows by viewModel.frequentlyUsedRows.observeAsState(1)
SliderPreference( SliderPreference(
@ -54,7 +64,20 @@ fun FavoritesSettingsScreen() {
max = 4, max = 4,
onValueChanged = { onValueChanged = {
viewModel.setFrequentlyUsedRows(it) 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, value = editButton == true,
onValueChanged = { onValueChanged = {
viewModel.setEditButton(it) viewModel.setEditButton(it)
} },
icon = Icons.Rounded.Edit
) )
} }
} }

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent 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()
}
}
}
} }

View File

@ -207,16 +207,17 @@ fun SearchSettingsScreen() {
) )
val searchResultOrdering by viewModel.searchResultOrdering.observeAsState() val searchResultOrdering by viewModel.searchResultOrdering.observeAsState()
ListPreference( ListPreference(
title = stringResource(R.string.preference_search_bar_ordering), title = stringResource(R.string.preference_search_result_ordering),
value = searchResultOrdering,
icon = Icons.Rounded.Sort,
items = listOf( items = listOf(
stringResource(R.string.preference_search_bar_ordering_alphabetic) to Settings.SearchBarSettings.SearchResultOrdering.Alphabetic, stringResource(R.string.preference_search_result_ordering_alphabetic) to Settings.SearchResultOrderingSettings.Ordering.Alphabetic,
stringResource(R.string.preference_search_bar_ordering_relevance) to Settings.SearchBarSettings.SearchResultOrdering.Relevance 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 = { onValueChanged = {
if (it != null) viewModel.setSearchResultOrdering(it) if (it != null) viewModel.setSearchResultOrdering(it)
} },
icon = Icons.Rounded.Sort
) )
} }
} }

View File

@ -7,7 +7,8 @@ import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore 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.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -21,29 +22,22 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setFavorites(favorites: Boolean) { fun setFavorites(favorites: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setFavorites(
.setFavorites( it.favorites.toBuilder().setEnabled(favorites)
it.favorites.toBuilder() ).build()
.setEnabled(favorites)
)
.build()
} }
} }
} }
val hasContactsPermission = val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
val contacts = dataStore.data.map { it.contactsSearch.enabled }.asLiveData() val contacts = dataStore.data.map { it.contactsSearch.enabled }.asLiveData()
fun setContacts(contacts: Boolean) { fun setContacts(contacts: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setContactsSearch(
.setContactsSearch( it.contactsSearch.toBuilder().setEnabled(contacts)
it.contactsSearch.toBuilder() ).build()
.setEnabled(contacts)
)
.build()
} }
} }
} }
@ -52,18 +46,14 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
permissionsManager.requestPermission(activity, PermissionGroup.Contacts) permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
} }
val hasCalendarPermission = val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
val calendar = dataStore.data.map { it.calendarSearch.enabled }.asLiveData() val calendar = dataStore.data.map { it.calendarSearch.enabled }.asLiveData()
fun setCalendar(calendar: Boolean) { fun setCalendar(calendar: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setCalendarSearch(
.setCalendarSearch( it.calendarSearch.toBuilder().setEnabled(calendar)
it.calendarSearch.toBuilder() ).build()
.setEnabled(calendar)
)
.build()
} }
} }
} }
@ -76,12 +66,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setCalculator(calculator: Boolean) { fun setCalculator(calculator: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setCalculatorSearch(
.setCalculatorSearch( it.calculatorSearch.toBuilder().setEnabled(calculator)
it.calculatorSearch.toBuilder() ).build()
.setEnabled(calculator)
)
.build()
} }
} }
} }
@ -90,12 +77,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setUnitConverter(unitConverter: Boolean) { fun setUnitConverter(unitConverter: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setUnitConverterSearch(
.setUnitConverterSearch( it.unitConverterSearch.toBuilder().setEnabled(unitConverter)
it.unitConverterSearch.toBuilder() ).build()
.setEnabled(unitConverter)
)
.build()
} }
} }
} }
@ -104,12 +88,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setWikipedia(wikipedia: Boolean) { fun setWikipedia(wikipedia: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setWikipediaSearch(
.setWikipediaSearch( it.wikipediaSearch.toBuilder().setEnabled(wikipedia)
it.wikipediaSearch.toBuilder() ).build()
.setEnabled(wikipedia)
)
.build()
} }
} }
} }
@ -118,12 +99,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setWebsites(websites: Boolean) { fun setWebsites(websites: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setWebsiteSearch(
.setWebsiteSearch( it.websiteSearch.toBuilder().setEnabled(websites)
it.websiteSearch.toBuilder() ).build()
.setEnabled(websites)
)
.build()
} }
} }
} }
@ -132,12 +110,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setWebSearch(webSearch: Boolean) { fun setWebSearch(webSearch: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setWebSearch(
.setWebSearch( it.webSearch.toBuilder().setEnabled(webSearch)
it.webSearch.toBuilder() ).build()
.setEnabled(webSearch)
)
.build()
} }
} }
} }
@ -146,12 +121,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setAutoFocus(autoFocus: Boolean) { fun setAutoFocus(autoFocus: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setSearchBar(
.setSearchBar( it.searchBar.toBuilder().setAutoFocus(autoFocus)
it.searchBar.toBuilder() ).build()
.setAutoFocus(autoFocus)
)
.build()
} }
} }
} }
@ -160,43 +132,32 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setLaunchOnEnter(launchOnEnter: Boolean) { fun setLaunchOnEnter(launchOnEnter: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setSearchBar(
.setSearchBar( it.searchBar.toBuilder().setLaunchOnEnter(launchOnEnter)
it.searchBar.toBuilder() ).build()
.setLaunchOnEnter(launchOnEnter)
)
.build()
} }
} }
} }
val searchResultOrdering = dataStore.data.map { it.searchBar.searchResultOrdering }.asLiveData() val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts).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 appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData() val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData()
fun setAppShortcuts(appShortcuts: Boolean) { fun setAppShortcuts(appShortcuts: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder().setAppShortcutSearch(
.setAppShortcutSearch( it.appShortcutSearch.toBuilder().setEnabled(appShortcuts)
it.appShortcutSearch.toBuilder() ).build()
.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()
} }
} }
} }

View File

@ -0,0 +1,499 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "5d3853b609231cdcab5fb3c681d05ebb",
"entities": [
{
"tableName": "forecasts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))",
"fields": [
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "temperature",
"columnName": "temperature",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "minTemp",
"columnName": "minTemp",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "maxTemp",
"columnName": "maxTemp",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "pressure",
"columnName": "pressure",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "humidity",
"columnName": "humidity",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "condition",
"columnName": "condition",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "clouds",
"columnName": "clouds",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "windSpeed",
"columnName": "windSpeed",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "windDirection",
"columnName": "windDirection",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "precipitation",
"columnName": "rain",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "snow",
"columnName": "snow",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "night",
"columnName": "night",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "location",
"columnName": "location",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "provider",
"columnName": "provider",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "providerUrl",
"columnName": "providerUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "precipProbability",
"columnName": "rainProbability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "snowProbability",
"columnName": "snowProbability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"timestamp"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Searchable",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `weight` REAL NOT NULL DEFAULT 0.0, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serializedSearchable",
"columnName": "searchable",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "launchCount",
"columnName": "launchCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinPosition",
"columnName": "pinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hidden",
"columnName": "hidden",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "REAL",
"notNull": true,
"defaultValue": "0.0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Currency",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))",
"fields": [
{
"fieldPath": "symbol",
"columnName": "symbol",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "lastUpdate",
"columnName": "lastUpdate",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"symbol"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Icons",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `packageName` TEXT, `activityName` TEXT, `drawable` TEXT, `extras` TEXT, `iconPack` TEXT NOT NULL, `name` TEXT, `themed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityName",
"columnName": "activityName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "drawable",
"columnName": "drawable",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "extras",
"columnName": "extras",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "iconPack",
"columnName": "iconPack",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "themed",
"columnName": "themed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "IconPack",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, `themed` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scale",
"columnName": "scale",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "themed",
"columnName": "themed",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Widget",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "CustomAttributes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SearchAction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT, `label` TEXT, `icon` INTEGER, `color` INTEGER, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))",
"fields": [
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "customIcon",
"columnName": "customIcon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "options",
"columnName": "options",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"position"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5d3853b609231cdcab5fb3c681d05ebb')"
]
}
}

View File

@ -7,7 +7,6 @@ import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import de.mm20.launcher2.database.entities.* import de.mm20.launcher2.database.entities.*
import de.mm20.launcher2.database.migrations.Migration_10_11 import de.mm20.launcher2.database.migrations.Migration_10_11
@ -21,6 +20,7 @@ import de.mm20.launcher2.database.migrations.Migration_17_18
import de.mm20.launcher2.database.migrations.Migration_18_19 import de.mm20.launcher2.database.migrations.Migration_18_19
import de.mm20.launcher2.database.migrations.Migration_19_20 import de.mm20.launcher2.database.migrations.Migration_19_20
import de.mm20.launcher2.database.migrations.Migration_20_21 import de.mm20.launcher2.database.migrations.Migration_20_21
import de.mm20.launcher2.database.migrations.Migration_21_22
import de.mm20.launcher2.database.migrations.Migration_6_7 import de.mm20.launcher2.database.migrations.Migration_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8 import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9 import de.mm20.launcher2.database.migrations.Migration_8_9
@ -35,8 +35,8 @@ import de.mm20.launcher2.database.migrations.Migration_9_10
IconPackEntity::class, IconPackEntity::class,
WidgetEntity::class, WidgetEntity::class,
CustomAttributeEntity::class, CustomAttributeEntity::class,
SearchActionEntity::class, SearchActionEntity::class
], version = 21, exportSchema = true ], version = 22, exportSchema = true
) )
@TypeConverters(ComponentNameConverter::class, StringListConverter::class) @TypeConverters(ComponentNameConverter::class, StringListConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -104,6 +104,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_18_19(), Migration_18_19(),
Migration_19_20(), Migration_19_20(),
Migration_20_21(), Migration_20_21(),
Migration_21_22()
).build() ).build()
if (_instance == null) _instance = instance if (_instance == null) _instance = instance
return instance return instance

View File

@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface SearchDao { interface SearchDao {
@Insert() @Insert
fun insertAll(items: List<SavedSearchableEntity>) fun insertAll(items: List<SavedSearchableEntity>)
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
@ -19,12 +19,13 @@ interface SearchDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllReplaceExisting(items: List<SavedSearchableEntity>) fun insertAllReplaceExisting(items: List<SavedSearchableEntity>)
@Query(
@Query("SELECT * FROM Searchable " + "SELECT * FROM Searchable " +
"WHERE ((:manuallySorted AND pinned > 1) OR " + "WHERE ((:manuallySorted AND pinned > 1) OR " +
"(:automaticallySorted AND pinned = 1) OR" + "(:automaticallySorted AND pinned = 1) OR" +
"(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" +
") ORDER BY pinned DESC, launchCount DESC LIMIT :limit") ") ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getFavorites( fun getFavorites(
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
@ -32,12 +33,14 @@ interface SearchDao {
limit: Int, limit: Int,
): Flow<List<SavedSearchableEntity>> ): Flow<List<SavedSearchableEntity>>
@Query("SELECT * FROM Searchable " + @Query(
"WHERE SUBSTR(`key`, 0, INSTR(`key`, '://')) IN (:includeTypes) AND (" + "SELECT * FROM Searchable " +
"(:manuallySorted AND pinned > 1) OR " + "WHERE SUBSTR(`key`, 0, INSTR(`key`, '://')) IN (:includeTypes) AND (" +
"(:automaticallySorted AND pinned = 1) OR" + "(:manuallySorted AND pinned > 1) OR " +
"(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + "(:automaticallySorted AND pinned = 1) OR" +
") ORDER BY pinned DESC, launchCount DESC LIMIT :limit") "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" +
") ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getFavoritesWithTypes( fun getFavoritesWithTypes(
includeTypes: List<String>, includeTypes: List<String>,
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
@ -46,12 +49,14 @@ interface SearchDao {
limit: Int, limit: Int,
): Flow<List<SavedSearchableEntity>> ): Flow<List<SavedSearchableEntity>>
@Query("SELECT * FROM Searchable " + @Query(
"WHERE `type` NOT IN (:excludeTypes) AND (" + "SELECT * FROM Searchable " +
"(:manuallySorted AND pinned > 1) OR " + "WHERE `type` NOT IN (:excludeTypes) AND (" +
"(:automaticallySorted AND pinned = 1) OR" + "(:manuallySorted AND pinned > 1) OR " +
"(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + "(:automaticallySorted AND pinned = 1) OR" +
") ORDER BY pinned DESC, launchCount DESC LIMIT :limit") "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" +
") ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getFavoritesWithoutTypes( fun getFavoritesWithoutTypes(
excludeTypes: List<String>, excludeTypes: List<String>,
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
@ -114,8 +119,10 @@ interface SearchDao {
fun incrementExistingLaunchCount(key: String) fun incrementExistingLaunchCount(key: String)
@Transaction @Transaction
fun incrementLaunchCount(item: SavedSearchableEntity) { fun incrementLaunchCount(item: SavedSearchableEntity, alpha: Double) {
incrementExistingLaunchCount(item.key) incrementExistingLaunchCount(item.key)
increaseWeightWhere(item.key, alpha)
reduceWeightExcept(item.key, alpha)
insertSkipExisting(item) insertSkipExisting(item)
} }
@ -140,9 +147,18 @@ interface SearchDao {
@Query("UPDATE Searchable SET `pinned` = 0") @Query("UPDATE Searchable SET `pinned` = 0")
fun unpinAll() fun unpinAll()
@Query("UPDATE Searchable Set `pinned` = 0, `launchCount` = 0 WHERE `key` = :key") @Query("UPDATE Searchable SET `pinned` = 0, `launchCount` = 0 WHERE `key` = :key")
suspend fun resetPinStatusAndLaunchCounter(key: String) suspend fun resetPinStatusAndLaunchCounter(key: String)
@Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) AND launchCount > 0 ORDER BY launchCount DESC, pinned DESC") @Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) AND launchCount > 0 ORDER BY launchCount DESC, pinned DESC")
fun sortByRelevance(keys: List<String>): Flow<List<String>> fun sortByRelevance(keys: List<String>): Flow<List<String>>
@Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) ORDER BY `weight` DESC, pinned DESC")
fun sortByWeight(keys: List<String>): Flow<List<String>>
@Query("UPDATE Searchable SET `weight` = `weight` * (1.0 - :alpha) WHERE `key` != :key")
fun reduceWeightExcept(key: String, alpha: Double)
@Query("UPDATE Searchable SET `weight` = `weight` + :alpha * (1.0 - `weight`) WHERE `key` == :key")
fun increaseWeightWhere(key: String, alpha: Double)
} }

View File

@ -11,5 +11,6 @@ data class SavedSearchableEntity(
@ColumnInfo(name = "searchable") val serializedSearchable: String, @ColumnInfo(name = "searchable") val serializedSearchable: String,
var launchCount: Int, var launchCount: Int,
@ColumnInfo(name = "pinned") var pinPosition: Int, @ColumnInfo(name = "pinned") var pinPosition: Int,
var hidden: Boolean var hidden: Boolean,
@ColumnInfo(defaultValue = "0.0") var weight: Double
) )

View File

@ -0,0 +1,37 @@
package de.mm20.launcher2.database.migrations
import android.util.Log
import androidx.core.database.getIntOrNull
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration_21_22: Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
ALTER TABLE `Searchable`
ADD `weight` DOUBLE NOT NULL DEFAULT 0.0
""")
database.query("""
SELECT MAX(`launchCount`)
FROM `Searchable`
""")
.runCatching {
if (!this.moveToFirst()) {
return
}
this.getIntOrNull(0)
?.run {
database.execSQL("""
UPDATE `Searchable`
SET `weight` = `launchCount` / $this
""")
}
}.onFailure {
Log.e("Migration_21_22", "Setting default values for weight failed", it)
}
}
}

View File

@ -574,7 +574,12 @@
<string name="preference_layout_fixed_rotation">Feste Bildschirmausrichtung</string> <string name="preference_layout_fixed_rotation">Feste Bildschirmausrichtung</string>
<string name="preference_layout_fixed_rotation_summary">Porträtmodus erzwingen</string> <string name="preference_layout_fixed_rotation_summary">Porträtmodus erzwingen</string>
<string name="icon_pack_dynamic_colors">Dynamische Farben</string> <string name="icon_pack_dynamic_colors">Dynamische Farben</string>
<string name="preference_search_bar_ordering">Anordnung der Suchergebnisse</string> <string name="preference_search_result_ordering">Sortierung der Suchergebnisse</string>
<string name="preference_search_bar_ordering_alphabetic">Alphabetisch</string> <string name="preference_search_result_ordering_alphabetic">Alphabetisch</string>
<string name="preference_search_bar_ordering_relevance">Relevanz</string> <string name="preference_search_result_ordering_launch_count">Aufrufhäufigkeit</string>
<string name="preference_search_result_ordering_weighted">Dynamisch</string>
<string name="preference_search_result_ordering_weight_factor">Ranking-Flexibilität</string>
<string name="preference_search_result_ordering_weight_factor_low">Stabil</string>
<string name="preference_search_result_ordering_weight_factor_default">Ausgewogen</string>
<string name="preference_search_result_ordering_weight_factor_high">Variabel</string>
</resources> </resources>

View File

@ -761,7 +761,12 @@
<string name="gesture_failed_message">You have performed a \"%1$s\" gesture. This gesture is currently set to trigger a \"%2$s\" action. However, the action could not be performed for the following reason:</string> <string name="gesture_failed_message">You have performed a \"%1$s\" gesture. This gesture is currently set to trigger a \"%2$s\" action. However, the action could not be performed for the following reason:</string>
<string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string> <string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string>
<string name="missing_permission_accessibility_gesture_settings">This action requires the launcher\'s accessibility service to be enabled.</string> <string name="missing_permission_accessibility_gesture_settings">This action requires the launcher\'s accessibility service to be enabled.</string>
<string name="preference_search_bar_ordering">Search-result order</string> <string name="preference_search_result_ordering">Order of search results</string>
<string name="preference_search_bar_ordering_alphabetic">Alphabetic</string> <string name="preference_search_result_ordering_alphabetic">Alphabetic</string>
<string name="preference_search_bar_ordering_relevance">Relevance</string> <string name="preference_search_result_ordering_launch_count">Launch count</string>
<string name="preference_search_result_ordering_weighted">Dynamic</string>
<string name="preference_search_result_ordering_weight_factor">Ranking flexibility</string>
<string name="preference_search_result_ordering_weight_factor_low">Stable</string>
<string name="preference_search_result_ordering_weight_factor_default">Balanced</string>
<string name="preference_search_result_ordering_weight_factor_high">Variable</string>
</resources> </resources>

View File

@ -183,6 +183,11 @@ fun createFactorySettings(context: Context): Settings {
.setSwipeLeft(Settings.GestureSettings.GestureAction.None) .setSwipeLeft(Settings.GestureSettings.GestureAction.None)
.setSwipeRight(Settings.GestureSettings.GestureAction.None) .setSwipeRight(Settings.GestureSettings.GestureAction.None)
) )
.setResultOrdering(
Settings.SearchResultOrderingSettings.newBuilder()
.setOrdering(Settings.SearchResultOrderingSettings.Ordering.Weighted)
.setWeightFactor(Settings.SearchResultOrderingSettings.WeightFactor.Default)
)
.build() .build()
} }

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.preferences.migrations package de.mm20.launcher2.preferences.migrations
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
class Migration_12_13: VersionedMigration(12, 13) { class Migration_12_13: VersionedMigration(12, 13) {
override suspend fun applyMigrations(builder: Settings.Builder): Settings.Builder { override suspend fun applyMigrations(builder: Settings.Builder): Settings.Builder {
@ -10,5 +11,10 @@ class Migration_12_13: VersionedMigration(12, 13) {
.setDatePart(true) .setDatePart(true)
.build() .build()
) )
.setResultOrdering(
builder.resultOrdering.toBuilder()
.setWeightFactor(WeightFactor.Default)
.build()
)
} }
} }

View File

@ -223,11 +223,6 @@ message Settings {
} }
SearchBarColors color = 3; SearchBarColors color = 3;
bool launch_on_enter = 4; bool launch_on_enter = 4;
enum SearchResultOrdering {
Alphabetic = 0;
Relevance = 1;
}
SearchResultOrdering search_result_ordering = 5;
} }
SearchBarSettings search_bar = 20; SearchBarSettings search_bar = 20;
@ -332,4 +327,20 @@ message Settings {
string long_press_app = 10; string long_press_app = 10;
} }
GestureSettings gestures = 28; GestureSettings gestures = 28;
message SearchResultOrderingSettings {
enum Ordering {
Alphabetic = 0;
LaunchCount = 1;
Weighted = 2;
}
Ordering ordering = 1;
enum WeightFactor {
Low = 0;
Default = 1;
High = 2;
}
WeightFactor weight_factor = 2;
}
SearchResultOrderingSettings result_ordering = 29;
} }

View File

@ -6,6 +6,8 @@ import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SavedSearchableEntity import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -58,6 +60,8 @@ interface FavoritesRepository {
*/ */
fun sortByRelevance(keys: List<String>): Flow<List<String>> fun sortByRelevance(keys: List<String>): Flow<List<String>>
fun sortByWeight(keys: List<String>): Flow<List<String>>
/** /**
* Remove this item from the Searchable database * Remove this item from the Searchable database
*/ */
@ -94,6 +98,7 @@ interface FavoritesRepository {
internal class FavoritesRepositoryImpl( internal class FavoritesRepositoryImpl(
private val context: Context, private val context: Context,
private val database: AppDatabase, private val database: AppDatabase,
private val dataStore: LauncherDataStore
) : FavoritesRepository, KoinComponent { ) : FavoritesRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
@ -160,7 +165,8 @@ internal class FavoritesRepositoryImpl(
searchable = searchable, searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0, launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 1, pinPosition = 1,
hidden = false hidden = false,
weight = databaseItem?.weight ?: 0.0
) )
savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) } savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) }
} }
@ -189,7 +195,8 @@ internal class FavoritesRepositoryImpl(
searchable = searchable, searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0, launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 0, pinPosition = 0,
hidden = true hidden = true,
weight = databaseItem?.weight ?: 0.0
) )
savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) } savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) }
} }
@ -207,10 +214,16 @@ internal class FavoritesRepositoryImpl(
override fun incrementLaunchCounter(searchable: SavableSearchable) { override fun incrementLaunchCounter(searchable: SavableSearchable) {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val item = SavedSearchable(searchable.key, searchable, 0, 0, false) val weightFactor =
when (dataStore.data.map { it.resultOrdering.weightFactor }.firstOrNull()) {
WeightFactor.Low -> 0.1
WeightFactor.High -> 0.5
else -> 0.2
}
val item = SavedSearchable(searchable.key, searchable, 0, 0, false, 0.0)
item.toDatabaseEntity()?.let { item.toDatabaseEntity()?.let {
database.searchDao() database.searchDao()
.incrementLaunchCount(it) .incrementLaunchCount(it, weightFactor)
} }
} }
} }
@ -249,6 +262,7 @@ internal class FavoritesRepositoryImpl(
launchCount = 0, launchCount = 0,
pinPosition = 0, pinPosition = 0,
hidden = false, hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@withContext ).toDatabaseEntity() ?: return@withContext
database.searchDao().insertSkipExisting(entity) database.searchDao().insertSkipExisting(entity)
} }
@ -271,6 +285,7 @@ internal class FavoritesRepositoryImpl(
launchCount = 0, launchCount = 0,
pinPosition = 0, pinPosition = 0,
hidden = false, hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@mapIndexedNotNull null ).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = manuallySorted.size - index + 1 entity.pinPosition = manuallySorted.size - index + 1
entity entity
@ -283,6 +298,7 @@ internal class FavoritesRepositoryImpl(
launchCount = 0, launchCount = 0,
pinPosition = 0, pinPosition = 0,
hidden = false, hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@mapIndexedNotNull null ).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = 1 entity.pinPosition = 1
entity entity
@ -300,6 +316,10 @@ internal class FavoritesRepositoryImpl(
return database.searchDao().sortByRelevance(keys) return database.searchDao().sortByRelevance(keys)
} }
override fun sortByWeight(keys: List<String>): Flow<List<String>> {
return database.searchDao().sortByWeight(keys)
}
private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable { private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
val deserializer: SearchableDeserializer = val deserializer: SearchableDeserializer =
getDeserializer(context, entity.type) getDeserializer(context, entity.type)
@ -310,7 +330,8 @@ internal class FavoritesRepositoryImpl(
searchable = searchable, searchable = searchable,
launchCount = entity.launchCount, launchCount = entity.launchCount,
pinPosition = entity.pinPosition, pinPosition = entity.pinPosition,
hidden = entity.hidden hidden = entity.hidden,
weight = entity.weight
) )
} }
@ -340,7 +361,8 @@ internal class FavoritesRepositoryImpl(
"hidden" to fav.hidden, "hidden" to fav.hidden,
"launchCount" to fav.launchCount, "launchCount" to fav.launchCount,
"pinPosition" to fav.pinPosition, "pinPosition" to fav.pinPosition,
"searchable" to fav.serializedSearchable "searchable" to fav.serializedSearchable,
"weight" to fav.weight,
) )
) )
} }
@ -373,7 +395,8 @@ internal class FavoritesRepositoryImpl(
serializedSearchable = json.getString("searchable"), serializedSearchable = json.getString("searchable"),
launchCount = json.getInt("launchCount"), launchCount = json.getInt("launchCount"),
hidden = json.getBoolean("hidden"), hidden = json.getBoolean("hidden"),
pinPosition = json.getInt("pinPosition") pinPosition = json.getInt("pinPosition"),
weight = json.optDouble("weight").takeIf { !it.isNaN() } ?: 0.0
) )
favorites.add(entity) favorites.add(entity)
} }

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val favoritesModule = module { val favoritesModule = module {
single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get()) } single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get(), get()) }
} }

View File

@ -11,7 +11,8 @@ data class SavedSearchable(
val searchable: SavableSearchable?, val searchable: SavableSearchable?,
var launchCount: Int, var launchCount: Int,
var pinPosition: Int, var pinPosition: Int,
var hidden: Boolean var hidden: Boolean,
var weight: Double
) { ) {
fun toDatabaseEntity(): SavedSearchableEntity? { fun toDatabaseEntity(): SavedSearchableEntity? {
val serializer = getSerializer(searchable) val serializer = getSerializer(searchable)
@ -24,7 +25,8 @@ data class SavedSearchable(
serializedSearchable = data, serializedSearchable = data,
hidden = hidden, hidden = hidden,
pinPosition = pinPosition, pinPosition = pinPosition,
launchCount = launchCount launchCount = launchCount,
weight = weight
) )
} }
} }

View File

@ -182,8 +182,13 @@ class BackupManager(
} }
companion object { companion object {
/**
* Format changelog:
* - 1.5: added `weight` to favorites
*/
private const val BackupFormatMajor = 1 private const val BackupFormatMajor = 1
private const val BackupFormatMinor = 4 private const val BackupFormatMinor = 5
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor" internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
} }
} }