From a1e8c20b2b238721f3651dafc37d2f6605dc87ea Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 31 Jul 2022 14:36:49 +0200 Subject: [PATCH] Improve icon picker, add icon search --- .../de/mm20/launcher2/database/IconDao.kt | 4 +- i18n/src/main/res/values/strings.xml | 4 + icons/build.gradle.kts | 2 - .../mm20/launcher2/icons/IconPackManager.kt | 16 +- .../launcher2/icons/IconPackPagingSource.kt | 54 ----- .../de/mm20/launcher2/icons/IconRepository.kt | 44 ++-- .../LauncherIconTransformation.kt | 16 +- settings.gradle.kts | 8 - ui/build.gradle.kts | 1 - .../customattrs/CustomizeSearchableSheet.kt | 202 ++++++++++++------ .../customattrs/CustomizeSearchableSheetVM.kt | 36 ++-- 11 files changed, 209 insertions(+), 178 deletions(-) delete mode 100644 icons/src/main/java/de/mm20/launcher2/icons/IconPackPagingSource.kt diff --git a/database/src/main/java/de/mm20/launcher2/database/IconDao.kt b/database/src/main/java/de/mm20/launcher2/database/IconDao.kt index acb2491b..85435747 100644 --- a/database/src/main/java/de/mm20/launcher2/database/IconDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/IconDao.kt @@ -19,8 +19,8 @@ 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 iconPack = :iconPack AND (type = 'app' OR type = 'calendar') LIMIT :limit OFFSET :offset") - suspend fun getIcons(iconPack: String, offset: Int, limit: Int): List + @Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND drawable LIKE :query ORDER BY iconPack, drawable LIMIT :limit") + suspend fun searchIconPackIcons(query: String, limit: Int = 100): List @Query("DELETE FROM Icons WHERE iconPack = :iconPack") fun deleteIcons(iconPack: String) diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index a6d8c88c..f7de34df 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -623,4 +623,8 @@ The backup has been restored. Pick icon + Default + Suggestions + Icon packs + Search icon \ No newline at end of file diff --git a/icons/build.gradle.kts b/icons/build.gradle.kts index 2bdb2f2f..a475d2be 100644 --- a/icons/build.gradle.kts +++ b/icons/build.gradle.kts @@ -40,8 +40,6 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.palette) - implementation(libs.androidx.paging.common) - implementation(libs.materialcomponents.core) implementation(libs.bundles.androidx.lifecycle) diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt index 70bf3030..2dc42df2 100644 --- a/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt @@ -15,6 +15,7 @@ import android.util.Log import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toBitmap import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.customattrs.CustomIconPackIcon import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.ktx.randomElementOrNull import kotlinx.coroutines.Dispatchers @@ -185,18 +186,12 @@ class IconPackManager( ) } - suspend fun getIcons(componentName: ComponentName): List { + suspend fun getAllIconPackIcons(componentName: ComponentName): List { val iconDao = appDatabase.iconDao() return iconDao.getIconsFromAllPacks(componentName.flattenToString()) .map { IconPackIcon(it) } } - suspend fun getIcons(iconPack: String, offset: Int, limit: Int): List { - val iconDao = appDatabase.iconDao() - return iconDao.getIcons(iconPack, offset, limit) - .map { IconPackIcon(it) } - } - private suspend fun getIconBack(iconPack: String): String? { val iconDao = appDatabase.iconDao() val iconbacks = iconDao.getIconBacks(iconPack) @@ -242,6 +237,13 @@ class IconPackManager( ) } + suspend fun searchIconPackIcon(query: String): List { + val iconDao = appDatabase.iconDao() + return iconDao.searchIconPackIcons("%$query%").map { + IconPackIcon(it) + } + } + } diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconPackPagingSource.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconPackPagingSource.kt deleted file mode 100644 index 7ab967b6..00000000 --- a/icons/src/main/java/de/mm20/launcher2/icons/IconPackPagingSource.kt +++ /dev/null @@ -1,54 +0,0 @@ -package de.mm20.launcher2.icons - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import de.mm20.launcher2.customattrs.CustomIconPackIcon -import de.mm20.launcher2.icons.transformations.LauncherIconTransformation -import de.mm20.launcher2.icons.transformations.apply -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -internal class IconPackPagingSource( - private val iconPackManager: IconPackManager, - private val iconPack: String, - private val transformations: List -) : PagingSource() { - override fun getRefreshKey(state: PagingState): Int? { - return null - } - - override suspend fun load(params: LoadParams): LoadResult { - val page = params.key ?: 0 - - val icons = withContext(Dispatchers.IO) { - iconPackManager.getIcons(iconPack, page, page + params.loadSize) - } - - val customIcons = mutableListOf() - withContext(Dispatchers.Default) { - for (icon in icons) { - val data = CustomIconPackIcon(iconPack, icon.componentName?.flattenToString() ?: continue) - - val ic = iconPackManager.getIcon( - iconPack, - icon.componentName - ) ?: continue - - customIcons.add( - CustomIconWithPreview( - preview = transformations.apply(ic), - customIcon = data, - ) - ) - } - } - - return LoadResult.Page( - data = customIcons, - prevKey = if (page > 0) page - 1 else null, - nextKey = if (icons.size == params.loadSize) page + 1 else null - ) - - } - -} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt index 0ec34cc6..6b52c5b4 100644 --- a/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt @@ -6,12 +6,11 @@ import android.content.Intent import android.content.IntentFilter import android.graphics.Color import android.util.LruCache -import androidx.paging.PagingSource import de.mm20.launcher2.customattrs.* import de.mm20.launcher2.icons.providers.* import de.mm20.launcher2.icons.transformations.LauncherIconTransformation import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation -import de.mm20.launcher2.icons.transformations.apply +import de.mm20.launcher2.icons.transformations.transform import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.Searchable @@ -117,7 +116,7 @@ class IconRepository( icon = provs.getFirstIcon(searchable, size) if (icon != null) { - icon = transforms.apply(icon) + icon = icon.transform(transforms) cache.put(searchable.key + customIcon.hashCode(), icon) send(icon) @@ -181,15 +180,6 @@ class IconRepository( val defaultTransformations = transformations.first() - val defaultTransformedIcon = defaultTransformations.apply(rawIcon) - - suggestions.add( - CustomIconWithPreview( - defaultTransformedIcon, - null, - ) - ) - val customIcons = mutableListOf(UnmodifiedSystemDefaultIcon) if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) { @@ -230,7 +220,7 @@ class IconRepository( val icon = providers.getFirstIcon(searchable, size) ?: rawIcon CustomIconWithPreview( - preview = transformations.apply(icon), + preview = icon.transform(transformations), customIcon = it, ) @@ -240,7 +230,7 @@ class IconRepository( val providerOptions = mutableListOf() if (searchable is LauncherApp) { - val iconPackIcons = iconPackManager.getIcons( + val iconPackIcons = iconPackManager.getAllIconPackIcons( searchable.launcherActivityInfo.componentName ) @@ -262,7 +252,7 @@ class IconRepository( val icon = providers.getFirstIcon(searchable, size) ?: return@mapNotNull null CustomIconWithPreview( - preview = defaultTransformations.apply(icon), + preview = icon.transform(defaultTransformations), customIcon = it, ) @@ -273,8 +263,28 @@ class IconRepository( } - suspend fun getAllIconsFromPack(iconPack: String): PagingSource { - return IconPackPagingSource(iconPackManager, iconPack, transformations.first()) + suspend fun getUncustomizedDefaultIcon(searchable: Searchable, size: Int): CustomIconWithPreview? { + val icon = iconProviders.first().getFirstIcon(searchable, size) + ?.transform(transformations.first()) ?: return null + return CustomIconWithPreview( + customIcon = null, + preview = icon + ) + } + + suspend fun searchIconPackIcon(query: String): List { + val transformations = this.transformations.first() + return iconPackManager.searchIconPackIcon(query).mapNotNull { + val componentName = it.componentName ?: return@mapNotNull null + + CustomIconWithPreview( + customIcon = CustomIconPackIcon( + iconPackPackage = it.iconPack, + iconComponentName = componentName.flattenToString(), + ), + preview = iconPackManager.getIcon(it.iconPack, componentName)?.transform(transformations) ?: return@mapNotNull null + ) + } } fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) { diff --git a/icons/src/main/java/de/mm20/launcher2/icons/transformations/LauncherIconTransformation.kt b/icons/src/main/java/de/mm20/launcher2/icons/transformations/LauncherIconTransformation.kt index 68de850b..fc3d21d6 100644 --- a/icons/src/main/java/de/mm20/launcher2/icons/transformations/LauncherIconTransformation.kt +++ b/icons/src/main/java/de/mm20/launcher2/icons/transformations/LauncherIconTransformation.kt @@ -8,17 +8,17 @@ internal interface LauncherIconTransformation { suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon } -internal suspend fun Iterable.apply(icon: LauncherIcon): LauncherIcon { - if (icon is StaticLauncherIcon) { - var transformedIcon = icon - for (transformation in this) { +internal suspend fun LauncherIcon.transform(transformations: Iterable): LauncherIcon { + if (this is StaticLauncherIcon) { + var transformedIcon = this + for (transformation in transformations) { transformedIcon = transformation.transform(transformedIcon as StaticLauncherIcon) } return transformedIcon } - if (icon is TransformableDynamicLauncherIcon) { - icon.setTransformations(this.toList()) - return icon + if (this is TransformableDynamicLauncherIcon) { + this.setTransformations(transformations.toList()) + return this } - return icon + return this } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1f922478..5ddafaef 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -238,14 +238,6 @@ dependencyResolutionManagement { .to("androidx.navigation", "navigation-compose") .version("2.5.0-rc02") - alias("androidx.paging.common") - .to("androidx.paging", "paging-common-ktx") - .version("3.2.0-alpha01") - alias("androidx.paging.compose") - .to("androidx.paging", "paging-compose") - .version("1.0.0-alpha15") - - alias("materialcomponents.core") .to("com.google.android.material", "material") .version("1.6.0-beta01") diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index ef614a61..04f85233 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -64,7 +64,6 @@ dependencies { implementation(libs.androidx.compose.animationgraphics) implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.paging.compose) implementation(libs.composecolorpicker) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt index e4cc1964..1d3f62ca 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt @@ -2,41 +2,34 @@ package de.mm20.launcher2.ui.launcher.search.common.customattrs import android.graphics.drawable.InsetDrawable import androidx.appcompat.content.res.AppCompatResources -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.paging.compose.itemsIndexed -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.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Clear +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext 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 androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems import de.mm20.launcher2.badges.Badge +import de.mm20.launcher2.icons.CustomIconWithPreview import de.mm20.launcher2.search.data.Searchable 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.ktx.toPixels import de.mm20.launcher2.ui.locals.LocalGridColumns +import kotlinx.coroutines.launch @Composable fun CustomizeSearchableSheet( @@ -111,69 +104,146 @@ fun CustomizeSearchableSheet( } else { val iconSize = 48.dp val iconSizePx = iconSize.toPixels() - val suggestions by - remember { viewModel.getIconSuggestions(iconSizePx.toInt()) } + + val scope = rememberCoroutineScope() + + val suggestions by remember { viewModel.getIconSuggestions(iconSizePx.toInt()) } .observeAsState(emptyList()) - val iconPackIcons by remember { - viewModel.getAllIconsFromAllIconPacks() - }.observeAsState(emptyList()) + val defaultIcon by remember { + viewModel.getDefaultIcon(iconSizePx.toInt()) + }.observeAsState() - val pagingItems = iconPackIcons.map { - it.flow.collectAsLazyPagingItems() - } + var query by remember { mutableStateOf("") } + val isSearching by viewModel.isSearchingIcons.observeAsState(initial = false) + val iconResults by viewModel.iconSearchResults.observeAsState(emptyList()) - LazyVerticalGrid(columns = GridCells.Fixed(LocalGridColumns.current)) { - items(suggestions) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.padding(vertical = 8.dp) - ) { - ShapedLauncherIcon( - size = iconSize, - icon = it.preview, - onClick = { - viewModel.pickIcon(it.customIcon) + val columns = LocalGridColumns.current + + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(columns) + ) { + + item(span = { GridItemSpan(columns) }) { + OutlinedTextField( + modifier = Modifier.padding(bottom = 16.dp), + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = null + ) + }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { + query = "" + scope.launch { + viewModel.searchIcon("") + } + }) { + Icon( + imageVector = Icons.Rounded.Clear, + contentDescription = null + ) + } } + }, + value = query, + onValueChange = { + query = it + scope.launch { + viewModel.searchIcon(query) + } + }, + label = { + Text(stringResource(R.string.icon_picker_search_icon)) + } + ) + } + + if (query.isEmpty()) { + if (defaultIcon != null) { + item(span = { GridItemSpan(columns) }) { + Separator(stringResource(R.string.icon_picker_default_icon)) + } + item { + IconPreview(item = defaultIcon, iconSize = iconSize, onClick = { + viewModel.pickIcon(null) + }) + } + } + item(span = { GridItemSpan(columns) }) { + Separator(stringResource(R.string.icon_picker_suggestions)) + } + + items(suggestions) { + IconPreview( + it, + iconSize, + onClick = { viewModel.pickIcon(it.customIcon) } ) } - } - for (pager in pagingItems) { - itemsIndexed(pager) { index, item -> - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.padding(vertical = 8.dp) - ) { - ShapedLauncherIcon( - size = iconSize, - icon = item?.preview, - onClick = { - viewModel.pickIcon(item?.customIcon) - } - ) + } else { + + item(span = { GridItemSpan(columns) }) { + Separator(stringResource(R.string.icon_picker_packs)) + } + + items(iconResults) { + IconPreview( + it, + iconSize, + onClick = { viewModel.pickIcon(it.customIcon) } + ) + } + + if (isSearching) { + item(span = { GridItemSpan(columns) }) { + Box( + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier + .padding(12.dp) + .size(24.dp) + ) + } } } } + } } } } -fun LazyGridScope.itemsIndexed( - items: LazyPagingItems, - key: ((index: Int, item: T) -> Any)? = null, - itemContent: @Composable LazyGridScope.(index: Int, value: T?) -> Unit +@Composable +fun IconPreview( + item: CustomIconWithPreview?, + iconSize: Dp, + onClick: () -> Unit, ) { - items( - count = items.itemCount, - key = if (key == null) null else { index -> - val item = items.peek(index) - if (item == null) { - } else { - key(index, item) - } - } - ) { index -> - this@itemsIndexed.itemContent(index, items[index]) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.padding(vertical = 8.dp) + ) { + ShapedLauncherIcon( + size = iconSize, + icon = item?.preview, + onClick = onClick + ) } +} + +@Composable +fun Separator(label: String) { + Text( + label, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier + .padding(top = 16.dp, bottom = 8.dp) + .fillMaxWidth() + ) } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt index 280b5e24..62793b0f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt @@ -2,15 +2,16 @@ package de.mm20.launcher2.ui.launcher.search.common.customattrs import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData -import androidx.paging.Pager -import androidx.paging.PagingConfig import de.mm20.launcher2.customattrs.CustomIcon +import de.mm20.launcher2.icons.CustomIconWithPreview import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import kotlin.coroutines.coroutineContext class CustomizeSearchableSheetVM( private val searchable: Searchable @@ -40,19 +41,28 @@ class CustomizeSearchableSheetVM( closeIconPicker() } - fun getAllIconsFromAllIconPacks() = liveData { - emit(emptyList()) - val iconPacks = iconRepository.getInstalledIconPacks() + fun getDefaultIcon(size: Int) = liveData { + emit(iconRepository.getUncustomizedDefaultIcon(searchable, size)) + } - emit(iconPacks.map { - val source = iconRepository.getAllIconsFromPack(it.packageName) + val iconSearchResults = MutableLiveData(emptyList()) + val isSearchingIcons = MutableLiveData(false) - Pager( - PagingConfig(pageSize = 20, enablePlaceholders = false, maxSize = 200), - ) { - source + private var debounceSearchJob: Job? = null + suspend fun searchIcon(query: String) { + debounceSearchJob?.cancelAndJoin() + if (query.isBlank()) { + iconSearchResults.value = emptyList() + isSearchingIcons.value = false + return + } + withContext(coroutineContext) { + debounceSearchJob = launch { + delay(1000) + isSearchingIcons.value = true + iconSearchResults.value = iconRepository.searchIconPackIcon(query) + isSearchingIcons.value = false } - }) - + } } } \ No newline at end of file