Search custom labels
This commit is contained in:
parent
f3c1cc8dcd
commit
15837b0cdc
@ -44,5 +44,5 @@ dependencies {
|
|||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":crashreporter"))
|
implementation(project(":crashreporter"))
|
||||||
|
implementation(project(":favorites"))
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ import android.util.Log
|
|||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
||||||
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@ -21,12 +22,15 @@ interface CustomAttributesRepository {
|
|||||||
fun setCustomLabel(searchable: Searchable, label: String)
|
fun setCustomLabel(searchable: Searchable, label: String)
|
||||||
fun clearCustomLabel(searchable: Searchable)
|
fun clearCustomLabel(searchable: Searchable)
|
||||||
|
|
||||||
|
suspend fun search(query: String): Flow<List<Searchable>>
|
||||||
|
|
||||||
suspend fun export(toDir: File)
|
suspend fun export(toDir: File)
|
||||||
suspend fun import(fromDir: File)
|
suspend fun import(fromDir: File)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class CustomAttributesRepositoryImpl(
|
internal class CustomAttributesRepositoryImpl(
|
||||||
private val appDatabase: AppDatabase,
|
private val appDatabase: AppDatabase,
|
||||||
|
private val favoritesRepository: FavoritesRepository
|
||||||
) : CustomAttributesRepository {
|
) : CustomAttributesRepository {
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
|
|
||||||
@ -59,6 +63,7 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
override fun setCustomLabel(searchable: Searchable, label: String) {
|
override fun setCustomLabel(searchable: Searchable, label: String) {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
favoritesRepository.save(searchable)
|
||||||
appDatabase.runInTransaction {
|
appDatabase.runInTransaction {
|
||||||
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
|
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
|
||||||
dao.setCustomAttribute(
|
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) {
|
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
|
||||||
val dao = appDatabase.backupDao()
|
val dao = appDatabase.backupDao()
|
||||||
var page = 0
|
var page = 0
|
||||||
|
|||||||
@ -3,5 +3,5 @@ package de.mm20.launcher2.customattrs
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val customAttrsModule = module {
|
val customAttrsModule = module {
|
||||||
single<CustomAttributesRepository> { CustomAttributesRepositoryImpl(get()) }
|
single<CustomAttributesRepository> { CustomAttributesRepositoryImpl(get(), get()) }
|
||||||
}
|
}
|
||||||
@ -19,4 +19,7 @@ interface CustomAttrsDao {
|
|||||||
|
|
||||||
@Query("SELECT * FROM CustomAttributes WHERE type = :type AND key IN (:keys)")
|
@Query("SELECT * FROM CustomAttributes WHERE type = :type AND key IN (:keys)")
|
||||||
fun getCustomAttributes(keys: List<String>, type: String) : Flow<List<CustomAttributeEntity>>
|
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>>
|
||||||
}
|
}
|
||||||
@ -106,6 +106,9 @@ interface SearchDao {
|
|||||||
@Query("SELECT * FROM Searchable WHERE `key` = :key")
|
@Query("SELECT * FROM Searchable WHERE `key` = :key")
|
||||||
fun getFavorite(key: String): FavoritesItemEntity?
|
fun getFavorite(key: String): FavoritesItemEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Searchable WHERE `key` IN (:keys)")
|
||||||
|
fun getFromKeys(keys: List<String>): List<FavoritesItemEntity>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insertReplaceExisting(toDatabaseEntity: FavoritesItemEntity)
|
fun insertReplaceExisting(toDatabaseEntity: FavoritesItemEntity)
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,18 @@ interface FavoritesRepository {
|
|||||||
fun getHiddenItemKeys(): Flow<List<String>>
|
fun getHiddenItemKeys(): Flow<List<String>>
|
||||||
fun remove(searchable: Searchable)
|
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 export(toDir: File)
|
||||||
suspend fun import(fromDir: 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 {
|
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
|
||||||
val deserializer: SearchableDeserializer =
|
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) {
|
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
|
||||||
val dao = database.backupDao()
|
val dao = database.backupDao()
|
||||||
var page = 0
|
var page = 0
|
||||||
@ -246,7 +278,8 @@ internal class FavoritesRepositoryImpl(
|
|||||||
val dao = database.backupDao()
|
val dao = database.backupDao()
|
||||||
dao.wipeFavorites()
|
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) {
|
for (file in files) {
|
||||||
val favorites = mutableListOf<FavoritesItemEntity>()
|
val favorites = mutableListOf<FavoritesItemEntity>()
|
||||||
|
|||||||
@ -43,7 +43,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":icons"))
|
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":crashreporter"))
|
implementation(project(":crashreporter"))
|
||||||
}
|
}
|
||||||
@ -113,49 +113,55 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
hideFavorites.postValue(query.isNotEmpty())
|
hideFavorites.postValue(query.isNotEmpty())
|
||||||
searchJob = viewModelScope.launch {
|
searchJob = viewModelScope.launch {
|
||||||
isSearching.postValue(true)
|
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>>()
|
val jobs = mutableListOf<Deferred<Any>>()
|
||||||
jobs += async(Dispatchers.Default) {
|
jobs += async(Dispatchers.Default) {
|
||||||
appRepository
|
appRepository
|
||||||
.search(query)
|
.search(query)
|
||||||
|
.withCustomAttributeResults(customAttrResults)
|
||||||
.withCustomLabels()
|
.withCustomLabels()
|
||||||
.sorted()
|
.sorted()
|
||||||
.collectLatest { apps ->
|
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
|
||||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
appResults.postValue(results)
|
||||||
val results = apps.partition { !hiddenKeys.contains(it.key) }
|
hiddenItems.update {
|
||||||
appResults.postValue(results.first)
|
it.copy(apps = hidden)
|
||||||
hiddenItems.update {
|
|
||||||
it.copy(apps = results.second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jobs += async(Dispatchers.Default) {
|
jobs += async(Dispatchers.Default) {
|
||||||
contactRepository
|
contactRepository
|
||||||
.search(query)
|
.search(query)
|
||||||
|
.withCustomAttributeResults(customAttrResults)
|
||||||
.withCustomLabels()
|
.withCustomLabels()
|
||||||
.sorted()
|
.sorted()
|
||||||
.collectLatest { contacts ->
|
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
|
||||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
contactResults.postValue(results)
|
||||||
val results = contacts.partition { !hiddenKeys.contains(it.key) }
|
hiddenItems.update {
|
||||||
contactResults.postValue(results.first)
|
it.copy(contacts = hidden)
|
||||||
hiddenItems.update {
|
|
||||||
it.copy(contacts = results.second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jobs += async(Dispatchers.Default) {
|
jobs += async(Dispatchers.Default) {
|
||||||
calendarRepository
|
calendarRepository
|
||||||
.search(query)
|
.search(query)
|
||||||
|
.withCustomAttributeResults(customAttrResults)
|
||||||
.withCustomLabels()
|
.withCustomLabels()
|
||||||
.sorted()
|
.sorted()
|
||||||
.collectLatest { events ->
|
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
|
||||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
calendarResults.postValue(results)
|
||||||
val results = events.partition { !hiddenKeys.contains(it.key) }
|
hiddenItems.update {
|
||||||
calendarResults.postValue(results.first)
|
it.copy(calendarEvents = hidden)
|
||||||
hiddenItems.update {
|
|
||||||
it.copy(calendarEvents = results.second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,15 +188,13 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
jobs += async(Dispatchers.Default) {
|
jobs += async(Dispatchers.Default) {
|
||||||
fileRepository
|
fileRepository
|
||||||
.search(query)
|
.search(query)
|
||||||
|
.withCustomAttributeResults(customAttrResults)
|
||||||
.withCustomLabels()
|
.withCustomLabels()
|
||||||
.sorted()
|
.sorted()
|
||||||
.collectLatest { files ->
|
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
|
||||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
fileResults.postValue(results)
|
||||||
val results = files.partition { !hiddenKeys.contains(it.key) }
|
hiddenItems.update {
|
||||||
fileResults.postValue(results.first)
|
it.copy(files = hidden)
|
||||||
hiddenItems.update {
|
|
||||||
it.copy(files = results.second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,11 +206,13 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
jobs += async(Dispatchers.Default) {
|
jobs += async(Dispatchers.Default) {
|
||||||
appShortcutRepository
|
appShortcutRepository
|
||||||
.search(query)
|
.search(query)
|
||||||
|
.withCustomAttributeResults(customAttrResults)
|
||||||
.withCustomLabels()
|
.withCustomLabels()
|
||||||
.sorted()
|
.sorted()
|
||||||
.collectLatest { shortcuts ->
|
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
|
||||||
hiddenItemKeys.collectLatest { hidden ->
|
appShortcutResults.postValue(results)
|
||||||
appShortcutResults.postValue(shortcuts.filter { !hidden.contains(it.key) })
|
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() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user