Redesign icon pack selector
This commit is contained in:
parent
b323953a75
commit
3c489fc648
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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"
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user