Search: reorder results by launchCount (#267)
* add app search result ordering by launchCount * add reordering for other domains * add preference in settings * bruh moment - database optimizaiton - fixing obvious fallacy while reordering * no reordering on empty queries && reordering early return * avoiding deserialization * listpreference * search settings screen icons & categories * regrouping * early-return tweak * Change ranking algorithm * Pepega * Prioritize launchCount over pinned for relevance sorting --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
parent
600e319c99
commit
0b2aa716ec
@ -105,12 +105,10 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
|
||||
val wallpaperBlur = dataStore.data.map { it.appearance.blurWallpaper }.asLiveData()
|
||||
|
||||
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData()
|
||||
val searchBarColor = dataStore.data.map { it.searchBar.color }.asLiveData()
|
||||
val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }.asLiveData()
|
||||
|
||||
|
||||
val gestureState: StateFlow<GestureState> = dataStore
|
||||
.data.map { it.gestures }
|
||||
.distinctUntilChanged()
|
||||
@ -155,7 +153,6 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, GestureState())
|
||||
|
||||
|
||||
var failedGestureState by mutableStateOf<FailedGesture?>(null)
|
||||
fun handleGesture(context: Context, gesture: Gesture): Boolean {
|
||||
val action = when (gesture) {
|
||||
|
||||
@ -198,6 +198,7 @@ fun SearchColumn(
|
||||
highlightedItem = bestMatch as? SavableSearchable
|
||||
)
|
||||
}
|
||||
|
||||
GridResults(
|
||||
items = if ((showWorkProfileApps || apps.isEmpty()) && workApps.isNotEmpty()) workApps.toImmutableList() else apps.toImmutableList(),
|
||||
columns = columns,
|
||||
|
||||
@ -6,9 +6,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
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.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
@ -29,6 +31,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@ -103,19 +106,19 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
hideFavorites.value = query.isNotEmpty()
|
||||
searchJob = viewModelScope.launch {
|
||||
dataStore.data.collectLatest {
|
||||
dataStore.data.collectLatest { settings ->
|
||||
searchService.search(
|
||||
query,
|
||||
calculator = it.calculatorSearch,
|
||||
unitConverter = it.unitConverterSearch,
|
||||
calendars = it.calendarSearch,
|
||||
contacts = it.contactsSearch,
|
||||
files = it.fileSearch,
|
||||
shortcuts = it.appShortcutSearch,
|
||||
websites = it.websiteSearch,
|
||||
wikipedia = it.wikipediaSearch,
|
||||
calculator = settings.calculatorSearch,
|
||||
unitConverter = settings.unitConverterSearch,
|
||||
calendars = settings.calendarSearch,
|
||||
contacts = settings.contactsSearch,
|
||||
files = settings.fileSearch,
|
||||
shortcuts = settings.appShortcutSearch,
|
||||
websites = settings.websiteSearch,
|
||||
wikipedia = settings.wikipediaSearch,
|
||||
).collectLatest { results ->
|
||||
val resultsList = withContext(Dispatchers.Default) {
|
||||
var resultsList = withContext(Dispatchers.Default) {
|
||||
listOfNotNull(
|
||||
results.apps,
|
||||
results.other,
|
||||
@ -129,10 +132,42 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
results.unitConverters,
|
||||
results.searchActions,
|
||||
).flatten()
|
||||
.sortedBy { (it as? SavableSearchable) }
|
||||
.distinctBy { if (it is SavableSearchable) it.key else it }
|
||||
.sortedBy { (it as? SavableSearchable) }
|
||||
}
|
||||
|
||||
|
||||
val relevance =
|
||||
if (query.isNotEmpty() && settings.searchBar.searchResultOrdering == SearchResultOrdering.Relevance) {
|
||||
favoritesRepository.sortByRelevance(
|
||||
resultsList.mapNotNull { (it as? SavableSearchable)?.key }
|
||||
).first()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
resultsList = resultsList.sortedWith { a, b ->
|
||||
when {
|
||||
a is SavableSearchable && b !is SavableSearchable -> -1
|
||||
a !is SavableSearchable && b is SavableSearchable -> 1
|
||||
a is SavableSearchable && b is SavableSearchable -> {
|
||||
val aKey = a.key
|
||||
val bKey = b.key
|
||||
val aRank = relevance.indexOf(aKey)
|
||||
val bRank = relevance.indexOf(bKey)
|
||||
when {
|
||||
aRank != -1 && bRank != -1 -> aRank.compareTo(bRank)
|
||||
aRank == -1 && bRank != -1 -> 1
|
||||
aRank != -1 && bRank == -1 -> -1
|
||||
else -> a.compareTo(b)
|
||||
}
|
||||
}
|
||||
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
||||
val hidden = mutableListOf<SavableSearchable>()
|
||||
val apps = mutableListOf<LauncherApp>()
|
||||
@ -151,6 +186,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
r is SavableSearchable && hiddenKeys.contains(r.key) -> {
|
||||
hidden.add(r)
|
||||
}
|
||||
|
||||
r is LauncherApp && !r.isMainProfile -> workApps.add(r)
|
||||
r is LauncherApp -> apps.add(r)
|
||||
r is AppShortcut -> shortcuts.add(r)
|
||||
@ -165,7 +201,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if (query.isNotEmpty() && launchOnEnter.value) {
|
||||
if (query.isNotEmpty() && launchOnEnter.value) {
|
||||
bestMatch.value = listOf(
|
||||
apps,
|
||||
workApps,
|
||||
@ -274,4 +310,20 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : SavableSearchable> MutableList<T>.reorderByRanks(ranks: List<SavedSearchableRankInfo>) {
|
||||
if (this.size < 2) // one element does not need reordering
|
||||
return
|
||||
|
||||
var i = 0
|
||||
|
||||
for (item in ranks) {
|
||||
val idx = this.indexOfFirst { it.key == item.key }
|
||||
if (idx == -1) continue
|
||||
|
||||
this.add(i++, this.removeAt(idx))
|
||||
|
||||
if (i >= this.size) break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,6 @@ abstract class SearchableItemVM(
|
||||
return customAttributesRepository.getTags(searchable)
|
||||
}
|
||||
|
||||
|
||||
open fun launch(context: Context, bounds: Rect? = null): Boolean {
|
||||
val view = (context as? AppCompatActivity)?.window?.decorView
|
||||
val options = if (bounds != null && view != null) {
|
||||
|
||||
@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.component.preferences.*
|
||||
@ -100,7 +101,10 @@ fun SearchSettingsScreen() {
|
||||
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.observeAsState()
|
||||
AnimatedVisibility(hasAppShortcutsPermission == false) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_appshortcuts_search_settings, stringResource(R.string.app_name)),
|
||||
text = stringResource(
|
||||
R.string.missing_permission_appshortcuts_search_settings,
|
||||
stringResource(R.string.app_name)
|
||||
),
|
||||
onClick = {
|
||||
viewModel.requestAppShortcutsPermission(context as AppCompatActivity)
|
||||
},
|
||||
@ -180,28 +184,48 @@ fun SearchSettingsScreen() {
|
||||
}
|
||||
}
|
||||
item {
|
||||
val autoFocus by viewModel.autoFocus.observeAsState()
|
||||
val launchOnEnter by viewModel.launchOnEnter.observeAsState()
|
||||
PreferenceCategory {
|
||||
val autoFocus by viewModel.autoFocus.observeAsState()
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.preference_search_bar_auto_focus),
|
||||
summary = stringResource(R.string.preference_search_bar_auto_focus_summary),
|
||||
icon = Icons.Rounded.Keyboard,
|
||||
value = autoFocus == true,
|
||||
onValueChanged = {
|
||||
viewModel.setAutoFocus(it)
|
||||
}
|
||||
)
|
||||
val launchOnEnter by viewModel.launchOnEnter.observeAsState()
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.preference_search_bar_launch_on_enter),
|
||||
summary = stringResource(R.string.preference_search_bar_launch_on_enter_summary),
|
||||
icon = Icons.Rounded.ArrowRightAlt,
|
||||
value = launchOnEnter == true,
|
||||
onValueChanged = {
|
||||
viewModel.setLaunchOnEnter(it)
|
||||
}
|
||||
)
|
||||
val searchResultOrdering by viewModel.searchResultOrdering.observeAsState()
|
||||
ListPreference(
|
||||
title = stringResource(R.string.preference_search_bar_ordering),
|
||||
value = searchResultOrdering,
|
||||
icon = Icons.Rounded.Sort,
|
||||
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
|
||||
),
|
||||
onValueChanged = {
|
||||
if (it != null) viewModel.setSearchResultOrdering(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceCategory {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_hidden_items),
|
||||
summary = stringResource(R.string.preference_hidden_items_summary),
|
||||
icon = Icons.Rounded.VisibilityOff,
|
||||
onClick = {
|
||||
navController?.navigate("settings/search/hiddenitems")
|
||||
}
|
||||
@ -209,6 +233,7 @@ fun SearchSettingsScreen() {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_screen_tags),
|
||||
summary = stringResource(R.string.preference_screen_tags_summary),
|
||||
icon = Icons.Rounded.Tag,
|
||||
onClick = {
|
||||
navController?.navigate("settings/search/tags")
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ 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 kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@ -31,7 +32,8 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
@ -45,11 +47,13 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestContactsPermission(activity: AppCompatActivity) {
|
||||
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 {
|
||||
@ -63,6 +67,7 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestCalendarPermission(activity: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
|
||||
}
|
||||
@ -165,8 +170,23 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -180,6 +200,7 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAppShortcutsPermission(activity: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.AppShortcuts)
|
||||
}
|
||||
|
||||
@ -63,12 +63,9 @@ interface SearchDao {
|
||||
@Query("SELECT `key` FROM Searchable WHERE hidden = 1 AND type = 'calendar'")
|
||||
fun getHiddenCalendarEventKeys(): Flow<List<String>>
|
||||
|
||||
|
||||
|
||||
@Query("DELETE FROM Searchable WHERE `key` IN (:keys)")
|
||||
fun deleteAll(keys: List<String>)
|
||||
|
||||
|
||||
@Query("UPDATE Searchable SET pinned = 1, hidden = 0 WHERE `key` = :key")
|
||||
fun pinExistingItem(key: String)
|
||||
|
||||
@ -145,4 +142,7 @@ interface SearchDao {
|
||||
|
||||
@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>>
|
||||
}
|
||||
|
||||
@ -574,4 +574,7 @@
|
||||
<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>
|
||||
</resources>
|
||||
@ -761,4 +761,7 @@
|
||||
<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>
|
||||
</resources>
|
||||
@ -223,6 +223,11 @@ 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;
|
||||
|
||||
|
||||
@ -51,6 +51,13 @@ interface FavoritesRepository {
|
||||
fun getHiddenItems(): Flow<List<SavableSearchable>>
|
||||
fun getHiddenItemKeys(): Flow<List<String>>
|
||||
|
||||
/**
|
||||
* Returns the given keys sorted by relevance.
|
||||
* The first item in the list is the most relevant.
|
||||
* Unknown keys will not be included in the result.
|
||||
*/
|
||||
fun sortByRelevance(keys: List<String>): Flow<List<String>>
|
||||
|
||||
/**
|
||||
* Remove this item from the Searchable database
|
||||
*/
|
||||
@ -107,6 +114,7 @@ internal class FavoritesRepositoryImpl(
|
||||
frequentlyUsed = frequentlyUsed,
|
||||
limit = limit
|
||||
)
|
||||
|
||||
includeTypes != null && excludeTypes == null -> {
|
||||
dao.getFavoritesWithTypes(
|
||||
includeTypes = includeTypes,
|
||||
@ -116,6 +124,7 @@ internal class FavoritesRepositoryImpl(
|
||||
limit = limit
|
||||
)
|
||||
}
|
||||
|
||||
excludeTypes != null && includeTypes == null -> {
|
||||
dao.getFavoritesWithoutTypes(
|
||||
excludeTypes = excludeTypes,
|
||||
@ -125,6 +134,7 @@ internal class FavoritesRepositoryImpl(
|
||||
limit = limit
|
||||
)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("You can either use includeTypes or excludeTypes, not both")
|
||||
}
|
||||
return entities.map {
|
||||
@ -137,13 +147,13 @@ internal class FavoritesRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun isPinned(searchable: SavableSearchable): Flow<Boolean> {
|
||||
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
|
||||
return database.searchDao().isPinned(searchable.key)
|
||||
}
|
||||
|
||||
override fun pinItem(searchable: SavableSearchable) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val dao = AppDatabase.getInstance(context).searchDao()
|
||||
val dao = database.searchDao()
|
||||
val databaseItem = dao.getFavorite(searchable.key)
|
||||
val savedSearchable = SavedSearchable(
|
||||
key = searchable.key,
|
||||
@ -160,19 +170,19 @@ internal class FavoritesRepositoryImpl(
|
||||
override fun unpinItem(searchable: SavableSearchable) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key)
|
||||
database.searchDao().unpinFavorite(searchable.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isHidden(searchable: SavableSearchable): Flow<Boolean> {
|
||||
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
|
||||
return database.searchDao().isHidden(searchable.key)
|
||||
}
|
||||
|
||||
override fun hideItem(searchable: SavableSearchable) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val dao = AppDatabase.getInstance(context).searchDao()
|
||||
val dao = database.searchDao()
|
||||
val databaseItem = dao.getFavorite(searchable.key)
|
||||
val savedSearchable = SavedSearchable(
|
||||
key = searchable.key,
|
||||
@ -189,7 +199,7 @@ internal class FavoritesRepositoryImpl(
|
||||
override fun unhideItem(searchable: SavableSearchable) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key)
|
||||
database.searchDao().unhideItem(searchable.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -199,7 +209,7 @@ internal class FavoritesRepositoryImpl(
|
||||
withContext(Dispatchers.IO) {
|
||||
val item = SavedSearchable(searchable.key, searchable, 0, 0, false)
|
||||
item.toDatabaseEntity()?.let {
|
||||
AppDatabase.getInstance(context).searchDao()
|
||||
database.searchDao()
|
||||
.incrementLaunchCount(it)
|
||||
}
|
||||
}
|
||||
@ -286,6 +296,9 @@ internal class FavoritesRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun sortByRelevance(keys: List<String>): Flow<List<String>> {
|
||||
return database.searchDao().sortByRelevance(keys)
|
||||
}
|
||||
|
||||
private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
|
||||
val deserializer: SearchableDeserializer =
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
package de.mm20.launcher2.favorites
|
||||
|
||||
data class SavedSearchableRankInfo(
|
||||
val key: String,
|
||||
val type: String,
|
||||
var launchCount: Int
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user