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.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.FormatPaint 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.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -30,32 +35,33 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel 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.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.preferences.IconShape import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.ui.GridSettings import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.ui.R 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.MissingPermissionBanner
import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.getShape 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.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SliderPreference import de.mm20.launcher2.ui.component.preferences.SliderPreference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.component.preferences.label import kotlinx.coroutines.flow.Flow
import de.mm20.launcher2.ui.component.preferences.value
@Composable @Composable
fun IconsSettingsScreen() { fun IconsSettingsScreen() {
@ -69,6 +75,8 @@ fun IconsSettingsScreen() {
val installedIconPacks by viewModel.installedIconPacks.collectAsState(emptyList()) val installedIconPacks by viewModel.installedIconPacks.collectAsState(emptyList())
var showIconPackSheet by remember { mutableStateOf(false) }
val hasNotificationsPermission by viewModel.hasNotificationsPermission.collectAsStateWithLifecycle( val hasNotificationsPermission by viewModel.hasNotificationsPermission.collectAsStateWithLifecycle(
null null
) )
@ -79,8 +87,10 @@ fun IconsSettingsScreen() {
val shortcutBadges by viewModel.shortcutBadges.collectAsStateWithLifecycle(null) val shortcutBadges by viewModel.shortcutBadges.collectAsStateWithLifecycle(null)
val pluginBadges by viewModel.pluginBadges.collectAsStateWithLifecycle(null) val pluginBadges by viewModel.pluginBadges.collectAsStateWithLifecycle(null)
val previewIcons by remember(grid?.iconSize) { val iconSize = with(density) { grid.iconSize.dp.toPx() }.toInt()
viewModel.getPreviewIcons(with(density) { grid.iconSize.dp.toPx() }.toInt())
val previewIcons = remember(iconSize) {
viewModel.getPreviewIcons(iconSize)
}.collectAsState( }.collectAsState(
emptyList() emptyList()
) )
@ -119,7 +129,7 @@ fun IconsSettingsScreen() {
} }
item { item {
PreferenceCategory(stringResource(R.string.preference_category_icons)) { PreferenceCategory(stringResource(R.string.preference_category_icons)) {
if (previewIcons.isNotEmpty()) { if (previewIcons.value.isNotEmpty()) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -131,7 +141,7 @@ fun IconsSettingsScreen() {
Row( Row(
modifier = Modifier.padding(vertical = 24.dp, horizontal = 8.dp) modifier = Modifier.padding(vertical = 24.dp, horizontal = 8.dp)
) { ) {
for (icon in previewIcons) { for (icon in previewIcons.value) {
Box( Box(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@ -181,55 +191,17 @@ fun IconsSettingsScreen() {
val items = installedIconPacks.map { val items = installedIconPacks.map {
it.name to it it.name to it
} }
ListPreference( Preference(
title = stringResource(R.string.preference_icon_pack), title = stringResource(R.string.preference_icon_pack),
items = items,
summary = if (items.size <= 1) { summary = if (items.size <= 1) {
stringResource(R.string.preference_icon_pack_summary_empty) stringResource(R.string.preference_icon_pack_summary_empty)
} else { } else {
iconPack?.name ?: "System" iconPack?.name ?: "System"
}, },
enabled = installedIconPacks.size > 1, enabled = installedIconPacks.size > 1,
value = iconPack, onClick = {
onValueChanged = { showIconPackSheet = true
if (it != null) viewModel.setIconPack(it.packageName)
}, },
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 package de.mm20.launcher2.ui.settings.icons
import android.content.ComponentName
import android.content.Context
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import de.mm20.launcher2.icons.IconPack import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.permissions.PermissionGroup 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.BadgeSettings
import de.mm20.launcher2.preferences.ui.IconSettings import de.mm20.launcher2.preferences.ui.IconSettings
import de.mm20.launcher2.preferences.ui.UiSettings import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@ -28,6 +36,7 @@ class IconsSettingsScreenVM(
private val iconService: IconService, private val iconService: IconService,
private val favoritesService: FavoritesService, private val favoritesService: FavoritesService,
private val permissionsManager: PermissionsManager, private val permissionsManager: PermissionsManager,
private val iconPackManager: IconPackManager,
) : ViewModel() { ) : ViewModel() {
val grid = uiSettings.gridSettings val grid = uiSettings.gridSettings
@ -68,14 +77,11 @@ class IconsSettingsScreenVM(
name = "System", name = "System",
packageName = "", packageName = "",
version = "", version = "",
themed = true,
) )
) + it ) + it
} }
fun setIconPackThemed(iconPackThemed: Boolean) {
iconSettings.setIconPackThemed(iconPackThemed)
}
fun setIconPack(iconPack: String?) { fun setIconPack(iconPack: String?) {
iconSettings.setIconPack(iconPack?.takeIf { it.isNotBlank() }) iconSettings.setIconPack(iconPack?.takeIf { it.isNotBlank() })
} }
@ -111,24 +117,77 @@ class IconsSettingsScreenVM(
badgeSettings.setPlugins(plugins) badgeSettings.setPlugins(plugins)
} }
fun getPreviewIcons(size: Int): Flow<List<LauncherIcon?>> { private val previewItems = grid.flatMapLatest { grid ->
return grid.flatMapLatest { grid -> favoritesService.getFavorites(
favoritesService.getFavorites( includeTypes = listOf("app"),
includeTypes = listOf("app"), limit = grid.columnCount,
limit = grid.columnCount, manuallySorted = true,
manuallySorted = true, automaticallySorted = true,
automaticallySorted = true, frequentlyUsed = true,
frequentlyUsed = true, )
) }.shareIn(viewModelScope, started = SharingStarted.WhileSubscribed(), 1)
}.flatMapLatest { apps ->
fun getPreviewIcons(size: Int): Flow<List<LauncherIcon>> {
return previewItems.flatMapLatest { apps ->
combine(apps.map { combine(apps.map {
iconService.getIcon(it, size) iconService.getIcon(it, size).filterNotNull()
}) { }) {
it.toList() 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 { companion object : KoinComponent {
val Factory = viewModelFactory { val Factory = viewModelFactory {
initializer { initializer {
@ -139,8 +198,24 @@ class IconsSettingsScreenVM(
favoritesService = get(), favoritesService = get(),
badgeSettings = get(), badgeSettings = get(),
iconSettings = 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"
),
)
} }
} }