diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index 8868d84d..f56ea4ba 100644 --- a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -130,7 +130,7 @@ internal class AppShortcutRepositoryImpl( it, label ) - }.sorted() + } ) } } diff --git a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt index 8d2237ab..0623b793 100644 --- a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt +++ b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt @@ -65,7 +65,7 @@ internal class ContactRepositoryImpl( for ((id, rawIds) in contactMap) { Contact.contactById(context, id, rawIds)?.let { results.add(it) } } - results.sortedBy { it } + results } return results } diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt index f2cae363..9e25ce75 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttribute.kt @@ -13,7 +13,8 @@ sealed interface CustomAttribute { if (entity == null) return null return when (entity.type) { CustomAttributeType.Label.value -> CustomLabel( - label = entity.value + label = entity.value, + key = entity.key ) CustomAttributeType.Tag.value -> CustomTag( tagName = entity.value @@ -31,6 +32,7 @@ sealed interface CustomAttribute { class CustomLabel( + val key: String, val label: String, ) : CustomAttribute { override fun toDatabaseEntity(key: String): CustomAttributeEntity { diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt index a9900be9..56fdcb7e 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt @@ -1,9 +1,9 @@ package de.mm20.launcher2.customattrs +import android.util.Log import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.entities.CustomAttributeEntity -import de.mm20.launcher2.database.entities.WebsearchEntity import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.search.data.Searchable import kotlinx.coroutines.* @@ -17,6 +17,10 @@ interface CustomAttributesRepository { fun getCustomIcon(searchable: Searchable): Flow fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) + fun getCustomLabels(items: List): Flow> + fun setCustomLabel(searchable: Searchable, label: String) + fun clearCustomLabel(searchable: Searchable) + suspend fun export(toDir: File) suspend fun import(fromDir: File) } @@ -44,6 +48,36 @@ internal class CustomAttributesRepositoryImpl( } } + override fun getCustomLabels(items: List): Flow> { + val dao = appDatabase.customAttrsDao() + return dao.getCustomAttributes(items.map { it.key }, CustomAttributeType.Label.value) + .map { list -> + list.mapNotNull { CustomAttribute.fromDatabaseEntity(it) as? CustomLabel } + } + } + + override fun setCustomLabel(searchable: Searchable, label: String) { + val dao = appDatabase.customAttrsDao() + scope.launch { + appDatabase.runInTransaction { + dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value) + dao.setCustomAttribute( + CustomLabel( + key = searchable.key, + label = label, + ).toDatabaseEntity(searchable.key) + ) + } + } + } + + override fun clearCustomLabel(searchable: Searchable) { + val dao = appDatabase.customAttrsDao() + scope.launch { + dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value) + } + } + override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { val dao = appDatabase.backupDao() var page = 0 @@ -72,7 +106,9 @@ internal class CustomAttributesRepositoryImpl( val dao = appDatabase.backupDao() dao.wipeCustomAttributes() - val files = fromDir.listFiles { _, name -> name.startsWith("customizations.") } ?: return@withContext + val files = + fromDir.listFiles { _, name -> name.startsWith("customizations.") } + ?: return@withContext for (file in files) { val customAttrs = mutableListOf() diff --git a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt index 419f661b..e03f550c 100644 --- a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt @@ -16,4 +16,7 @@ interface CustomAttrsDao { @Insert fun setCustomAttribute(entity: CustomAttributeEntity) + + @Query("SELECT * FROM CustomAttributes WHERE type = :type AND key IN (:keys)") + fun getCustomAttributes(keys: List, type: String) : Flow> } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt index 0116f8f9..f6287616 100644 --- a/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt +++ b/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt @@ -25,7 +25,7 @@ internal class GDriveFileProvider( viewUri = it.viewUri, metaData = getMetadata(it.metadata) ) - }.sorted() + } } private fun getMetadata(file: DriveFileMeta): List> { diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt index 983e8b0e..2033cdcc 100644 --- a/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt +++ b/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt @@ -61,6 +61,6 @@ internal class LocalFileProvider( results.add(file) } cursor.close() - return@withContext results.sortedBy { it } + return@withContext results } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt b/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt index db457de0..57a34b9d 100644 --- a/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt +++ b/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFileProvider.kt @@ -27,7 +27,7 @@ internal class OneDriveFileProvider( webUrl = driveItem.webUrl ) } - return files.sorted() + return files } private fun getMetaData(driveItem: DriveItem): List> { diff --git a/search/src/main/java/de/mm20/launcher2/search/data/Searchable.kt b/search/src/main/java/de/mm20/launcher2/search/data/Searchable.kt index 7df9244e..676cef86 100644 --- a/search/src/main/java/de/mm20/launcher2/search/data/Searchable.kt +++ b/search/src/main/java/de/mm20/launcher2/search/data/Searchable.kt @@ -16,6 +16,8 @@ abstract class Searchable : Comparable { abstract val key: String abstract val label: String + var labelOverride: String? = null + open fun serialize(): String = "" open fun getLaunchIntent(context: Context): Intent? = null @@ -43,8 +45,10 @@ abstract class Searchable : Comparable { abstract fun getPlaceholderIcon(context: Context): StaticLauncherIcon override fun compareTo(other: Searchable): Int { + val label1 = labelOverride ?: label + val label2 = other.labelOverride ?: other.label return Collator.getInstance().apply { strength = Collator.SECONDARY } - .compare(label.romanize(), other.label.romanize()) + .compare(label1.romanize(), label2.romanize()) } override fun equals(other: Any?): Boolean { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index ec471ffa..bccf1d6f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -10,6 +10,7 @@ import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.calculator.CalculatorRepository import de.mm20.launcher2.calendar.CalendarRepository import de.mm20.launcher2.contacts.ContactRepository +import de.mm20.launcher2.customattrs.CustomAttributesRepository import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.files.FileRepository import de.mm20.launcher2.permissions.PermissionGroup @@ -31,6 +32,7 @@ class SearchVM : ViewModel(), KoinComponent { private val favoritesRepository: FavoritesRepository by inject() private val widgetRepository: WidgetRepository by inject() private val permissionsManager: PermissionsManager by inject() + private val customAttributesRepository: CustomAttributesRepository by inject() private val dataStore: LauncherDataStore by inject() private val calendarRepository: CalendarRepository by inject() @@ -81,12 +83,15 @@ class SearchVM : ViewModel(), KoinComponent { } widgetRepository.isCalendarWidgetEnabled().collectLatest { excludeCalendar -> dataStore.data.map { it.grid.columnCount }.collectLatest { columns -> - favoritesRepository.getFavorites( - columns = columns, - excludeCalendarEvents = excludeCalendar - ).collectLatest { - favorites.value = it - } + favoritesRepository + .getFavorites( + columns = columns, + excludeCalendarEvents = excludeCalendar + ) + .withCustomLabels() + .collectLatest { + favorites.value = it + } } } } @@ -103,44 +108,53 @@ class SearchVM : ViewModel(), KoinComponent { try { searchJob?.cancel() - } catch (e: CancellationException) { + } catch (_: CancellationException) { } hideFavorites.postValue(query.isNotEmpty()) searchJob = viewModelScope.launch { isSearching.postValue(true) val jobs = mutableListOf>() jobs += async(Dispatchers.Default) { - appRepository.search(query).collectLatest { apps -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = apps.partition { !hiddenKeys.contains(it.key) } - appResults.postValue(results.first) - hiddenItems.update { - it.copy(apps = results.second) + appRepository + .search(query) + .withCustomLabels() + .collectLatest { apps -> + hiddenItemKeys.collectLatest { hiddenKeys -> + val results = apps.partition { !hiddenKeys.contains(it.key) } + appResults.postValue(results.first) + hiddenItems.update { + it.copy(apps = results.second) + } } } - } } jobs += async(Dispatchers.Default) { - contactRepository.search(query).collectLatest { contacts -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = contacts.partition { !hiddenKeys.contains(it.key) } - contactResults.postValue(results.first) - hiddenItems.update { - it.copy(contacts = results.second) + contactRepository + .search(query) + .withCustomLabels() + .collectLatest { contacts -> + hiddenItemKeys.collectLatest { hiddenKeys -> + val results = contacts.partition { !hiddenKeys.contains(it.key) } + contactResults.postValue(results.first) + hiddenItems.update { + it.copy(contacts = results.second) + } } } - } } jobs += async(Dispatchers.Default) { - calendarRepository.search(query).collectLatest { events -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = events.partition { !hiddenKeys.contains(it.key) } - calendarResults.postValue(results.first) - hiddenItems.update { - it.copy(calendarEvents = results.second) + calendarRepository + .search(query) + .withCustomLabels() + .collectLatest { events -> + hiddenItemKeys.collectLatest { hiddenKeys -> + val results = events.partition { !hiddenKeys.contains(it.key) } + calendarResults.postValue(results.first) + hiddenItems.update { + it.copy(calendarEvents = results.second) + } } } - } } jobs += async(Dispatchers.Default) { wikipediaRepository.search(query).collectLatest { @@ -163,15 +177,18 @@ class SearchVM : ViewModel(), KoinComponent { } } jobs += async(Dispatchers.Default) { - fileRepository.search(query).collectLatest { files -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = files.partition { !hiddenKeys.contains(it.key) } - fileResults.postValue(results.first) - hiddenItems.update { - it.copy(files = results.second) + fileRepository + .search(query) + .withCustomLabels() + .collectLatest { files -> + hiddenItemKeys.collectLatest { hiddenKeys -> + val results = files.partition { !hiddenKeys.contains(it.key) } + fileResults.postValue(results.first) + hiddenItems.update { + it.copy(files = results.second) + } } } - } } jobs += async(Dispatchers.Default) { websearchRepository.search(query).collectLatest { @@ -179,11 +196,14 @@ class SearchVM : ViewModel(), KoinComponent { } } jobs += async(Dispatchers.Default) { - appShortcutRepository.search(query).collectLatest { shortcuts -> - hiddenItemKeys.collectLatest { hidden -> - appShortcutResults.postValue(shortcuts.filter { !hidden.contains(it.key) }) + appShortcutRepository + .search(query) + .withCustomLabels() + .collectLatest { shortcuts -> + hiddenItemKeys.collectLatest { hidden -> + appShortcutResults.postValue(shortcuts.filter { !hidden.contains(it.key) }) + } } - } } launch(Dispatchers.Default) { hiddenItems.collectLatest { @@ -272,6 +292,22 @@ class SearchVM : ViewModel(), KoinComponent { } } + /** + * Inject custom labels and sort by the actual label + */ + private fun Flow>.withCustomLabels(): Flow> = channelFlow { + this@withCustomLabels.collectLatest { items -> + val labelsFlow = customAttributesRepository.getCustomLabels(items) + labelsFlow.collectLatest { labels -> + for (item in items) { + val customLabel = labels.find { it.key == item.key } + item.labelOverride = customLabel?.label + } + send(items.sorted()) + } + } + } + } private data class HiddenItemResults( diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt index e26cc8f7..77d7526c 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt @@ -62,7 +62,7 @@ fun AppItem( .weight(1f) .padding(16.dp) ) { - Text(text = app.label, style = MaterialTheme.typography.titleMedium) + Text(text = app.labelOverride ?: app.label, style = MaterialTheme.typography.titleMedium) app.version?.let { Text( text = stringResource(R.string.app_info_version, it), diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt index cc841ef3..947c584d 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt @@ -80,7 +80,7 @@ fun CalendarItem( if (showDetails) MaterialTheme.typography.titleMedium else MaterialTheme.typography.titleSmall ) - Text(text = calendar.label, style = textStyle) + Text(text = calendar.labelOverride ?: calendar.label, style = textStyle) AnimatedVisibility(!showDetails) { Text( modifier = Modifier.padding(top = 2.dp), diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt index a7692568..cdfa19d3 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt @@ -97,23 +97,29 @@ fun CustomizeSearchableSheet( viewModel.openIconPicker() } ) + + var customLabelValue by remember { + mutableStateOf(searchable.labelOverride ?: "") + } OutlinedTextField( modifier = Modifier .fillMaxWidth() - .padding(top = 24.dp) - .clickable { - Toast - .makeText(context, "Soon™", Toast.LENGTH_SHORT) - .show() - }, - enabled = false, - value = searchable.label, - onValueChange = {}, + .padding(top = 24.dp), + value = customLabelValue, + onValueChange = { + customLabelValue = it + }, placeholder = { Text(searchable.label) }, ) + + DisposableEffect(searchable.key) { + onDispose { + viewModel.setCustomLabel(customLabelValue) + } + } } } else { val iconSize = 48.dp diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt index 83e522d5..2a5d966e 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt @@ -2,7 +2,9 @@ package de.mm20.launcher2.ui.launcher.search.common.customattrs import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData +import de.mm20.launcher2.customattrs.CustomAttributesRepository import de.mm20.launcher2.customattrs.CustomIcon +import de.mm20.launcher2.customattrs.customAttrsModule import de.mm20.launcher2.icons.CustomIconWithPreview import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.LauncherIcon @@ -17,6 +19,7 @@ class CustomizeSearchableSheetVM( private val searchable: Searchable ) : KoinComponent { private val iconRepository: IconRepository by inject() + private val customAttributesRepository: CustomAttributesRepository by inject() val isIconPickerOpen = MutableLiveData(false) @@ -65,4 +68,12 @@ class CustomizeSearchableSheetVM( } } } + + fun setCustomLabel(label: String) { + if (label.isBlank()) { + customAttributesRepository.clearCustomLabel(searchable) + } else { + customAttributesRepository.setCustomLabel(searchable, label) + } + } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt index d02ba4d8..289e5a58 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt @@ -98,7 +98,7 @@ fun GridItem(modifier: Modifier = Modifier, item: Searchable, showLabels: Boolea modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), - text = item.label, + text = item.labelOverride ?: item.label, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodySmall, maxLines = 1, diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt index a30fd98b..dd1340a2 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt @@ -80,7 +80,7 @@ fun ContactItem( else MaterialTheme.typography.titleSmall ) Text( - text = contact.label, + text = contact.labelOverride ?: contact.label, style = textStyle, maxLines = 1, overflow = TextOverflow.Ellipsis diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt index 3bed2ebc..280abfb3 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt @@ -72,7 +72,7 @@ fun FileItem( else MaterialTheme.typography.titleSmall ) Text( - text = file.label, + text = file.labelOverride ?: file.label, style = textStyle, maxLines = 1, overflow = TextOverflow.Ellipsis diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt index e10c1802..8b761061 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt @@ -65,7 +65,7 @@ fun AppShortcutItem( ) { val titleStyle by animateTextStyleAsState(if (showDetails) MaterialTheme.typography.titleMedium else MaterialTheme.typography.titleSmall) Text( - text = shortcut.label, + text = shortcut.labelOverride ?: shortcut.label, style = titleStyle, maxLines = 1, overflow = TextOverflow.Ellipsis