From 6adbee224e2a199f329c28f6715255b7c4347c37 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:39:18 +0100 Subject: [PATCH] Icon picker: add icon pack filter --- .../sheets/CustomizeSearchableSheet.kt | 115 +++++++++++++++--- .../sheets/CustomizeSearchableSheetVM.kt | 27 ++-- .../de/mm20/launcher2/database/IconDao.kt | 10 +- core/i18n/src/main/res/values/strings.xml | 2 + .../mm20/launcher2/icons/IconPackManager.kt | 3 +- .../de/mm20/launcher2/icons/IconRepository.kt | 4 +- 6 files changed, 128 insertions(+), 33 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt index 5611df25..d5715f35 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt @@ -1,18 +1,40 @@ package de.mm20.launcher2.ui.launcher.sheets +import android.content.pm.PackageManager import android.graphics.drawable.InsetDrawable import androidx.appcompat.content.res.AppCompatResources -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Clear +import androidx.compose.material.icons.rounded.FilterAlt import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb @@ -21,13 +43,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.icons.CustomIconWithPreview +import de.mm20.launcher2.icons.IconPack import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.BottomSheetDialog -import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.OutlinedTagsInputField +import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.locals.LocalGridSettings import kotlinx.coroutines.flow.first @@ -42,7 +66,7 @@ fun CustomizeSearchableSheet( remember(searchable.key) { CustomizeSearchableSheetVM(searchable) } val context = LocalContext.current - val pickIcon by viewModel.isIconPickerOpen.observeAsState(false) + val pickIcon by viewModel.isIconPickerOpen BottomSheetDialog( onDismissRequest = onDismiss, @@ -156,8 +180,13 @@ fun CustomizeSearchableSheet( }.observeAsState() var query by remember { mutableStateOf("") } - val isSearching by viewModel.isSearchingIcons.observeAsState(initial = false) - val iconResults by viewModel.iconSearchResults.observeAsState(emptyList()) + var filterIconPack by remember { mutableStateOf(null) } + val isSearching by viewModel.isSearchingIcons + val iconResults by viewModel.iconSearchResults + + var showIconPackFilter by remember { mutableStateOf(false) } + val installedIconPacks by viewModel.installedIconPacks.collectAsState(null) + val noPacksInstalled = installedIconPacks?.isEmpty() == true val columns = LocalGridSettings.current.columnCount @@ -176,18 +205,67 @@ fun CustomizeSearchableSheet( contentDescription = null ) }, + enabled = !noPacksInstalled, + placeholder = { + Text( + stringResource( + if (noPacksInstalled) R.string.icon_picker_no_packs_installed else R.string.icon_picker_search_icon + ) + ) + }, trailingIcon = { - if (query.isNotEmpty()) { + if (query.isNotEmpty() && !installedIconPacks.isNullOrEmpty()) { IconButton(onClick = { - query = "" - scope.launch { - viewModel.searchIcon("") - } + showIconPackFilter = !showIconPackFilter }) { - Icon( - imageVector = Icons.Rounded.Clear, - contentDescription = null + if (filterIconPack == null) { + Icon( + imageVector = Icons.Rounded.FilterAlt, + contentDescription = null + ) + } else { + val icon = remember(filterIconPack?.packageName) { + try { + filterIconPack?.packageName?.let { pkg -> + context.packageManager.getApplicationIcon(pkg) + } + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + AsyncImage( + modifier = Modifier.size(24.dp), + model = icon, + contentDescription = null + ) + } + } + DropdownMenu( + expanded = showIconPackFilter, + onDismissRequest = { showIconPackFilter = false }) { + DropdownMenuItem( + text = { Text(stringResource(id = R.string.icon_picker_filter_all_packs)) }, + onClick = { + showIconPackFilter = false + filterIconPack = null + scope.launch { + viewModel.searchIcon(query, filterIconPack) + } + } ) + installedIconPacks?.forEach { iconPack -> + DropdownMenuItem( + onClick = { + showIconPackFilter = false + filterIconPack = iconPack + scope.launch { + viewModel.searchIcon(query, filterIconPack) + } + }, + text = { + Text(iconPack.name) + }) + } } } }, @@ -195,12 +273,9 @@ fun CustomizeSearchableSheet( onValueChange = { query = it scope.launch { - viewModel.searchIcon(query) + viewModel.searchIcon(query, filterIconPack) } }, - label = { - Text(stringResource(R.string.icon_picker_search_icon)) - }, singleLine = true, ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt index 0d53619e..30b85c39 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt @@ -1,16 +1,22 @@ package de.mm20.launcher2.ui.launcher.sheets -import androidx.lifecycle.MutableLiveData +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.liveData import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomIcon import de.mm20.launcher2.icons.CustomIconWithPreview +import de.mm20.launcher2.icons.IconPack import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.search.SavableSearchable -import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import kotlin.coroutines.coroutineContext @@ -21,7 +27,7 @@ class CustomizeSearchableSheetVM( private val iconRepository: IconRepository by inject() private val customAttributesRepository: CustomAttributesRepository by inject() - val isIconPickerOpen = MutableLiveData(false) + val isIconPickerOpen = mutableStateOf(false) fun getIcon(size: Int): Flow { return iconRepository.getIcon(searchable, size) @@ -48,11 +54,15 @@ class CustomizeSearchableSheetVM( emit(iconRepository.getUncustomizedDefaultIcon(searchable, size)) } - val iconSearchResults = MutableLiveData(emptyList()) - val isSearchingIcons = MutableLiveData(false) + val iconSearchResults = mutableStateOf(emptyList()) + val isSearchingIcons = mutableStateOf(false) + + val installedIconPacks = flow { + emit(iconRepository.getInstalledIconPacks().sortedBy { it.name }) + } private var debounceSearchJob: Job? = null - suspend fun searchIcon(query: String) { + suspend fun searchIcon(query: String, iconPack: IconPack?) { debounceSearchJob?.cancelAndJoin() if (query.isBlank()) { iconSearchResults.value = emptyList() @@ -61,9 +71,10 @@ class CustomizeSearchableSheetVM( } withContext(coroutineContext) { debounceSearchJob = launch { - delay(1000) + delay(500) isSearchingIcons.value = true - iconSearchResults.value = iconRepository.searchCustomIcons(query) + iconSearchResults.value = emptyList() + iconSearchResults.value = iconRepository.searchCustomIcons(query, iconPack) isSearchingIcons.value = false } } diff --git a/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt index 42c2b233..b00143b6 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/IconDao.kt @@ -19,8 +19,14 @@ interface IconDao { @Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')") suspend fun getIconsFromAllPacks(componentName: String): List - @Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND (drawable LIKE :drawableQuery OR componentName LIKE :componentQuery OR name LIKE :nameQuery) ORDER BY iconPack, drawable LIMIT :limit") - suspend fun searchIconPackIcons(componentQuery: String, nameQuery: String, drawableQuery: String, limit: Int = 100): List + @Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND (drawable LIKE :drawableQuery OR componentName LIKE :componentQuery OR name LIKE :nameQuery) AND (:iconPack IS NULL OR iconPack = :iconPack) ORDER BY iconPack, drawable LIMIT :limit") + suspend fun searchIconPackIcons( + componentQuery: String, + nameQuery: String, + drawableQuery: String, + iconPack: String?, + limit: Int = 100 + ): List @Query("SELECT * FROM Icons WHERE (type = 'greyscale_icon') AND componentName LIKE :query GROUP BY componentName ORDER BY drawable LIMIT :limit") suspend fun searchGreyscaleIcons(query: String, limit: Int = 100): List diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index bb4ab121..2931c873 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -681,6 +681,8 @@ Default Suggestions Search icon + No icon packs installed + All icon packs Personal Work Favorites diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt index d6fc8203..ca4fd4d5 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt @@ -427,13 +427,14 @@ class IconPackManager( return null } - suspend fun searchIconPackIcon(query: String): List { + suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List { val iconDao = appDatabase.iconDao() val drawableQuery = query.replace(" ", "_").lowercase() return iconDao.searchIconPackIcons( drawableQuery = "%$drawableQuery%", componentQuery = "%$query%", nameQuery = "%$query%", + iconPack = iconPack?.packageName, ).map { IconPackIcon(it) } diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt index 388d19fa..36c09b66 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt @@ -356,9 +356,9 @@ class IconRepository( ) } - suspend fun searchCustomIcons(query: String): List { + suspend fun searchCustomIcons(query: String, iconPack: IconPack?): List { val transformations = this.transformations.first() - val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull { + val iconPackIcons = iconPackManager.searchIconPackIcon(query, iconPack).mapNotNull { val componentName = it.componentName ?: return@mapNotNull null CustomIconWithPreview(