Custom labels

This commit is contained in:
MM20 2022-09-11 16:17:39 +02:00
parent 29a06dfc4f
commit 7b86677fc2
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
18 changed files with 161 additions and 63 deletions

View File

@ -130,7 +130,7 @@ internal class AppShortcutRepositoryImpl(
it,
label
)
}.sorted()
}
)
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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>()

View File

@ -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>>
}

View File

@ -25,7 +25,7 @@ internal class GDriveFileProvider(
viewUri = it.viewUri,
metaData = getMetadata(it.metadata)
)
}.sorted()
}
}
private fun getMetadata(file: DriveFileMeta): List<Pair<Int, String>> {

View File

@ -61,6 +61,6 @@ internal class LocalFileProvider(
results.add(file)
}
cursor.close()
return@withContext results.sortedBy { it }
return@withContext results
}
}

View File

@ -27,7 +27,7 @@ internal class OneDriveFileProvider(
webUrl = driveItem.webUrl
)
}
return files.sorted()
return files
}
private fun getMetaData(driveItem: DriveItem): List<Pair<Int, String>> {

View File

@ -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 {

View File

@ -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(

View File

@ -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),

View File

@ -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),

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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