From 3c489fc648c7a591ad372652dfbfa4c8fa765f8a Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 20 Apr 2024 16:50:45 +0200 Subject: [PATCH] Redesign icon pack selector --- .../ui/settings/icons/IconsSettingsScreen.kt | 191 +++++++++++++----- .../settings/icons/IconsSettingsScreenVM.kt | 105 ++++++++-- 2 files changed, 231 insertions(+), 65 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt index 09b4ae05..794e916d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt @@ -4,19 +4,24 @@ import android.graphics.drawable.ColorDrawable import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.FormatPaint +import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -30,32 +35,33 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.icons.IconPack +import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.StaticIconLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.preferences.IconShape import de.mm20.launcher2.preferences.ui.GridSettings import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.getShape -import de.mm20.launcher2.ui.component.preferences.ListPreference import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.SliderPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference -import de.mm20.launcher2.ui.component.preferences.label -import de.mm20.launcher2.ui.component.preferences.value +import kotlinx.coroutines.flow.Flow @Composable fun IconsSettingsScreen() { @@ -69,6 +75,8 @@ fun IconsSettingsScreen() { val installedIconPacks by viewModel.installedIconPacks.collectAsState(emptyList()) + var showIconPackSheet by remember { mutableStateOf(false) } + val hasNotificationsPermission by viewModel.hasNotificationsPermission.collectAsStateWithLifecycle( null ) @@ -79,8 +87,10 @@ fun IconsSettingsScreen() { val shortcutBadges by viewModel.shortcutBadges.collectAsStateWithLifecycle(null) val pluginBadges by viewModel.pluginBadges.collectAsStateWithLifecycle(null) - val previewIcons by remember(grid?.iconSize) { - viewModel.getPreviewIcons(with(density) { grid.iconSize.dp.toPx() }.toInt()) + val iconSize = with(density) { grid.iconSize.dp.toPx() }.toInt() + + val previewIcons = remember(iconSize) { + viewModel.getPreviewIcons(iconSize) }.collectAsState( emptyList() ) @@ -119,7 +129,7 @@ fun IconsSettingsScreen() { } item { PreferenceCategory(stringResource(R.string.preference_category_icons)) { - if (previewIcons.isNotEmpty()) { + if (previewIcons.value.isNotEmpty()) { Surface( modifier = Modifier .fillMaxWidth() @@ -131,7 +141,7 @@ fun IconsSettingsScreen() { Row( modifier = Modifier.padding(vertical = 24.dp, horizontal = 8.dp) ) { - for (icon in previewIcons) { + for (icon in previewIcons.value) { Box( modifier = Modifier.weight(1f), contentAlignment = Alignment.Center @@ -181,55 +191,17 @@ fun IconsSettingsScreen() { val items = installedIconPacks.map { it.name to it } - ListPreference( + Preference( title = stringResource(R.string.preference_icon_pack), - items = items, summary = if (items.size <= 1) { stringResource(R.string.preference_icon_pack_summary_empty) } else { iconPack?.name ?: "System" }, enabled = installedIconPacks.size > 1, - value = iconPack, - onValueChanged = { - if (it != null) viewModel.setIconPack(it.packageName) + onClick = { + showIconPackSheet = true }, - itemLabel = { - Column( - verticalArrangement = Arrangement.Center, - ) { - Text( - text = it.label, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - if (it.value?.themed == true) { - Surface( - shape = MaterialTheme.shapes.extraSmall, - color = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.padding(top = 4.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier - .size(20.dp) - .padding(end = 4.dp), - imageVector = Icons.Rounded.FormatPaint, - contentDescription = null, - ) - Text( - text = stringResource(R.string.icon_pack_dynamic_colors), - style = MaterialTheme.typography.labelSmall - ) - } - } - } - - } - } ) } } @@ -290,6 +262,125 @@ fun IconsSettingsScreen() { } } } + + if (showIconPackSheet) { + val iconPackPreviewIcons = remember(installedIconPacks, iconSize) { + + installedIconPacks.associate { + it.packageName to viewModel.getIconPackPreviewIcons( + context, + it, + grid.columnCount, + iconSize, + icons?.themedIcons == true + ) + } + } + + IconPackSelectorSheet( + installedIconPacks, + iconPackPreviewIcons = iconPackPreviewIcons, + columns = grid.columnCount, + onSelect = { + viewModel.setIconPack(it.packageName) + showIconPackSheet = false + }, + onDismiss = { showIconPackSheet = false }, + ) + } +} + +@Composable +private fun IconPackSelectorSheet( + installedIconPacks: List, + iconPackPreviewIcons: Map>>, + columns: Int, + onSelect: (IconPack) -> Unit, + onDismiss: () -> Unit, +) { + BottomSheetDialog(onDismissRequest = onDismiss) { + LazyColumn( + contentPadding = it, + ) { + items(installedIconPacks.size) { + + val pack = installedIconPacks[it] + + + if (it > 0) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp) + ) + } + Column { + + + Column( + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .clickable { + onSelect(pack) + } + ) { + val icons by iconPackPreviewIcons[pack.packageName]!!.collectAsState(null) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 8.dp) + ) { + Text( + text = pack.name, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(end = 8.dp) + ) + if (pack.themed) { + Icon( + modifier = Modifier + .size(20.dp), + imageVector = Icons.Rounded.Palette, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 16.dp) + .height(48.dp) + ) { + if (icons == null) { + for (i in 0 until columns) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + ShapedLauncherIcon(size = 48.dp, icon = { null }) + } + } + } else { + for (icon in icons!!) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + ShapedLauncherIcon(size = 48.dp, icon = { icon }) + } + } + for (i in 0..<(columns - icons!!.size)) { + Box( + modifier = Modifier.weight(1f), + ) + } + } + } + } + } + } + } + } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt index e367df1e..c16b37fe 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt @@ -1,10 +1,14 @@ package de.mm20.launcher2.ui.settings.icons +import android.content.ComponentName +import android.content.Context import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import de.mm20.launcher2.icons.IconPack +import de.mm20.launcher2.icons.IconPackManager import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.permissions.PermissionGroup @@ -13,11 +17,15 @@ import de.mm20.launcher2.preferences.IconShape import de.mm20.launcher2.preferences.ui.BadgeSettings import de.mm20.launcher2.preferences.ui.IconSettings import de.mm20.launcher2.preferences.ui.UiSettings +import de.mm20.launcher2.search.Application import de.mm20.launcher2.services.favorites.FavoritesService import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -28,6 +36,7 @@ class IconsSettingsScreenVM( private val iconService: IconService, private val favoritesService: FavoritesService, private val permissionsManager: PermissionsManager, + private val iconPackManager: IconPackManager, ) : ViewModel() { val grid = uiSettings.gridSettings @@ -68,14 +77,11 @@ class IconsSettingsScreenVM( name = "System", packageName = "", version = "", + themed = true, ) ) + it } - fun setIconPackThemed(iconPackThemed: Boolean) { - iconSettings.setIconPackThemed(iconPackThemed) - } - fun setIconPack(iconPack: String?) { iconSettings.setIconPack(iconPack?.takeIf { it.isNotBlank() }) } @@ -111,24 +117,77 @@ class IconsSettingsScreenVM( badgeSettings.setPlugins(plugins) } - fun getPreviewIcons(size: Int): Flow> { - return grid.flatMapLatest { grid -> - favoritesService.getFavorites( - includeTypes = listOf("app"), - limit = grid.columnCount, - manuallySorted = true, - automaticallySorted = true, - frequentlyUsed = true, - ) - }.flatMapLatest { apps -> + private val previewItems = grid.flatMapLatest { grid -> + favoritesService.getFavorites( + includeTypes = listOf("app"), + limit = grid.columnCount, + manuallySorted = true, + automaticallySorted = true, + frequentlyUsed = true, + ) + }.shareIn(viewModelScope, started = SharingStarted.WhileSubscribed(), 1) + + fun getPreviewIcons(size: Int): Flow> { + return previewItems.flatMapLatest { apps -> combine(apps.map { - iconService.getIcon(it, size) + iconService.getIcon(it, size).filterNotNull() }) { it.toList() } } } + fun getIconPackPreviewIcons( + context: Context, + iconPack: IconPack, + count: Int, + size: Int, + themed: Boolean + ): Flow> { + return previewItems.map { items -> + val apps = items.filterIsInstance() + val icons = mutableListOf() + + val usedApps = mutableSetOf() + + for (app in apps) { + val icon = if (iconPack.packageName == "") { + app.loadIcon(context, size, themed) + } else { + iconPackManager.getIcon( + packageName = app.componentName.packageName, + activityName = app.componentName.className, + iconPack = iconPack.packageName, + allowThemed = themed, + ) + } + if (icon != null) { + icons += icon + usedApps += app.componentName + } + } + + for (fallback in fallbackIconPackIcons) { + if (icons.size >= count) break + + if (fallback in usedApps) continue + + val icon = iconPackManager.getIcon( + packageName = fallback.packageName, + activityName = fallback.className, + iconPack = iconPack.packageName, + allowThemed = themed, + ) + if (icon != null) { + icons += icon + usedApps += fallback + } + } + + return@map icons + } + } + companion object : KoinComponent { val Factory = viewModelFactory { initializer { @@ -139,8 +198,24 @@ class IconsSettingsScreenVM( favoritesService = get(), badgeSettings = get(), iconSettings = get(), + iconPackManager = get(), ) } } + + // Some very common activities that are likely included in most icon packs + private val fallbackIconPackIcons = mutableListOf( + ComponentName("com.android.vending", "com.android.vending.AssetBrowserActivity"), + ComponentName("com.google.android.camera", "com.android.camera.Camera"), + ComponentName("com.android.settings", "com.android.settings.Settings"), + ComponentName("com.google.android.deskclock", "com.android.deskclock.DeskClock"), + ComponentName("com.android.chrome", "com.android.chrome.Main"), + ComponentName("com.google.android.calendar", "com.android.calendar.AllInOneActivity"), + ComponentName("com.android.dialer", "com.android.dialer.main.impl.MainActivity"), + ComponentName( + "com.google.android.googlequicksearchbox", + "com.google.android.googlequicksearchbox.SearchActivity" + ), + ) } } \ No newline at end of file