diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt index b4dead19..2654ae9f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt @@ -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 > SliderPreference( + title: String, + icon: ImageVector? = null, + value: T, + enabled: Boolean = true, + labels: List>? = 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 = ListPreferenceItem diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index 4f681af0..f0f900b0 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -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 -> diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt index e7fa0bcc..075a1ad3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt @@ -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 diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt index e2ec82f4..89821bd4 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt @@ -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 ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreenVM.kt index 6800ca1f..6ee460d8 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreenVM.kt @@ -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() + } + } + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index ee041585..6b6dde21 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -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 ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt index 8f48f2e6..6224cad9 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt @@ -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() } } } diff --git a/core/database/schemas/de.mm20.launcher2.database.AppDatabase/22.json b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/22.json new file mode 100644 index 00000000..7134c574 --- /dev/null +++ b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/22.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt index 88018323..7c30c9ff 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -7,7 +7,6 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import de.mm20.launcher2.database.entities.* 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_19_20 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_7_8 import de.mm20.launcher2.database.migrations.Migration_8_9 @@ -35,8 +35,8 @@ import de.mm20.launcher2.database.migrations.Migration_9_10 IconPackEntity::class, WidgetEntity::class, CustomAttributeEntity::class, - SearchActionEntity::class, - ], version = 21, exportSchema = true + SearchActionEntity::class + ], version = 22, exportSchema = true ) @TypeConverters(ComponentNameConverter::class, StringListConverter::class) abstract class AppDatabase : RoomDatabase() { @@ -104,6 +104,7 @@ abstract class AppDatabase : RoomDatabase() { Migration_18_19(), Migration_19_20(), Migration_20_21(), + Migration_21_22() ).build() if (_instance == null) _instance = instance return instance diff --git a/core/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt index ea6488db..bcf9901a 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface SearchDao { - @Insert() + @Insert fun insertAll(items: List) @Insert(onConflict = OnConflictStrategy.IGNORE) @@ -19,12 +19,13 @@ interface SearchDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAllReplaceExisting(items: List) - - @Query("SELECT * FROM Searchable " + - "WHERE ((:manuallySorted AND pinned > 1) OR " + - "(:automaticallySorted AND pinned = 1) OR" + - "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + - ") ORDER BY pinned DESC, launchCount DESC LIMIT :limit") + @Query( + "SELECT * FROM Searchable " + + "WHERE ((:manuallySorted AND pinned > 1) OR " + + "(:automaticallySorted AND pinned = 1) OR" + + "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + + ") ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit" + ) fun getFavorites( manuallySorted: Boolean = false, automaticallySorted: Boolean = false, @@ -32,12 +33,14 @@ interface SearchDao { limit: Int, ): Flow> - @Query("SELECT * FROM Searchable " + - "WHERE SUBSTR(`key`, 0, INSTR(`key`, '://')) IN (:includeTypes) AND (" + - "(:manuallySorted AND pinned > 1) OR " + - "(:automaticallySorted AND pinned = 1) OR" + - "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + - ") ORDER BY pinned DESC, launchCount DESC LIMIT :limit") + @Query( + "SELECT * FROM Searchable " + + "WHERE SUBSTR(`key`, 0, INSTR(`key`, '://')) IN (:includeTypes) AND (" + + "(:manuallySorted AND pinned > 1) OR " + + "(:automaticallySorted AND pinned = 1) OR" + + "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + + ") ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit" + ) fun getFavoritesWithTypes( includeTypes: List, manuallySorted: Boolean = false, @@ -46,12 +49,14 @@ interface SearchDao { limit: Int, ): Flow> - @Query("SELECT * FROM Searchable " + - "WHERE `type` NOT IN (:excludeTypes) AND (" + - "(:manuallySorted AND pinned > 1) OR " + - "(:automaticallySorted AND pinned = 1) OR" + - "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + - ") ORDER BY pinned DESC, launchCount DESC LIMIT :limit") + @Query( + "SELECT * FROM Searchable " + + "WHERE `type` NOT IN (:excludeTypes) AND (" + + "(:manuallySorted AND pinned > 1) OR " + + "(:automaticallySorted AND pinned = 1) OR" + + "(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" + + ") ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit" + ) fun getFavoritesWithoutTypes( excludeTypes: List, manuallySorted: Boolean = false, @@ -114,8 +119,10 @@ interface SearchDao { fun incrementExistingLaunchCount(key: String) @Transaction - fun incrementLaunchCount(item: SavedSearchableEntity) { + fun incrementLaunchCount(item: SavedSearchableEntity, alpha: Double) { incrementExistingLaunchCount(item.key) + increaseWeightWhere(item.key, alpha) + reduceWeightExcept(item.key, alpha) insertSkipExisting(item) } @@ -140,9 +147,18 @@ interface SearchDao { @Query("UPDATE Searchable SET `pinned` = 0") 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) @Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) AND launchCount > 0 ORDER BY launchCount DESC, pinned DESC") fun sortByRelevance(keys: List): Flow> + + @Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) ORDER BY `weight` DESC, pinned DESC") + fun sortByWeight(keys: List): Flow> + + @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) } diff --git a/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt b/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt index 9af986f7..21bb5895 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt @@ -11,5 +11,6 @@ data class SavedSearchableEntity( @ColumnInfo(name = "searchable") val serializedSearchable: String, var launchCount: Int, @ColumnInfo(name = "pinned") var pinPosition: Int, - var hidden: Boolean + var hidden: Boolean, + @ColumnInfo(defaultValue = "0.0") var weight: Double ) diff --git a/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_21_22.kt b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_21_22.kt new file mode 100644 index 00000000..98a789cd --- /dev/null +++ b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_21_22.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/core/i18n/src/main/res/values-de/strings.xml b/core/i18n/src/main/res/values-de/strings.xml index fa9d74f2..e02eb7b9 100644 --- a/core/i18n/src/main/res/values-de/strings.xml +++ b/core/i18n/src/main/res/values-de/strings.xml @@ -574,7 +574,12 @@ Feste Bildschirmausrichtung Porträtmodus erzwingen Dynamische Farben - Anordnung der Suchergebnisse - Alphabetisch - Relevanz + Sortierung der Suchergebnisse + Alphabetisch + Aufrufhäufigkeit + Dynamisch + Ranking-Flexibilität + Stabil + Ausgewogen + Variabel \ No newline at end of file diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 1ae586d0..8b0a5623 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -761,7 +761,12 @@ 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: The launcher\'s accessibility service needs to be enabled to perform this action. This action requires the launcher\'s accessibility service to be enabled. - Search-result order - Alphabetic - Relevance + Order of search results + Alphabetic + Launch count + Dynamic + Ranking flexibility + Stable + Balanced + Variable \ No newline at end of file diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt index e0616da7..5475b506 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt @@ -183,6 +183,11 @@ fun createFactorySettings(context: Context): Settings { .setSwipeLeft(Settings.GestureSettings.GestureAction.None) .setSwipeRight(Settings.GestureSettings.GestureAction.None) ) + .setResultOrdering( + Settings.SearchResultOrderingSettings.newBuilder() + .setOrdering(Settings.SearchResultOrderingSettings.Ordering.Weighted) + .setWeightFactor(Settings.SearchResultOrderingSettings.WeightFactor.Default) + ) .build() } diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_12_13.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_12_13.kt index 74cd7127..f0364f44 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_12_13.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/migrations/Migration_12_13.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.preferences.migrations import de.mm20.launcher2.preferences.Settings +import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor class Migration_12_13: VersionedMigration(12, 13) { override suspend fun applyMigrations(builder: Settings.Builder): Settings.Builder { @@ -10,5 +11,10 @@ class Migration_12_13: VersionedMigration(12, 13) { .setDatePart(true) .build() ) + .setResultOrdering( + builder.resultOrdering.toBuilder() + .setWeightFactor(WeightFactor.Default) + .build() + ) } } \ No newline at end of file diff --git a/core/preferences/src/main/proto/settings.proto b/core/preferences/src/main/proto/settings.proto index bcc97408..6f23b1c3 100644 --- a/core/preferences/src/main/proto/settings.proto +++ b/core/preferences/src/main/proto/settings.proto @@ -223,11 +223,6 @@ message Settings { } SearchBarColors color = 3; bool launch_on_enter = 4; - enum SearchResultOrdering { - Alphabetic = 0; - Relevance = 1; - } - SearchResultOrdering search_result_ordering = 5; } SearchBarSettings search_bar = 20; @@ -332,4 +327,20 @@ message Settings { string long_press_app = 10; } 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; } \ No newline at end of file diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt b/data/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt index b301189c..1ecdcfcc 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt +++ b/data/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt @@ -6,6 +6,8 @@ import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.entities.SavedSearchableEntity 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.SearchableDeserializer import kotlinx.coroutines.* @@ -58,6 +60,8 @@ interface FavoritesRepository { */ fun sortByRelevance(keys: List): Flow> + fun sortByWeight(keys: List): Flow> + /** * Remove this item from the Searchable database */ @@ -94,6 +98,7 @@ interface FavoritesRepository { internal class FavoritesRepositoryImpl( private val context: Context, private val database: AppDatabase, + private val dataStore: LauncherDataStore ) : FavoritesRepository, KoinComponent { private val scope = CoroutineScope(Job() + Dispatchers.Default) @@ -160,7 +165,8 @@ internal class FavoritesRepositoryImpl( searchable = searchable, launchCount = databaseItem?.launchCount ?: 0, pinPosition = 1, - hidden = false + hidden = false, + weight = databaseItem?.weight ?: 0.0 ) savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) } } @@ -189,7 +195,8 @@ internal class FavoritesRepositoryImpl( searchable = searchable, launchCount = databaseItem?.launchCount ?: 0, pinPosition = 0, - hidden = true + hidden = true, + weight = databaseItem?.weight ?: 0.0 ) savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) } } @@ -207,10 +214,16 @@ internal class FavoritesRepositoryImpl( override fun incrementLaunchCounter(searchable: SavableSearchable) { scope.launch { 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 { database.searchDao() - .incrementLaunchCount(it) + .incrementLaunchCount(it, weightFactor) } } } @@ -249,6 +262,7 @@ internal class FavoritesRepositoryImpl( launchCount = 0, pinPosition = 0, hidden = false, + weight = 0.0 ).toDatabaseEntity() ?: return@withContext database.searchDao().insertSkipExisting(entity) } @@ -271,6 +285,7 @@ internal class FavoritesRepositoryImpl( launchCount = 0, pinPosition = 0, hidden = false, + weight = 0.0 ).toDatabaseEntity() ?: return@mapIndexedNotNull null entity.pinPosition = manuallySorted.size - index + 1 entity @@ -283,6 +298,7 @@ internal class FavoritesRepositoryImpl( launchCount = 0, pinPosition = 0, hidden = false, + weight = 0.0 ).toDatabaseEntity() ?: return@mapIndexedNotNull null entity.pinPosition = 1 entity @@ -300,6 +316,10 @@ internal class FavoritesRepositoryImpl( return database.searchDao().sortByRelevance(keys) } + override fun sortByWeight(keys: List): Flow> { + return database.searchDao().sortByWeight(keys) + } + private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable { val deserializer: SearchableDeserializer = getDeserializer(context, entity.type) @@ -310,7 +330,8 @@ internal class FavoritesRepositoryImpl( searchable = searchable, launchCount = entity.launchCount, pinPosition = entity.pinPosition, - hidden = entity.hidden + hidden = entity.hidden, + weight = entity.weight ) } @@ -340,7 +361,8 @@ internal class FavoritesRepositoryImpl( "hidden" to fav.hidden, "launchCount" to fav.launchCount, "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"), launchCount = json.getInt("launchCount"), hidden = json.getBoolean("hidden"), - pinPosition = json.getInt("pinPosition") + pinPosition = json.getInt("pinPosition"), + weight = json.optDouble("weight").takeIf { !it.isNaN() } ?: 0.0 ) favorites.add(entity) } diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt b/data/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt index c0439184..4e5e57f4 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt +++ b/data/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt @@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val favoritesModule = module { - single { FavoritesRepositoryImpl(androidContext(), get()) } + single { FavoritesRepositoryImpl(androidContext(), get(), get()) } } \ No newline at end of file diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchable.kt b/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchable.kt index b1118fdb..71461a41 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchable.kt +++ b/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchable.kt @@ -11,7 +11,8 @@ data class SavedSearchable( val searchable: SavableSearchable?, var launchCount: Int, var pinPosition: Int, - var hidden: Boolean + var hidden: Boolean, + var weight: Double ) { fun toDatabaseEntity(): SavedSearchableEntity? { val serializer = getSerializer(searchable) @@ -24,7 +25,8 @@ data class SavedSearchable( serializedSearchable = data, hidden = hidden, pinPosition = pinPosition, - launchCount = launchCount + launchCount = launchCount, + weight = weight ) } } \ No newline at end of file diff --git a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt index e776eb60..26ce6d23 100644 --- a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt +++ b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt @@ -182,8 +182,13 @@ class BackupManager( } companion object { + /** + * Format changelog: + * - 1.5: added `weight` to favorites + */ + private const val BackupFormatMajor = 1 - private const val BackupFormatMinor = 4 + private const val BackupFormatMinor = 5 internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor" } }