Custom labels
This commit is contained in:
parent
29a06dfc4f
commit
7b86677fc2
@ -130,7 +130,7 @@ internal class AppShortcutRepositoryImpl(
|
||||
it,
|
||||
label
|
||||
)
|
||||
}.sorted()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<CustomIcon?>
|
||||
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?)
|
||||
|
||||
fun getCustomLabels(items: List<Searchable>): Flow<List<CustomLabel>>
|
||||
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<Searchable>): Flow<List<CustomLabel>> {
|
||||
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<CustomAttributeEntity>()
|
||||
|
||||
@ -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<String>, type: String) : Flow<List<CustomAttributeEntity>>
|
||||
}
|
||||
@ -25,7 +25,7 @@ internal class GDriveFileProvider(
|
||||
viewUri = it.viewUri,
|
||||
metaData = getMetadata(it.metadata)
|
||||
)
|
||||
}.sorted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMetadata(file: DriveFileMeta): List<Pair<Int, String>> {
|
||||
|
||||
@ -61,6 +61,6 @@ internal class LocalFileProvider(
|
||||
results.add(file)
|
||||
}
|
||||
cursor.close()
|
||||
return@withContext results.sortedBy { it }
|
||||
return@withContext results
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,7 @@ internal class OneDriveFileProvider(
|
||||
webUrl = driveItem.webUrl
|
||||
)
|
||||
}
|
||||
return files.sorted()
|
||||
return files
|
||||
}
|
||||
|
||||
private fun getMetaData(driveItem: DriveItem): List<Pair<Int, String>> {
|
||||
|
||||
@ -16,6 +16,8 @@ abstract class Searchable : Comparable<Searchable> {
|
||||
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<Searchable> {
|
||||
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 {
|
||||
|
||||
@ -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<Deferred<Any>>()
|
||||
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 <T : Searchable> Flow<List<T>>.withCustomLabels(): Flow<List<T>> = 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(
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user