Redesign icon pack selector

This commit is contained in:
MM20 2024-04-20 16:50:45 +02:00
parent b323953a75
commit 3c489fc648
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
2 changed files with 231 additions and 65 deletions

View File

@ -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<IconPack>,
iconPackPreviewIcons: Map<String, Flow<List<LauncherIcon>>>,
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),
)
}
}
}
}
}
}
}
}
}

View File

@ -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<List<LauncherIcon?>> {
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<List<LauncherIcon>> {
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<List<LauncherIcon>> {
return previewItems.map { items ->
val apps = items.filterIsInstance<Application>()
val icons = mutableListOf<LauncherIcon>()
val usedApps = mutableSetOf<ComponentName>()
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"
),
)
}
}