Search custom labels

This commit is contained in:
MM20 2022-09-13 20:53:46 +02:00
parent f3c1cc8dcd
commit 15837b0cdc
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 112 additions and 36 deletions

View File

@ -44,5 +44,5 @@ dependencies {
implementation(project(":search"))
implementation(project(":ktx"))
implementation(project(":crashreporter"))
implementation(project(":favorites"))
}

View File

@ -4,6 +4,7 @@ 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.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.*
@ -21,12 +22,15 @@ interface CustomAttributesRepository {
fun setCustomLabel(searchable: Searchable, label: String)
fun clearCustomLabel(searchable: Searchable)
suspend fun search(query: String): Flow<List<Searchable>>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
}
internal class CustomAttributesRepositoryImpl(
private val appDatabase: AppDatabase,
private val favoritesRepository: FavoritesRepository
) : CustomAttributesRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
@ -59,6 +63,7 @@ internal class CustomAttributesRepositoryImpl(
override fun setCustomLabel(searchable: Searchable, label: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
appDatabase.runInTransaction {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
dao.setCustomAttribute(
@ -78,6 +83,13 @@ internal class CustomAttributesRepositoryImpl(
}
}
override suspend fun search(query: String): Flow<List<Searchable>> {
val dao = appDatabase.customAttrsDao()
return dao.search("%$query%").map {
favoritesRepository.getFromKeys(it)
}
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = appDatabase.backupDao()
var page = 0

View File

@ -3,5 +3,5 @@ package de.mm20.launcher2.customattrs
import org.koin.dsl.module
val customAttrsModule = module {
single<CustomAttributesRepository> { CustomAttributesRepositoryImpl(get()) }
single<CustomAttributesRepository> { CustomAttributesRepositoryImpl(get(), get()) }
}

View File

@ -19,4 +19,7 @@ interface CustomAttrsDao {
@Query("SELECT * FROM CustomAttributes WHERE type = :type AND key IN (:keys)")
fun getCustomAttributes(keys: List<String>, type: String) : Flow<List<CustomAttributeEntity>>
@Query("SELECT DISTINCT key FROM CustomAttributes WHERE (type = 'label' OR type = 'tag') AND value LIKE :query")
fun search(query: String): Flow<List<String>>
}

View File

@ -106,6 +106,9 @@ interface SearchDao {
@Query("SELECT * FROM Searchable WHERE `key` = :key")
fun getFavorite(key: String): FavoritesItemEntity?
@Query("SELECT * FROM Searchable WHERE `key` IN (:keys)")
fun getFromKeys(keys: List<String>): List<FavoritesItemEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertReplaceExisting(toDatabaseEntity: FavoritesItemEntity)

View File

@ -41,6 +41,18 @@ interface FavoritesRepository {
fun getHiddenItemKeys(): Flow<List<String>>
fun remove(searchable: Searchable)
/**
* Ensure that this searchable exists in the Favorites table.
* If it doesn't exist, insert it with 0 launch count, not pinned and not hidden
*/
fun save(searchable: Searchable)
/**
* Get items with the given keys from the favorites database.
* Items that don't exist in the database will not be returned.
*/
fun getFromKeys(keys: List<String>): List<Searchable>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
}
@ -203,6 +215,21 @@ internal class FavoritesRepositoryImpl(
}
}
override fun save(searchable: Searchable) {
scope.launch {
withContext(Dispatchers.IO) {
val entity = FavoritesItem(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
).toDatabaseEntity() ?: return@withContext
database.searchDao().insertSkipExisting(entity)
}
}
}
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
val deserializer: SearchableDeserializer =
@ -216,6 +243,11 @@ internal class FavoritesRepositoryImpl(
)
}
override fun getFromKeys(keys: List<String>): List<Searchable> {
val dao = database.searchDao()
return dao.getFromKeys(keys).mapNotNull { fromDatabaseEntity(it).searchable }
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao()
var page = 0
@ -246,7 +278,8 @@ internal class FavoritesRepositoryImpl(
val dao = database.backupDao()
dao.wipeFavorites()
val files = fromDir.listFiles { _, name -> name.startsWith("favorites.") } ?: return@withContext
val files =
fromDir.listFiles { _, name -> name.startsWith("favorites.") } ?: return@withContext
for (file in files) {
val favorites = mutableListOf<FavoritesItemEntity>()

View File

@ -43,7 +43,6 @@ dependencies {
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":icons"))
implementation(project(":search"))
implementation(project(":crashreporter"))
}

View File

@ -113,49 +113,55 @@ class SearchVM : ViewModel(), KoinComponent {
hideFavorites.postValue(query.isNotEmpty())
searchJob = viewModelScope.launch {
isSearching.postValue(true)
val customAttrResults = customAttributesRepository.search(query)
.combine(dataStore.data) { items, settings ->
items.filter {
it is Application
|| it is Contact && settings.contactsSearch.enabled
|| it is CalendarEvent && settings.calendarSearch.enabled
|| it is AppShortcut && settings.appShortcutSearch.enabled
|| it is LocalFile && settings.fileSearch.localFiles
|| it is GDriveFile && settings.fileSearch.gdrive
|| it is OneDriveFile && settings.fileSearch.onedrive
}
}
val jobs = mutableListOf<Deferred<Any>>()
jobs += async(Dispatchers.Default) {
appRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels()
.sorted()
.collectLatest { apps ->
hiddenItemKeys.collectLatest { hiddenKeys ->
val results = apps.partition { !hiddenKeys.contains(it.key) }
appResults.postValue(results.first)
hiddenItems.update {
it.copy(apps = results.second)
}
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
appResults.postValue(results)
hiddenItems.update {
it.copy(apps = hidden)
}
}
}
jobs += async(Dispatchers.Default) {
contactRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels()
.sorted()
.collectLatest { contacts ->
hiddenItemKeys.collectLatest { hiddenKeys ->
val results = contacts.partition { !hiddenKeys.contains(it.key) }
contactResults.postValue(results.first)
hiddenItems.update {
it.copy(contacts = results.second)
}
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
contactResults.postValue(results)
hiddenItems.update {
it.copy(contacts = hidden)
}
}
}
jobs += async(Dispatchers.Default) {
calendarRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels()
.sorted()
.collectLatest { events ->
hiddenItemKeys.collectLatest { hiddenKeys ->
val results = events.partition { !hiddenKeys.contains(it.key) }
calendarResults.postValue(results.first)
hiddenItems.update {
it.copy(calendarEvents = results.second)
}
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
calendarResults.postValue(results)
hiddenItems.update {
it.copy(calendarEvents = hidden)
}
}
}
@ -182,15 +188,13 @@ class SearchVM : ViewModel(), KoinComponent {
jobs += async(Dispatchers.Default) {
fileRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels()
.sorted()
.collectLatest { files ->
hiddenItemKeys.collectLatest { hiddenKeys ->
val results = files.partition { !hiddenKeys.contains(it.key) }
fileResults.postValue(results.first)
hiddenItems.update {
it.copy(files = results.second)
}
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
fileResults.postValue(results)
hiddenItems.update {
it.copy(files = hidden)
}
}
}
@ -202,11 +206,13 @@ class SearchVM : ViewModel(), KoinComponent {
jobs += async(Dispatchers.Default) {
appShortcutRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels()
.sorted()
.collectLatest { shortcuts ->
hiddenItemKeys.collectLatest { hidden ->
appShortcutResults.postValue(shortcuts.filter { !hidden.contains(it.key) })
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
appShortcutResults.postValue(results)
hiddenItems.update {
it.copy(appShortcuts = hidden)
}
}
}
@ -313,7 +319,27 @@ class SearchVM : ViewModel(), KoinComponent {
}
}
private fun <T: Searchable> Flow<List<T>>.sorted(): Flow<List<T>> = this.map { it.sorted() }
private inline fun <reified T : Searchable> Flow<List<T>>.withCustomAttributeResults(
customAttributeResults: Flow<List<Searchable>>
): Flow<List<T>> {
return this.combine(customAttributeResults) { items, items2 ->
(items + items2.filterIsInstance<T>()).distinctBy { it.key }
}
}
private suspend fun <T : Searchable> Flow<List<T>>.collectWithHiddenItems(
hiddenItemKeys: Flow<List<String>>,
action: (items: List<T>, hidden: List<T>) -> Unit
) {
return collectLatest { items ->
hiddenItemKeys.collectLatest { hiddenKeys ->
val (results, hidden) = items.partition { !hiddenKeys.contains(it.key) }
action(results, hidden)
}
}
}
private fun <T : Searchable> Flow<List<T>>.sorted(): Flow<List<T>> = this.map { it.sorted() }
}