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:
parent
003789f310
commit
2c9cba72a9
@ -25,7 +25,8 @@ fun SliderPreference(
|
||||
max: Float = 1f,
|
||||
step: Float? = null,
|
||||
onValueChanged: (Float) -> Unit,
|
||||
enabled: Boolean = true
|
||||
enabled: Boolean = true,
|
||||
label: (@Composable (Float) -> Unit)? = null
|
||||
) {
|
||||
var sliderValue by remember(value) { mutableStateOf(value) }
|
||||
Row(
|
||||
@ -72,16 +73,20 @@ fun SliderPreference(
|
||||
onValueChanged(sliderValue)
|
||||
}
|
||||
)
|
||||
val decimalPlaces = -log(step ?: 0.01f, 10f)
|
||||
val format = remember { DecimalFormat().apply {
|
||||
maximumFractionDigits = floor(decimalPlaces).toInt()
|
||||
minimumFractionDigits = 0
|
||||
} }
|
||||
Text(
|
||||
modifier = Modifier.width(56.dp).padding(start = 24.dp),
|
||||
text = format.format(sliderValue),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
if (label != null) {
|
||||
label(sliderValue)
|
||||
} else {
|
||||
val decimalPlaces = -log(step ?: 0.01f, 10f)
|
||||
val format = remember { DecimalFormat().apply {
|
||||
maximumFractionDigits = floor(decimalPlaces).toInt()
|
||||
minimumFractionDigits = 0
|
||||
} }
|
||||
Text(
|
||||
modifier = Modifier.width(56.dp).padding(start = 24.dp),
|
||||
text = format.format(sliderValue),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,7 +101,8 @@ fun SliderPreference(
|
||||
max: Int = 100,
|
||||
step: Int = 1,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
enabled: Boolean = true
|
||||
enabled: Boolean = true,
|
||||
label: (@Composable (Int) -> Unit)? = null
|
||||
) {
|
||||
SliderPreference(
|
||||
title = title,
|
||||
@ -108,6 +114,45 @@ fun SliderPreference(
|
||||
step = step.toFloat(),
|
||||
onValueChanged = {
|
||||
onValueChanged(it.roundToInt())
|
||||
},
|
||||
label = if (label == null) null else {
|
||||
{ label(it.roundToInt()) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
inline fun <reified T: Enum<T>> SliderPreference(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
value: T,
|
||||
enabled: Boolean = true,
|
||||
labels: List<EnumLocalization<T>>? = null,
|
||||
crossinline onValueChanged: (T) -> Unit
|
||||
) {
|
||||
val values = labels?.map { it.value }?.toTypedArray() ?: enumValues()
|
||||
SliderPreference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
value = values.indexOf(value),
|
||||
min = 0,
|
||||
max = values.size - 1,
|
||||
step = 1,
|
||||
onValueChanged = {
|
||||
onValueChanged(values[it])
|
||||
},
|
||||
enabled = enabled,
|
||||
label = if (labels == null) null else {
|
||||
{
|
||||
val idx = labels.indexOfFirst { l -> l.value == values[it] }
|
||||
Text(
|
||||
modifier = Modifier.width(68.dp).padding(start = 12.dp),
|
||||
text = if (idx != -1) labels[idx].label else "",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
typealias EnumLocalization<T> = ListPreferenceItem<T>
|
||||
|
||||
@ -10,7 +10,8 @@ import de.mm20.launcher2.favorites.SavedSearchableRankInfo
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchResultOrdering
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.Ordering
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
@ -136,14 +137,23 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
.sortedBy { (it as? SavableSearchable) }
|
||||
}
|
||||
|
||||
|
||||
val relevance =
|
||||
if (query.isNotEmpty() && settings.searchBar.searchResultOrdering == SearchResultOrdering.Relevance) {
|
||||
favoritesRepository.sortByRelevance(
|
||||
resultsList.mapNotNull { (it as? SavableSearchable)?.key }
|
||||
).first()
|
||||
} else {
|
||||
if (query.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
val keys = resultsList.mapNotNull { (it as? SavableSearchable)?.key }
|
||||
when (settings.resultOrdering.ordering) {
|
||||
|
||||
Ordering.LaunchCount -> favoritesRepository.sortByRelevance(
|
||||
keys
|
||||
).first()
|
||||
|
||||
Ordering.Weighted -> favoritesRepository.sortByWeight(
|
||||
keys
|
||||
).first()
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
resultsList = resultsList.sortedWith { a, b ->
|
||||
|
||||
@ -10,10 +10,13 @@ import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
import de.mm20.launcher2.icons.IconRepository
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
package de.mm20.launcher2.ui.settings.favorites
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Insights
|
||||
import androidx.compose.material.icons.rounded.Sort
|
||||
import androidx.compose.material.icons.rounded.SwapVert
|
||||
import androidx.compose.material.icons.rounded.TableRows
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
@ -8,7 +14,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.preferences.ListPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
@ -28,6 +36,7 @@ fun FavoritesSettingsScreen() {
|
||||
Preference(
|
||||
title = stringResource(R.string.menu_item_edit_favs),
|
||||
summary = stringResource(R.string.preference_edit_favorites_summary),
|
||||
icon = Icons.Rounded.Sort,
|
||||
onClick = {
|
||||
showEditSheet = true
|
||||
}
|
||||
@ -43,7 +52,8 @@ fun FavoritesSettingsScreen() {
|
||||
value = frequentlyUsed == true,
|
||||
onValueChanged = {
|
||||
viewModel.setFrequentlyUsed(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.Insights
|
||||
)
|
||||
val frequentlyUsedRows by viewModel.frequentlyUsedRows.observeAsState(1)
|
||||
SliderPreference(
|
||||
@ -54,7 +64,20 @@ fun FavoritesSettingsScreen() {
|
||||
max = 4,
|
||||
onValueChanged = {
|
||||
viewModel.setFrequentlyUsedRows(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.TableRows
|
||||
)
|
||||
val searchResultWeightFactor by viewModel.searchResultWeightFactor.observeAsState(WeightFactor.Default)
|
||||
ListPreference(
|
||||
title = stringResource(R.string.preference_search_result_ordering_weight_factor),
|
||||
icon = Icons.Rounded.SwapVert,
|
||||
value = searchResultWeightFactor,
|
||||
items = listOf(
|
||||
stringResource(R.string.preference_search_result_ordering_weight_factor_low) to WeightFactor.Low,
|
||||
stringResource(R.string.preference_search_result_ordering_weight_factor_default) to WeightFactor.Default,
|
||||
stringResource(R.string.preference_search_result_ordering_weight_factor_high) to WeightFactor.High
|
||||
),
|
||||
onValueChanged = { viewModel.setSearchResultWeightFactor(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -67,7 +90,8 @@ fun FavoritesSettingsScreen() {
|
||||
value = editButton == true,
|
||||
onValueChanged = {
|
||||
viewModel.setEditButton(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.Edit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@ -53,4 +54,18 @@ class FavoritesSettingsScreenVM: ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchResultWeightFactor = dataStore.data.map { it.resultOrdering.weightFactor }.asLiveData()
|
||||
fun setSearchResultWeightFactor(searchResultWeightFactor: WeightFactor) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setResultOrdering(
|
||||
it.resultOrdering.toBuilder()
|
||||
.setWeightFactor(searchResultWeightFactor)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -207,16 +207,17 @@ fun SearchSettingsScreen() {
|
||||
)
|
||||
val searchResultOrdering by viewModel.searchResultOrdering.observeAsState()
|
||||
ListPreference(
|
||||
title = stringResource(R.string.preference_search_bar_ordering),
|
||||
value = searchResultOrdering,
|
||||
icon = Icons.Rounded.Sort,
|
||||
title = stringResource(R.string.preference_search_result_ordering),
|
||||
items = listOf(
|
||||
stringResource(R.string.preference_search_bar_ordering_alphabetic) to Settings.SearchBarSettings.SearchResultOrdering.Alphabetic,
|
||||
stringResource(R.string.preference_search_bar_ordering_relevance) to Settings.SearchBarSettings.SearchResultOrdering.Relevance
|
||||
stringResource(R.string.preference_search_result_ordering_alphabetic) to Settings.SearchResultOrderingSettings.Ordering.Alphabetic,
|
||||
stringResource(R.string.preference_search_result_ordering_launch_count) to Settings.SearchResultOrderingSettings.Ordering.LaunchCount,
|
||||
stringResource(R.string.preference_search_result_ordering_weighted) to Settings.SearchResultOrderingSettings.Ordering.Weighted
|
||||
),
|
||||
value = searchResultOrdering,
|
||||
onValueChanged = {
|
||||
if (it != null) viewModel.setSearchResultOrdering(it)
|
||||
}
|
||||
},
|
||||
icon = Icons.Rounded.Sort
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchResultOrdering
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@ -21,29 +22,22 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setFavorites(favorites: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setFavorites(
|
||||
it.favorites.toBuilder()
|
||||
.setEnabled(favorites)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setFavorites(
|
||||
it.favorites.toBuilder().setEnabled(favorites)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val hasContactsPermission =
|
||||
permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
|
||||
val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
|
||||
val contacts = dataStore.data.map { it.contactsSearch.enabled }.asLiveData()
|
||||
fun setContacts(contacts: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setContactsSearch(
|
||||
it.contactsSearch.toBuilder()
|
||||
.setEnabled(contacts)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setContactsSearch(
|
||||
it.contactsSearch.toBuilder().setEnabled(contacts)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,18 +46,14 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
|
||||
}
|
||||
|
||||
val hasCalendarPermission =
|
||||
permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
|
||||
val calendar = dataStore.data.map { it.calendarSearch.enabled }.asLiveData()
|
||||
fun setCalendar(calendar: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setCalendarSearch(
|
||||
it.calendarSearch.toBuilder()
|
||||
.setEnabled(calendar)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setCalendarSearch(
|
||||
it.calendarSearch.toBuilder().setEnabled(calendar)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,12 +66,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setCalculator(calculator: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setCalculatorSearch(
|
||||
it.calculatorSearch.toBuilder()
|
||||
.setEnabled(calculator)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setCalculatorSearch(
|
||||
it.calculatorSearch.toBuilder().setEnabled(calculator)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,12 +77,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setUnitConverter(unitConverter: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setUnitConverterSearch(
|
||||
it.unitConverterSearch.toBuilder()
|
||||
.setEnabled(unitConverter)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setUnitConverterSearch(
|
||||
it.unitConverterSearch.toBuilder().setEnabled(unitConverter)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,12 +88,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setWikipedia(wikipedia: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setWikipediaSearch(
|
||||
it.wikipediaSearch.toBuilder()
|
||||
.setEnabled(wikipedia)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setWikipediaSearch(
|
||||
it.wikipediaSearch.toBuilder().setEnabled(wikipedia)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -118,12 +99,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setWebsites(websites: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setWebsiteSearch(
|
||||
it.websiteSearch.toBuilder()
|
||||
.setEnabled(websites)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setWebsiteSearch(
|
||||
it.websiteSearch.toBuilder().setEnabled(websites)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,12 +110,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setWebSearch(webSearch: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setWebSearch(
|
||||
it.webSearch.toBuilder()
|
||||
.setEnabled(webSearch)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setWebSearch(
|
||||
it.webSearch.toBuilder().setEnabled(webSearch)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -146,12 +121,9 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setAutoFocus(autoFocus: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchBar(
|
||||
it.searchBar.toBuilder()
|
||||
.setAutoFocus(autoFocus)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setSearchBar(
|
||||
it.searchBar.toBuilder().setAutoFocus(autoFocus)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -160,43 +132,32 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
fun setLaunchOnEnter(launchOnEnter: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchBar(
|
||||
it.searchBar.toBuilder()
|
||||
.setLaunchOnEnter(launchOnEnter)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setSearchBar(
|
||||
it.searchBar.toBuilder().setLaunchOnEnter(launchOnEnter)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchResultOrdering = dataStore.data.map { it.searchBar.searchResultOrdering }.asLiveData()
|
||||
fun setSearchResultOrdering(searchResultOrdering: SearchResultOrdering) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchBar(
|
||||
it.searchBar.toBuilder()
|
||||
.setSearchResultOrdering(searchResultOrdering)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val hasAppShortcutPermission =
|
||||
permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData()
|
||||
val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData()
|
||||
val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData()
|
||||
fun setAppShortcuts(appShortcuts: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setAppShortcutSearch(
|
||||
it.appShortcutSearch.toBuilder()
|
||||
.setEnabled(appShortcuts)
|
||||
)
|
||||
.build()
|
||||
it.toBuilder().setAppShortcutSearch(
|
||||
it.appShortcutSearch.toBuilder().setEnabled(appShortcuts)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchResultOrdering = dataStore.data.map { it.resultOrdering.ordering }.asLiveData()
|
||||
fun setSearchResultOrdering(searchResultOrdering: Settings.SearchResultOrderingSettings.Ordering) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder().setResultOrdering(
|
||||
it.resultOrdering.toBuilder().setOrdering(searchResultOrdering)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface SearchDao {
|
||||
|
||||
@Insert()
|
||||
@Insert
|
||||
fun insertAll(items: List<SavedSearchableEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
@ -19,12 +19,13 @@ interface SearchDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAllReplaceExisting(items: List<SavedSearchableEntity>)
|
||||
|
||||
|
||||
@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<List<SavedSearchableEntity>>
|
||||
|
||||
@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<String>,
|
||||
manuallySorted: Boolean = false,
|
||||
@ -46,12 +49,14 @@ interface SearchDao {
|
||||
limit: Int,
|
||||
): Flow<List<SavedSearchableEntity>>
|
||||
|
||||
@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<String>,
|
||||
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<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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -574,7 +574,12 @@
|
||||
<string name="preference_layout_fixed_rotation">Feste Bildschirmausrichtung</string>
|
||||
<string name="preference_layout_fixed_rotation_summary">Porträtmodus erzwingen</string>
|
||||
<string name="icon_pack_dynamic_colors">Dynamische Farben</string>
|
||||
<string name="preference_search_bar_ordering">Anordnung der Suchergebnisse</string>
|
||||
<string name="preference_search_bar_ordering_alphabetic">Alphabetisch</string>
|
||||
<string name="preference_search_bar_ordering_relevance">Relevanz</string>
|
||||
<string name="preference_search_result_ordering">Sortierung der Suchergebnisse</string>
|
||||
<string name="preference_search_result_ordering_alphabetic">Alphabetisch</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>
|
||||
@ -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="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="preference_search_bar_ordering">Search-result order</string>
|
||||
<string name="preference_search_bar_ordering_alphabetic">Alphabetic</string>
|
||||
<string name="preference_search_bar_ordering_relevance">Relevance</string>
|
||||
<string name="preference_search_result_ordering">Order of search results</string>
|
||||
<string name="preference_search_result_ordering_alphabetic">Alphabetic</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>
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<String>): Flow<List<String>>
|
||||
|
||||
fun sortByWeight(keys: List<String>): Flow<List<String>>
|
||||
|
||||
/**
|
||||
* 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<String>): Flow<List<String>> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val favoritesModule = module {
|
||||
single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get()) }
|
||||
single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get(), get()) }
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user