(feat) shape schemes

This commit is contained in:
MM20 2025-06-01 17:13:12 +02:00
parent 17c4a51ca5
commit cfb7bb0c72
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
41 changed files with 2129 additions and 921 deletions

View File

@ -67,6 +67,7 @@
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />

View File

@ -40,7 +40,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.Colors
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage
@ -63,7 +63,7 @@ fun ImportThemeSheet(
viewModel.readTheme(context, uri)
}
val theme by viewModel.theme
val theme by viewModel.colors
val error by viewModel.error
var apply by viewModel.apply
@ -132,13 +132,13 @@ fun ImportThemeSheet(
@Composable
fun ThemePreview(
theme: Theme,
colors: Colors,
modifier: Modifier = Modifier,
) {
val darkMode = LocalDarkTheme.current
var darkTheme by remember { mutableStateOf(darkMode) }
val colorScheme = if (darkTheme) darkColorSchemeOf(theme) else lightColorSchemeOf(theme)
val colorScheme = if (darkTheme) darkColorSchemeOf(colors) else lightColorSchemeOf(colors)
Column(modifier = modifier) {
Row(
@ -146,7 +146,7 @@ fun ThemePreview(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = theme.name,
text = colors.name,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurfaceVariant

View File

@ -5,11 +5,11 @@ import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.ThemeDescriptor
import de.mm20.launcher2.preferences.ColorsDescriptor
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.Colors
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.themes.fromJson
import de.mm20.launcher2.themes.fromLegacyJson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -20,12 +20,12 @@ class ImportThemeSheetVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
private val uiSettings: UiSettings by inject()
val theme = mutableStateOf<Theme?>(null)
val colors = mutableStateOf<Colors?>(null)
val error = mutableStateOf<Boolean>(false)
val apply = mutableStateOf<Boolean>(false)
fun import() {
val theme = theme.value
val theme = colors.value
val apply = apply.value
if (theme != null) {
viewModelScope.launch {
@ -37,30 +37,30 @@ class ImportThemeSheetVM : ViewModel(), KoinComponent {
fun readTheme(context: Context, uri: Uri) {
error.value = false
theme.value = null
colors.value = null
apply.value = true
viewModelScope.launch(Dispatchers.IO) {
val inputStream =
context.contentResolver.openInputStream(uri) ?: return@launch
val theme = inputStream.use {
val colors = inputStream.use {
val json = it.readBytes().toString(Charsets.UTF_8)
try {
Theme.fromJson(json)
Colors.fromLegacyJson(json)
} catch (e: IllegalArgumentException) {
null
}
}
this@ImportThemeSheetVM.theme.value = theme
if (theme == null) {
this@ImportThemeSheetVM.colors.value = colors
if (colors == null) {
error.value = true
}
}
}
private fun importTheme(theme: Theme, apply: Boolean) {
themeRepository.createTheme(theme)
private fun importTheme(colors: Colors, apply: Boolean) {
themeRepository.createColors(colors)
if (apply) {
uiSettings.setTheme(ThemeDescriptor.Custom(theme.id.toString()))
uiSettings.setColors(ColorsDescriptor.Custom(colors.id.toString()))
}
}
}

View File

@ -28,7 +28,7 @@ fun Modifier.verticalFadingEdges(
if(!enabled) return this
if (top == 0.dp && bottom == 0.dp) return this
return this then drawWithContent {
return drawWithContent {
val topColors = if (top > 0.dp) createColors(
1f - amount,

View File

@ -27,7 +27,7 @@ fun Modifier.verticalScrims(
if (!enabled) return this
if (top == 0.dp && bottom == 0.dp) return this
return this then drawWithCache {
return drawWithCache {
onDrawWithContent {
val topColors = if (top > 0.dp) createColors(
1f - amount,

View File

@ -42,8 +42,8 @@ import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarProviderSettingsScreen
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemesSettingsScreen
import de.mm20.launcher2.ui.settings.contacts.ContactsSettingsScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
@ -69,6 +69,8 @@ import de.mm20.launcher2.ui.settings.plugins.PluginSettingsScreen
import de.mm20.launcher2.ui.settings.plugins.PluginsSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
import de.mm20.launcher2.ui.settings.shapes.ShapeSchemeSettingsScreen
import de.mm20.launcher2.ui.settings.shapes.ShapeSchemesSettingsScreen
import de.mm20.launcher2.ui.settings.tags.TagsSettingsScreen
import de.mm20.launcher2.ui.settings.tasks.TasksIntegrationSettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterHelpSettingsScreen
@ -160,11 +162,11 @@ class SettingsActivity : BaseActivity() {
composable("settings/icons") {
IconsSettingsScreen()
}
composable("settings/appearance/themes") {
ThemesSettingsScreen()
composable("settings/appearance/colors") {
ColorSchemesSettingsScreen()
}
composable(
"settings/appearance/themes/{id}",
"settings/appearance/colors/{id}",
arguments = listOf(navArgument("id") {
nullable = false
})
@ -172,7 +174,21 @@ class SettingsActivity : BaseActivity() {
val id = it.arguments?.getString("id")?.let {
UUID.fromString(it)
} ?: return@composable
ThemeSettingsScreen(id)
ColorSchemeSettingsScreen(id)
}
composable("settings/appearance/shapes") {
ShapeSchemesSettingsScreen()
}
composable(
"settings/appearance/shapes/{id}",
arguments = listOf(navArgument("id") {
nullable = false
})
) {
val id = it.arguments?.getString("id")?.let {
UUID.fromString(it)
} ?: return@composable
ShapeSchemeSettingsScreen(id)
}
composable("settings/appearance/cards") {
CardsSettingsScreen()

View File

@ -1,5 +1,10 @@
package de.mm20.launcher2.ui.settings.appearance
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CropSquare
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.TextFields
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -45,12 +50,17 @@ fun AppearanceSettingsScreen() {
viewModel.setColorScheme(newValue)
}
)
}
}
item {
PreferenceCategory {
Preference(
title = stringResource(id = R.string.preference_screen_colors),
summary = themeName,
onClick = {
navController?.navigate("settings/appearance/themes")
}
navController?.navigate("settings/appearance/colors")
},
icon = Icons.Rounded.Palette,
)
val font by viewModel.font.collectAsState()
ListPreference(
@ -68,7 +78,16 @@ fun AppearanceSettingsScreen() {
getTypography(context, it.value)
}
Text(it.first, style = typography.titleMedium)
}
},
icon = Icons.Rounded.TextFields,
)
Preference(
title = stringResource(id = R.string.preference_screen_shapes),
summary = themeName,
onClick = {
navController?.navigate("settings/appearance/shapes")
},
icon = Icons.Rounded.CropSquare,
)
Preference(

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.settings.appearance
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.preferences.ColorScheme
import de.mm20.launcher2.preferences.Font
import de.mm20.launcher2.preferences.ui.UiSettings
@ -26,8 +25,8 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
uiSettings.setColorScheme(colorScheme)
}
val themeName = uiSettings.theme.flatMapLatest {
themeRepository.getThemeOrDefault(it)
val themeName = uiSettings.colors.flatMapLatest {
themeRepository.getColorsOrDefault(it)
}.map {
it.name
}

View File

@ -47,28 +47,6 @@ fun CardsSettingsScreen() {
}
item {
PreferenceCategory {
ListPreference(
icon = Icons.Rounded.Rectangle,
title = stringResource(R.string.preference_cards_shape),
items = listOf(
stringResource(R.string.preference_cards_shape_rounded) to SurfaceShape.Rounded,
stringResource(R.string.preference_cards_shape_cut) to SurfaceShape.Cut,
),
value = cardStyle.shape,
onValueChanged = {
viewModel.setShape(it)
})
SliderPreference(
title = stringResource(R.string.preference_cards_corner_radius),
icon = Icons.Rounded.RoundedCorner,
value = cardStyle.cornerRadius,
min = 0,
max = 24,
step = 1,
onValueChanged = {
viewModel.setRadius(it)
}
)
SliderPreference(
title = stringResource(R.string.preference_cards_opacity),
icon = Icons.Rounded.Opacity,

View File

@ -1,7 +1,6 @@
package de.mm20.launcher2.ui.settings.cards
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.preferences.SurfaceShape
import de.mm20.launcher2.preferences.ui.UiSettings
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -15,15 +14,8 @@ class CardsSettingsScreenVM : ViewModel(), KoinComponent {
uiSettings.setCardOpacity(opacity)
}
fun setRadius(radius: Int) {
uiSettings.setCardRadius(radius)
}
fun setBorderWidth(borderWidth: Int) {
uiSettings.setCardBorderWidth(borderWidth)
}
fun setShape(shape: SurfaceShape) {
uiSettings.setCardShape(shape)
}
}

View File

@ -1,16 +1,11 @@
package de.mm20.launcher2.ui.settings.colorscheme
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.FlowRowScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DarkMode
@ -31,7 +26,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ThemePreferenceCategory(
fun ColorSchemePreferenceCategory(
title: String,
previewColorScheme: ColorScheme,
darkMode: Boolean,
@ -95,15 +90,8 @@ fun ThemePreferenceCategory(
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(16.dp)
) {
colorPreferences()
}
HorizontalDivider()
colorPreferences()
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
}
}

View File

@ -1,20 +1,16 @@
package de.mm20.launcher2.ui.settings.colorscheme
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.OpenInNew
@ -60,6 +56,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
@ -78,8 +75,8 @@ import palettes.CorePalette
import java.util.UUID
@Composable
fun ThemeSettingsScreen(themeId: UUID) {
val viewModel: ThemesSettingsScreenVM = viewModel()
fun ColorSchemeSettingsScreen(themeId: UUID) {
val viewModel: ColorSchemesSettingsScreenVM = viewModel()
val context = LocalContext.current
val dark = LocalDarkTheme.current
@ -107,7 +104,13 @@ fun ThemeSettingsScreen(themeId: UUID) {
var name by remember(theme) { mutableStateOf(theme?.name ?: "") }
AlertDialog(
onDismissRequest = { editName = false },
text = { OutlinedTextField(value = name, onValueChange = { name = it }, singleLine = true) },
text = {
OutlinedTextField(
value = name,
onValueChange = { name = it },
singleLine = true
)
},
confirmButton = {
Button(
onClick = {
@ -146,135 +149,129 @@ fun ThemeSettingsScreen(themeId: UUID) {
stringResource(R.string.preference_custom_colors_corepalette),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(16.dp)
) {
CorePaletteColorPreference(
title = "Primary",
value = theme?.corePalette?.primary,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
primary = it
)
CorePaletteColorPreference(
title = "Primary",
value = theme?.corePalette?.primary,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
primary = it
)
)
},
defaultValue = systemPalette.primary,
modifier = Modifier.padding(end = 12.dp),
)
CorePaletteColorPreference(
title = "Secondary",
value = theme?.corePalette?.secondary,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
secondary = it
)
)
},
defaultValue = systemPalette.primary,
modifier = Modifier.padding(end = 12.dp),
)
CorePaletteColorPreference(
title = "Secondary",
value = theme?.corePalette?.secondary,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
secondary = it
)
)
},
defaultValue = systemPalette.secondary,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).a2.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Tertiary",
value = theme?.corePalette?.tertiary,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
tertiary = it
)
)
},
defaultValue = systemPalette.secondary,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).a2.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Tertiary",
value = theme?.corePalette?.tertiary,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
tertiary = it
)
)
},
defaultValue = systemPalette.tertiary,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).a3.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Neutral",
value = theme?.corePalette?.neutral,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
neutral = it
)
)
},
defaultValue = systemPalette.tertiary,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).a3.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Neutral",
value = theme?.corePalette?.neutral,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
neutral = it
)
)
},
defaultValue = systemPalette.neutral,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).n1.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Neutral Variant",
value = theme?.corePalette?.neutralVariant,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
neutralVariant = it
)
)
},
defaultValue = systemPalette.neutral,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).n1.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Neutral Variant",
value = theme?.corePalette?.neutralVariant,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
neutralVariant = it
)
)
},
defaultValue = systemPalette.neutralVariant,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).n2.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Error",
value = theme?.corePalette?.error,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
error = it
)
)
},
defaultValue = systemPalette.neutralVariant,
modifier = Modifier.padding(end = 12.dp),
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).n2.keyColor.toInt()
}
},
)
CorePaletteColorPreference(
title = "Error",
value = theme?.corePalette?.error,
onValueChange = {
viewModel.updateTheme(
theme!!.copy(
corePalette = theme!!.corePalette.copy(
error = it
)
)
},
defaultValue = systemPalette.error,
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).error.keyColor.toInt()
}
},
)
}
HorizontalDivider()
)
},
defaultValue = systemPalette.error,
autoGenerate = {
theme!!.corePalette.primary?.let {
CorePalette.of(it).error.keyColor.toInt()
}
},
)
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Primary colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -302,7 +299,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.primary,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Primary",
@ -326,7 +322,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onPrimary,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Primary Container",
@ -350,7 +345,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.primaryContainer,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Primary Container",
@ -374,7 +368,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onPrimaryContainer,
modifier = Modifier.padding(end = 12.dp),
)
},
@ -419,7 +412,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Secondary colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -447,7 +440,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.secondary,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Secondary",
@ -471,7 +463,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onSecondary,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Secondary Container",
@ -495,7 +486,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.secondaryContainer,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Secondary Container",
@ -519,7 +509,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onSecondaryContainer,
modifier = Modifier.padding(end = 12.dp),
)
},
) {
@ -557,7 +546,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Tertiary colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -585,7 +574,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.tertiary,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Tertiary",
@ -609,7 +597,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onTertiary,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Tertiary Container",
@ -633,7 +620,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.tertiaryContainer,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Tertiary Container",
@ -657,14 +643,17 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onTertiaryContainer,
modifier = Modifier.padding(end = 12.dp),
)
},
) {
ShapedLauncherIcon(
badge = { Badge() },
size = 48.dp,
)
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Surface colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -716,7 +705,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surface,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Surface Bright",
@ -740,7 +728,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceBright,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Surface Tint",
@ -764,7 +751,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceTint,
modifier = Modifier.padding(end = 12.dp),
)
},
) {
@ -824,7 +810,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Surface container colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -852,7 +838,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceContainerLowest,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Surface Container Low",
@ -876,7 +861,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceContainerLow,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Surface Container",
@ -900,7 +884,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceContainer,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Surface Container High",
@ -924,7 +907,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceContainerHigh,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Surface Container Highest",
@ -948,7 +930,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceContainerHighest,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Surface Variant",
@ -972,7 +953,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.surfaceVariant,
modifier = Modifier.padding(end = 12.dp),
)
},
) {
@ -994,7 +974,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Content colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -1022,7 +1002,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onSurface,
modifier = Modifier.padding(end = 12.dp),
)
@ -1048,7 +1027,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
@ -1073,7 +1051,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 12.dp),
)
},
) {
@ -1126,7 +1103,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
Surface(
color = MaterialTheme.colorScheme.surface,
color = MaterialTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.extraSmall,
tonalElevation = 3.dp,
shadowElevation = 3.dp,
@ -1148,7 +1125,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Outline colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -1176,7 +1153,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.outline,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Outline Variant",
@ -1200,7 +1176,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.outlineVariant,
modifier = Modifier.padding(end = 12.dp),
)
},
) {
@ -1240,7 +1215,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Error colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -1268,7 +1243,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.error,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Error",
@ -1292,7 +1266,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onError,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Error Container",
@ -1316,7 +1289,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.errorContainer,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "On Error Container",
@ -1340,7 +1312,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.onErrorContainer,
modifier = Modifier.padding(end = 12.dp),
)
},
) {
@ -1354,7 +1325,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
}
}
item {
ThemePreferenceCategory(
ColorSchemePreferenceCategory(
title = "Inverse colors",
previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme,
@ -1382,7 +1353,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.inverseSurface,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Inverse Surface",
@ -1406,7 +1376,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.inverseOnSurface,
modifier = Modifier.padding(end = 12.dp),
)
ThemeColorPreference(
title = "Inverse Primary",
@ -1430,7 +1399,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
)
},
defaultValue = selectedDefaultScheme.inversePrimary,
modifier = Modifier.padding(end = 12.dp),
)
},
) {

View File

@ -45,7 +45,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.Colors
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.ImportThemeSheet
import de.mm20.launcher2.ui.component.preferences.Preference
@ -57,15 +57,15 @@ import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf
import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
@Composable
fun ThemesSettingsScreen() {
val viewModel: ThemesSettingsScreenVM = viewModel()
fun ColorSchemesSettingsScreen() {
val viewModel: ColorSchemesSettingsScreenVM = viewModel()
val navController = LocalNavController.current
val context = LocalContext.current
val selectedTheme by viewModel.selectedTheme.collectAsStateWithLifecycle(null)
val themes by viewModel.themes.collectAsStateWithLifecycle(emptyList())
val selectedTheme by viewModel.selectedColors.collectAsStateWithLifecycle(null)
val themes by viewModel.colors.collectAsStateWithLifecycle(emptyList())
var deleteTheme by remember { mutableStateOf<Theme?>(null) }
var deleteColors by remember { mutableStateOf<Colors?>(null) }
var importThemeUri by remember { mutableStateOf<Uri?>(null) }
@ -114,7 +114,7 @@ fun ThemesSettingsScreen() {
},
text = { Text(stringResource(R.string.edit)) },
onClick = {
navController?.navigate("settings/appearance/themes/${theme.id}")
navController?.navigate("settings/appearance/colors/${theme.id}")
showMenu = false
}
)
@ -146,7 +146,7 @@ fun ThemesSettingsScreen() {
},
text = { Text(stringResource(R.string.menu_delete)) },
onClick = {
deleteTheme = theme
deleteColors = theme
showMenu = false
}
)
@ -162,22 +162,22 @@ fun ThemesSettingsScreen() {
}
}
}
if (deleteTheme != null) {
if (deleteColors != null) {
AlertDialog(
onDismissRequest = { deleteTheme = null },
onDismissRequest = { deleteColors = null },
text = {
Text(
stringResource(
R.string.confirmation_delete_color_scheme,
deleteTheme!!.name
deleteColors!!.name
)
)
},
confirmButton = {
TextButton(
onClick = {
viewModel.delete(deleteTheme!!)
deleteTheme = null
viewModel.delete(deleteColors!!)
deleteColors = null
}
) {
Text(stringResource(android.R.string.ok))
@ -185,7 +185,7 @@ fun ThemesSettingsScreen() {
},
dismissButton = {
TextButton(
onClick = { deleteTheme = null }
onClick = { deleteColors = null }
) {
Text(stringResource(android.R.string.cancel))
}
@ -199,9 +199,9 @@ fun ThemesSettingsScreen() {
}
@Composable
fun ColorSchemePreview(theme: Theme) {
fun ColorSchemePreview(colors: Colors) {
val dark = LocalDarkTheme.current
val scheme = if (dark) darkColorSchemeOf(theme) else lightColorSchemeOf(theme)
val scheme = if (dark) darkColorSchemeOf(colors) else lightColorSchemeOf(colors)
Box(
modifier = Modifier
.height(28.dp)

View File

@ -6,13 +6,13 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.ThemeDescriptor
import de.mm20.launcher2.preferences.ColorsDescriptor
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.BlackAndWhiteThemeId
import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.Colors
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.themes.toJson
import de.mm20.launcher2.themes.toLegacyJson
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -24,49 +24,49 @@ import org.koin.core.component.inject
import java.io.File
import java.util.UUID
class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
class ColorSchemesSettingsScreenVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
private val uiSettings: UiSettings by inject()
val selectedTheme = uiSettings.theme.map {
val selectedColors = uiSettings.colors.map {
when(it) {
ThemeDescriptor.Default -> DefaultThemeId
ThemeDescriptor.BlackAndWhite -> BlackAndWhiteThemeId
is ThemeDescriptor.Custom -> UUID.fromString(it.id)
ColorsDescriptor.Default -> DefaultThemeId
ColorsDescriptor.BlackAndWhite -> BlackAndWhiteThemeId
is ColorsDescriptor.Custom -> UUID.fromString(it.id)
}
}
val themes: Flow<List<Theme>> = themeRepository.getThemes()
val colors: Flow<List<Colors>> = themeRepository.getAllColors()
fun getTheme(id: UUID): Flow<Theme?> {
return themeRepository.getTheme(id)
fun getTheme(id: UUID): Flow<Colors?> {
return themeRepository.getColors(id)
}
fun updateTheme(theme: Theme) {
themeRepository.updateTheme(theme)
fun updateTheme(colors: Colors) {
themeRepository.updateColors(colors)
}
fun selectTheme(theme: Theme) {
uiSettings.setTheme(when(theme.id) {
DefaultThemeId -> ThemeDescriptor.Default
BlackAndWhiteThemeId -> ThemeDescriptor.BlackAndWhite
else -> ThemeDescriptor.Custom(theme.id.toString())
fun selectTheme(colors: Colors) {
uiSettings.setColors(when(colors.id) {
DefaultThemeId -> ColorsDescriptor.Default
BlackAndWhiteThemeId -> ColorsDescriptor.BlackAndWhite
else -> ColorsDescriptor.Custom(colors.id.toString())
})
}
fun duplicate(theme: Theme) {
themeRepository.createTheme(theme.copy(id = UUID.randomUUID()))
fun duplicate(colors: Colors) {
themeRepository.createColors(colors.copy(id = UUID.randomUUID()))
}
fun delete(theme: Theme) {
themeRepository.deleteTheme(theme)
fun delete(colors: Colors) {
themeRepository.deleteColors(colors)
}
fun exportTheme(context: Context, theme: Theme) {
fun exportTheme(context: Context, colors: Colors) {
viewModelScope.launch {
val file = withContext(Dispatchers.IO) {
val file = File(context.cacheDir, "${theme.name}.kvtheme")
file.writeText(theme.toJson())
val file = File(context.cacheDir, "${colors.name}.kvtheme")
file.writeText(colors.toLegacyJson())
file
}
context.tryStartActivity(Intent().apply {
@ -82,8 +82,8 @@ class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
}
fun createNew(context: Context) {
themeRepository.createTheme(
Theme(
themeRepository.createColors(
Colors(
id = UUID.randomUUID(),
name = context.getString(R.string.new_color_scheme_name)
)

View File

@ -3,8 +3,10 @@ package de.mm20.launcher2.ui.settings.colorscheme
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -16,6 +18,7 @@ import androidx.compose.material.icons.rounded.SettingsSuggest
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -28,7 +31,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.themes.get
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.Tooltip
@ -47,19 +54,25 @@ fun CorePaletteColorPreference(
) {
var showDialog by remember { mutableStateOf(false) }
Tooltip(
tooltipText = title
Row(
modifier = modifier.fillMaxWidth()
.clickable(
onClick = { showDialog = true },
)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ColorSwatch(
color = Color(value ?: defaultValue),
modifier = modifier
.size(48.dp)
.combinedClickable(
onClick = { showDialog = true },
onLongClick = {
onValueChange(null)
}
),
modifier = Modifier.padding(end = 20.dp).size(48.dp),
)
Text(
title,
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}

View File

@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Canvas
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
@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
@ -45,7 +47,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.themes.ColorRef
import de.mm20.launcher2.themes.CorePaletteColor
@ -66,7 +70,7 @@ import de.mm20.launcher2.themes.Color as ThemeColor
@Composable
fun ThemeColorPreference(
title: String,
value: de.mm20.launcher2.themes.Color?,
value: ThemeColor?,
corePalette: FullCorePalette,
onValueChange: (ThemeColor?) -> Unit,
defaultValue: ThemeColor,
@ -74,16 +78,25 @@ fun ThemeColorPreference(
) {
var showDialog by remember { mutableStateOf(false) }
Tooltip(
tooltipText = title
Row(
modifier = modifier.fillMaxWidth()
.clickable(
onClick = { showDialog = true },
)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ColorSwatch(
color = Color((value ?: defaultValue).get(corePalette)),
modifier = modifier
.size(48.dp)
.clickable(
onClick = { showDialog = true },
),
modifier = Modifier.padding(end = 20.dp).size(48.dp),
)
Text(
title,
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}

View File

@ -0,0 +1,733 @@
package de.mm20.launcher2.ui.settings.shapes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
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.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.OpenInNew
import androidx.compose.material.icons.rounded.RestartAlt
import androidx.compose.material.icons.rounded.RoundedCorner
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Shapes
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
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.min
import androidx.compose.ui.unit.times
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.CutCorner
import de.mm20.launcher2.icons.RoundedCornerAlt
import de.mm20.launcher2.themes.CornerStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.ktx.withCorners
import de.mm20.launcher2.ui.theme.shapes.shapesOf
import java.util.UUID
import kotlin.math.max
import kotlin.math.min
import de.mm20.launcher2.themes.Shape as ThemeShape
@Composable
fun ShapeSchemeSettingsScreen(themeId: UUID) {
val viewModel: ShapeSchemesSettingsScreenVM = viewModel()
val context = LocalContext.current
val theme by remember(
viewModel,
themeId
) { viewModel.getShapes(themeId) }.collectAsStateWithLifecycle(null)
val previewShapes = theme?.let { shapesOf(it) }
var editName by remember { mutableStateOf(false) }
if (editName) {
var name by remember(theme) { mutableStateOf(theme?.name ?: "") }
AlertDialog(
onDismissRequest = { editName = false },
text = {
OutlinedTextField(
value = name,
onValueChange = { name = it },
singleLine = true
)
},
confirmButton = {
Button(
onClick = {
viewModel.updateShapes(theme!!.copy(name = name))
editName = false
}
) {
Text(stringResource(R.string.save))
}
}
)
}
PreferenceScreen(
title = {
Text(
theme?.name ?: "",
modifier = Modifier.clickable {
editName = true
},
)
},
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/customization/color-schemes",
) {
if (theme == null || previewShapes == null) return@PreferenceScreen
val baseShape = theme!!.baseShape
item {
PreferenceCategory {
ShapePreference(
title = stringResource(R.string.preference_shapes_base),
shape = baseShape,
baseShape = baseShape,
factor = 1f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(
baseShape = it ?: ThemeShape(
corners = CornerStyle.Rounded,
radii = intArrayOf(12, 12, 12, 12)
)
)
)
},
titleTextStyle = MaterialTheme.typography.titleMedium
)
}
}
item {
PreferenceCategory {
ShapePreview(
previewShapes = previewShapes,
) {
Column(
modifier = Modifier
.width(200.dp)
) {
Box(
Modifier
.fillMaxWidth()
.height(32.dp)
.background(
MaterialTheme.colorScheme.surface,
MaterialTheme.shapes.extraSmall.withCorners(
topStart = false,
topEnd = false
)
)
)
Box(
Modifier
.fillMaxWidth()
.padding(top = 2.dp)
.height(32.dp)
.background(
MaterialTheme.colorScheme.surface,
MaterialTheme.shapes.extraSmall.withCorners(
bottomStart = false,
bottomEnd = false
)
)
)
}
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.extraSmall,
tonalElevation = 3.dp,
shadowElevation = 3.dp,
modifier = Modifier.wrapContentWidth()
) {
Column(
modifier = Modifier
.padding(vertical = 8.dp)
.width(IntrinsicSize.Max),
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.OpenInNew, null)
},
text = { Text("Menu") },
onClick = { })
}
}
}
ShapePreference(
title = "Extra small",
shape = theme!!.extraSmall,
baseShape = baseShape,
factor = 1f / 3f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(extraSmall = it)
)
}
)
}
PreferenceCategory {
ShapePreview(
previewShapes = previewShapes,
) {
FilterChip(
onClick = {},
label = {
Text("Chip")
},
selected = false,
)
}
ShapePreference(
title = "Small",
shape = theme!!.small,
baseShape = baseShape,
factor = 2f / 3f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(small = it)
)
}
)
}
PreferenceCategory {
ShapePreview(
previewShapes = previewShapes,
) {
Box(
Modifier
.fillMaxWidth()
.width(200.dp)
.height(48.dp)
.background(
MaterialTheme.colorScheme.surface,
MaterialTheme.shapes.medium
)
) {
Icon(
Icons.Rounded.Search,
null,
modifier = Modifier.padding(12.dp)
)
}
}
ShapePreference(
title = "Medium",
shape = theme!!.medium,
baseShape = baseShape,
factor = 1f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(medium = it)
)
}
)
}
PreferenceCategory {
ShapePreview(
previewShapes = previewShapes,
) {
FloatingActionButton(onClick = {}) {
Icon(Icons.Rounded.Edit, null)
}
}
ShapePreference(
title = "Large",
shape = theme!!.large,
baseShape = baseShape,
factor = 4f / 3f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(large = it)
)
}
)
}
PreferenceCategory {
ShapePreference(
title = "Large increased",
shape = theme!!.largeIncreased,
baseShape = baseShape,
factor = 5f / 3f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(largeIncreased = it)
)
}
)
}
PreferenceCategory {
ShapePreview(
previewShapes = previewShapes,
) {
Surface(
shape = BottomSheetDefaults.ExpandedShape,
color = BottomSheetDefaults.ContainerColor,
shadowElevation = BottomSheetDefaults.Elevation,
tonalElevation = BottomSheetDefaults.Elevation,
modifier = Modifier
.width(250.dp)
.height(144.dp),
) {
Box(
contentAlignment = Alignment.TopCenter,
) {
BottomSheetDefaults.DragHandle()
}
}
}
ShapePreference(
title = "Extra large",
shape = theme!!.extraLarge,
baseShape = baseShape,
factor = 7f / 3f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(extraLarge = it)
)
}
)
}
PreferenceCategory {
ShapePreference(
title = "Extra large increased",
shape = theme!!.extraLargeIncreased,
baseShape = baseShape,
factor = 8f / 3f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(extraLargeIncreased = it)
)
}
)
}
PreferenceCategory {
ShapePreference(
title = "Extra extra large",
shape = theme!!.extraExtraLarge,
baseShape = baseShape,
factor = 12f / 3f,
onValueChange = {
viewModel.updateShapes(
theme!!.copy(extraExtraLarge = it)
)
}
)
}
}
}
}
@Composable
fun ShapePreference(
title: String,
shape: ThemeShape?,
baseShape: ThemeShape,
factor: Float = 1f,
onValueChange: (ThemeShape?) -> Unit,
titleTextStyle: TextStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace)
) {
var showDialog by remember { mutableStateOf(false) }
val f = min(1f, factor)
val topStart =
(shape?.radii?.get(0)?.div(factor) ?: baseShape.radii?.get(0)?.toFloat() ?: 12f) * f
val topEnd =
(shape?.radii?.get(1)?.div(factor) ?: baseShape.radii?.get(1)?.toFloat() ?: 12f) * f
val bottomEnd =
(shape?.radii?.get(2)?.div(factor) ?: baseShape.radii?.get(2)?.toFloat() ?: 12f) * f
val bottomStart =
(shape?.radii?.get(3)?.div(factor) ?: baseShape.radii?.get(3)?.toFloat() ?: 12f) * f
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.clickable(
onClick = { showDialog = true },
)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.padding(end = 20.dp)
.size(48.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(min(48.dp, factor * 48.dp))
.border(
2.dp,
MaterialTheme.colorScheme.primary,
if ((shape?.corners ?: baseShape.corners
?: CornerStyle.Rounded) == CornerStyle.Cut
) {
CutCornerShape(
topStart = topStart.dp,
topEnd = topEnd.dp,
bottomEnd = bottomEnd.dp,
bottomStart = bottomStart.dp
)
} else {
RoundedCornerShape(
topStart = topStart.dp,
topEnd = topEnd.dp,
bottomEnd = bottomEnd.dp,
bottomStart = bottomStart.dp
)
}
)
)
}
Text(
title,
style = titleTextStyle,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
if (showDialog) {
val maxRadius = (24 * factor).toInt()
val baseTopStart = ((baseShape.radii?.get(0) ?: 12) * factor).toInt()
val baseTopEnd = ((baseShape.radii?.get(1) ?: 12) * factor).toInt()
val baseBottomEnd = ((baseShape.radii?.get(2) ?: 12) * factor).toInt()
val baseBottomStart = ((baseShape.radii?.get(3) ?: 12) * factor).toInt()
var currentCornerStyle by remember(shape) { mutableStateOf(shape?.corners) }
var currentTopStart by remember(shape) { mutableStateOf(shape?.radii?.get(0)) }
var currentTopEnd by remember(shape) { mutableStateOf(shape?.radii?.get(1)) }
var currentBottomEnd by remember(shape) { mutableStateOf(shape?.radii?.get(2)) }
var currentBottomStart by remember(shape) { mutableStateOf(shape?.radii?.get(3)) }
val actualCornerStyle = currentCornerStyle ?: baseShape.corners ?: CornerStyle.Rounded
val actualTopStart = currentTopStart ?: baseTopStart
val actualTopEnd = currentTopEnd ?: baseTopEnd
val actualBottomEnd = currentBottomEnd ?: baseBottomEnd
val actualBottomStart = currentBottomStart ?: baseBottomStart
BottomSheetDialog(
onDismissRequest = {
showDialog = false
onValueChange(
ThemeShape(
corners = currentCornerStyle,
radii = if (currentTopStart != null || currentTopEnd != null || currentBottomEnd != null || currentBottomStart != null) {
intArrayOf(
currentTopStart ?: baseTopStart,
currentTopEnd ?: baseTopEnd,
currentBottomEnd ?: baseBottomEnd,
currentBottomStart ?: baseBottomStart,
)
} else null
)
)
}) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(it),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
val previewShape = if ((currentCornerStyle ?: baseShape.corners
?: CornerStyle.Rounded) == CornerStyle.Rounded
) {
RoundedCornerShape(
topStart = (currentTopStart ?: baseTopStart).dp,
topEnd = (currentTopEnd ?: baseTopEnd).dp,
bottomEnd = (currentBottomEnd ?: baseBottomEnd).dp,
bottomStart = (currentBottomStart ?: baseBottomStart).dp
)
} else {
CutCornerShape(
topStart = (currentTopStart ?: baseTopStart).dp,
topEnd = (currentTopEnd ?: baseTopEnd).dp,
bottomEnd = (currentBottomEnd ?: baseBottomEnd).dp,
bottomStart = (currentBottomStart ?: baseBottomStart).dp
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(max(96, maxRadius * 2).dp)
.background(MaterialTheme.colorScheme.surfaceContainer, previewShape)
.border(
2.dp,
MaterialTheme.colorScheme.primary,
previewShape
)
)
SingleChoiceSegmentedButtonRow(
modifier = Modifier
.fillMaxWidth()
) {
SegmentedButton(
selected = actualCornerStyle == CornerStyle.Rounded,
onClick = {
currentCornerStyle = CornerStyle.Rounded
},
shape = SegmentedButtonDefaults.itemShape(0, 2),
icon = {
SegmentedButtonDefaults.Icon(
active = actualCornerStyle == CornerStyle.Rounded,
) {
Icon(Icons.Rounded.RoundedCornerAlt, null)
}
}
) {
Text(stringResource(R.string.preference_cards_shape_rounded))
}
SegmentedButton(
selected = actualCornerStyle == CornerStyle.Cut,
onClick = {
currentCornerStyle = CornerStyle.Cut
},
shape = SegmentedButtonDefaults.itemShape(1, 2),
icon = {
SegmentedButtonDefaults.Icon(
active = actualCornerStyle == CornerStyle.Cut,
) {
Icon(Icons.Rounded.CutCorner, null)
}
}
) {
Text(stringResource(R.string.preference_cards_shape_cut))
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
Icons.Rounded.RoundedCorner,
null,
modifier = Modifier
.padding(end = 8.dp)
.rotate(-90f)
)
Slider(
modifier = Modifier
.weight(1f),
value = actualTopStart.toFloat(),
onValueChange = {
currentTopStart = it.toInt()
},
valueRange = 0f..maxRadius.toFloat(),
steps = maxRadius + 1
)
Text(
text = actualTopStart.toString(),
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
Icons.Rounded.RoundedCorner,
null,
modifier = Modifier.padding(end = 8.dp)
)
Slider(
modifier = Modifier
.weight(1f),
value = actualTopEnd.toFloat(),
onValueChange = {
currentTopEnd = it.toInt()
},
valueRange = 0f..maxRadius.toFloat(),
steps = maxRadius + 1,
)
Text(
text = actualTopEnd.toString(),
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
Icons.Rounded.RoundedCorner,
null,
modifier = Modifier
.padding(end = 8.dp)
.rotate(90f)
)
Slider(
modifier = Modifier
.weight(1f),
value = actualBottomEnd.toFloat(),
onValueChange = {
currentBottomEnd = it.toInt()
},
valueRange = 0f..maxRadius.toFloat(),
steps = maxRadius + 1,
)
Text(
text = actualBottomEnd.toString(),
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
Icons.Rounded.RoundedCorner,
null,
modifier = Modifier
.padding(end = 8.dp)
.rotate(180f)
)
Slider(
modifier = Modifier
.weight(1f),
value = actualBottomStart.toFloat(),
onValueChange = {
currentBottomStart = it.toInt()
},
valueRange = 0f..maxRadius.toFloat(),
steps = maxRadius + 1,
)
Text(
text = actualBottomStart.toString(),
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center,
)
}
HorizontalDivider(
modifier = Modifier.padding(top = 16.dp)
)
TextButton(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.End),
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
onClick = {
currentTopStart = null
currentTopEnd = null
currentBottomEnd = null
currentBottomStart = null
currentCornerStyle = null
onValueChange(null)
}
) {
Icon(
Icons.Rounded.RestartAlt, null,
modifier = Modifier
.padding(ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.preference_restore_default))
}
}
}
}
}
@Composable
private fun ShapePreview(
previewShapes: Shapes,
content: @Composable () -> Unit,
) {
MaterialTheme(
shapes = previewShapes
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
content()
}
}
}
}

View File

@ -0,0 +1,204 @@
package de.mm20.launcher2.ui.settings.shapes
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RadioButtonChecked
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.themes.CornerStyle
import de.mm20.launcher2.themes.Shapes
import de.mm20.launcher2.ui.R
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.locals.LocalNavController
import de.mm20.launcher2.ui.theme.shapes.shapesOf
@Composable
fun ShapeSchemesSettingsScreen() {
val viewModel: ShapeSchemesSettingsScreenVM = viewModel()
val navController = LocalNavController.current
val context = LocalContext.current
val selectedTheme by viewModel.selectedShapes.collectAsStateWithLifecycle(null)
val themes by viewModel.shapes.collectAsStateWithLifecycle(emptyList())
var deleteShapes by remember { mutableStateOf<Shapes?>(null) }
PreferenceScreen(
title = stringResource(R.string.preference_screen_shapes),
floatingActionButton = {
FloatingActionButton(onClick = { viewModel.createNew(context) }) {
Icon(Icons.Rounded.Add, null)
}
}
) {
item {
PreferenceCategory {
for (theme in themes) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
title = theme.name,
controls = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
ShapesPreview(theme)
IconButton(
modifier = Modifier.padding(start = 12.dp),
onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (!theme.builtIn) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Edit, null)
},
text = { Text(stringResource(R.string.edit)) },
onClick = {
navController?.navigate("settings/appearance/shapes/${theme.id}")
showMenu = false
}
)
}
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
if (!theme.builtIn) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Delete, null)
},
text = { Text(stringResource(R.string.menu_delete)) },
onClick = {
deleteShapes = theme
showMenu = false
}
)
}
}
}
},
onClick = {
viewModel.selectShapes(theme)
}
)
}
}
}
}
if (deleteShapes != null) {
AlertDialog(
onDismissRequest = { deleteShapes = null },
text = {
Text(
stringResource(
R.string.confirmation_delete_shapes_scheme,
deleteShapes!!.name
)
)
},
confirmButton = {
TextButton(
onClick = {
viewModel.delete(deleteShapes!!)
deleteShapes = null
}
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = { deleteShapes = null }
) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
}
@Composable
private fun ShapesPreview(theme: Shapes) {
val shape = theme.medium
val baseShape = theme.baseShape
val topStart =
(shape?.radii?.get(0)?.toFloat() ?: baseShape.radii?.get(0)?.toFloat() ?: 8f) / 3f * 2f
val topEnd =
(shape?.radii?.get(1)?.toFloat() ?: baseShape.radii?.get(1)?.toFloat() ?: 8f) / 3f * 2f
val bottomEnd =
(shape?.radii?.get(2)?.toFloat() ?: baseShape.radii?.get(2)?.toFloat() ?: 8f) / 3f * 2f
val bottomStart =
(shape?.radii?.get(3)?.toFloat() ?: baseShape.radii?.get(3)?.toFloat() ?: 8f) / 3f * 2f
Box(
modifier = Modifier
.size(32.dp)
.border(
2.dp,
MaterialTheme.colorScheme.primary,
if ((theme.medium?.corners ?: theme.baseShape.corners
?: CornerStyle.Rounded) == CornerStyle.Cut
) {
CutCornerShape(
topStart = topStart.dp,
topEnd = topEnd.dp,
bottomEnd = bottomEnd.dp,
bottomStart = bottomStart.dp
)
} else {
RoundedCornerShape(
topStart = topStart.dp,
topEnd = topEnd.dp,
bottomEnd = bottomEnd.dp,
bottomStart = bottomStart.dp
)
}
)
)
}

View File

@ -0,0 +1,68 @@
package de.mm20.launcher2.ui.settings.shapes
import android.content.Context
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.preferences.ShapesDescriptor
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.CutShapesId
import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.ExtraRoundShapesId
import de.mm20.launcher2.themes.RectShapesId
import de.mm20.launcher2.themes.Shapes
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.UUID
import kotlin.getValue
class ShapeSchemesSettingsScreenVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
private val uiSettings: UiSettings by inject()
val selectedShapes = uiSettings.shapes.map {
when(it) {
ShapesDescriptor.Default -> DefaultThemeId
ShapesDescriptor.Cut -> CutShapesId
ShapesDescriptor.ExtraRound -> ExtraRoundShapesId
ShapesDescriptor.Rect -> RectShapesId
is ShapesDescriptor.Custom -> UUID.fromString(it.id)
}
}
val shapes: Flow<List<Shapes>> = themeRepository.getAllShapes()
fun getShapes(id: UUID): Flow<Shapes?> {
return themeRepository.getShapes(id)
}
fun updateShapes(shapes: Shapes) {
themeRepository.updateShapes(shapes)
}
fun selectShapes(shapes: Shapes) {
uiSettings.setShapes(when(shapes.id) {
DefaultThemeId -> ShapesDescriptor.Default
else -> ShapesDescriptor.Custom(shapes.id.toString())
})
}
fun duplicate(shapes: Shapes) {
themeRepository.createShapes(shapes.copy(id = UUID.randomUUID()))
}
fun delete(shapes: Shapes) {
themeRepository.deleteShapes(shapes)
}
fun createNew(context: Context) {
themeRepository.createShapes(
Shapes(
id = UUID.randomUUID(),
name = context.getString(R.string.new_shapes_name)
)
)
}
}

View File

@ -16,6 +16,7 @@ import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.theme.colorscheme.*
import de.mm20.launcher2.ui.theme.shapes.shapesOf
import de.mm20.launcher2.ui.theme.typography.DefaultTypography
import de.mm20.launcher2.ui.theme.typography.getDeviceDefaultTypography
import kotlinx.coroutines.flow.flatMapLatest
@ -33,11 +34,17 @@ fun LauncherTheme(
val uiSettings: UiSettings = koinInject()
val themeRepository: ThemeRepository = koinInject()
val theme by remember {
uiSettings.theme.flatMapLatest {
themeRepository.getThemeOrDefault(it)
val themeColors by remember {
uiSettings.colors.flatMapLatest {
themeRepository.getColorsOrDefault(it)
}
}.collectAsState(themeRepository.getDefaultTheme())
}.collectAsState(null)
val themeShapes by remember {
uiSettings.shapes.flatMapLatest {
themeRepository.getShapesOrDefault(it)
}
}.collectAsState(null)
val colorSchemePref by remember { uiSettings.colorScheme }.collectAsState(
ColorSchemePref.System
@ -45,27 +52,20 @@ fun LauncherTheme(
val darkTheme =
colorSchemePref == ColorSchemePref.Dark || colorSchemePref == ColorSchemePref.System && isSystemInDarkTheme()
val cornerRadius by remember {
uiSettings.cardStyle.map {
it.cornerRadius.dp
}
}.collectAsState(8.dp)
val baseShape by remember {
uiSettings.cardStyle.map {
when (it.shape) {
SurfaceShape.Cut -> CutCornerShape(0f)
else -> RoundedCornerShape(0f)
}
}
}.collectAsState(RoundedCornerShape(0f))
if (themeColors == null || themeShapes == null) {
return
}
val colorScheme = if (darkTheme) {
darkColorSchemeOf(theme)
darkColorSchemeOf(themeColors!!)
} else {
lightColorSchemeOf(theme)
lightColorSchemeOf(themeColors!!)
}
val shapes = shapesOf(themeShapes!!)
val font by remember { uiSettings.font }.collectAsState(
Font.Outfit
)
@ -80,13 +80,7 @@ fun LauncherTheme(
MaterialExpressiveTheme(
colorScheme = colorScheme,
typography = typography,
shapes = Shapes(
extraSmall = baseShape.copy(CornerSize(cornerRadius / 3f)),
small = baseShape.copy(CornerSize(cornerRadius / 3f * 2f)),
medium = baseShape.copy(CornerSize(cornerRadius)),
large = baseShape.copy(CornerSize((cornerRadius / 3f * 4f).coerceAtMost(16.dp))),
extraLarge = baseShape.copy(CornerSize((cornerRadius / 3f * 7f).coerceAtMost(28.dp))),
),
shapes = shapes,
content = content
)
}

View File

@ -1,83 +0,0 @@
package de.mm20.launcher2.ui.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
val LightBlackAndWhiteColorScheme = lightColorScheme(
primary = Color.Black,
onPrimary = Color.White,
primaryContainer = Color.White,
onPrimaryContainer = Color.Black,
inversePrimary = Color.White,
secondary = Color.Black,
onSecondary = Color.White,
secondaryContainer = Color.White,
onSecondaryContainer = Color.Black,
tertiary = Color.Black,
onTertiary = Color.White,
tertiaryContainer = Color.White,
onTertiaryContainer = Color.Black,
background = Color.White,
onBackground = Color.Black,
surface = Color.White,
onSurface = Color.Black,
surfaceVariant = Color.White,
onSurfaceVariant = Color.Black,
inverseSurface = Color.Black,
inverseOnSurface = Color.White,
error = Color(0xFFC10000),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFFFFDAD3),
onErrorContainer = Color(0xFF410000),
outline = Color.Black,
surfaceTint = Color.White,
outlineVariant = Color.Black,
scrim = Color.Black,
surfaceDim = Color.White,
surfaceBright = Color.White,
surfaceContainer = Color.White,
surfaceContainerHigh = Color.White,
surfaceContainerHighest = Color.White,
surfaceContainerLow = Color.White,
surfaceContainerLowest = Color.White,
)
val DarkBlackAndWhiteColorScheme = darkColorScheme(
primary = Color.White,
onPrimary = Color.Black,
primaryContainer = Color.Black,
onPrimaryContainer = Color.White,
inversePrimary = Color.Black,
secondary = Color.White,
onSecondary = Color.Black,
secondaryContainer = Color.Black,
onSecondaryContainer = Color.White,
tertiary = Color.White,
onTertiary = Color.Black,
tertiaryContainer = Color.Black,
onTertiaryContainer = Color.White,
background = Color.Black,
onBackground = Color.White,
surface = Color.Black,
onSurface = Color.White,
surfaceVariant = Color.Black,
onSurfaceVariant = Color.White,
inverseSurface = Color.White,
inverseOnSurface = Color.Black,
error = Color(0xffffb4a6),
onError = Color(0xff690000),
errorContainer = Color(0xff940000),
onErrorContainer = Color(0xffffb4a6),
outline = Color.White,
surfaceTint = Color.White,
outlineVariant = Color.White,
scrim = Color.White,
surfaceDim = Color.Black,
surfaceBright = Color.Black,
surfaceContainer = Color.Black,
surfaceContainerHigh = Color.Black,
surfaceContainerHighest = Color.Black,
surfaceContainerLow = Color.Black,
surfaceContainerLowest = Color.Black,
)

View File

@ -16,20 +16,20 @@ import de.mm20.launcher2.themes.DefaultDarkColorScheme
import de.mm20.launcher2.themes.DefaultLightColorScheme
import de.mm20.launcher2.themes.FullColorScheme
import de.mm20.launcher2.themes.PartialCorePalette
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.Colors as ThemeColors
import de.mm20.launcher2.themes.get
import de.mm20.launcher2.themes.merge
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
import org.koin.compose.koinInject
@Composable
fun lightColorSchemeOf(theme: Theme): ColorScheme {
return colorSchemeOf(theme.lightColorScheme.merge(DefaultLightColorScheme), theme.corePalette)
fun lightColorSchemeOf(colors: ThemeColors): ColorScheme {
return colorSchemeOf(colors.lightColorScheme.merge(DefaultLightColorScheme), colors.corePalette)
}
@Composable
fun darkColorSchemeOf(theme: Theme): ColorScheme {
return colorSchemeOf(theme.darkColorScheme.merge(DefaultDarkColorScheme), theme.corePalette)
fun darkColorSchemeOf(colors: ThemeColors): ColorScheme {
return colorSchemeOf(colors.darkColorScheme.merge(DefaultDarkColorScheme), colors.corePalette)
}
@Composable

View File

@ -1,67 +0,0 @@
package de.mm20.launcher2.ui.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
val LightDefaultColorScheme = lightColorScheme(
primary = Color(0xFF3D608A),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFD2E4FF),
onPrimaryContainer = Color(0xFF001C37),
inversePrimary = Color(0xFFA5C9F8),
secondary = Color(0xFF535F70),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFD7E3F8),
onSecondaryContainer = Color(0xFF101C2B),
tertiary = Color(0xFF6B5778),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFF3DAFF),
onTertiaryContainer = Color(0xFF251431),
surface = Color(0xFFFAF9FC),
onSurface = Color(0xFF1A1C1E),
onSurfaceVariant = Color(0xFF43474E),
inverseSurface = Color(0xFF2F3033),
inverseOnSurface = Color(0xFFF1F0F3),
error = Color(0xFFBA1A1A),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFFFFDAD5),
onErrorContainer = Color(0xFF410002),
outline = Color(0xFF73777F),
outlineVariant = Color(0xFFC3C6CF),
scrim = Color(0xFF000000),
background = Color(0xFFFDFCFF),
onBackground = Color(0xFF1A1C1E),
surfaceVariant = Color(0xFFDFE2EB),
)
val DarkDefaultColorScheme = darkColorScheme(
primary = Color(0xFFA5C9F8),
onPrimary = Color(0xFF023258),
primaryContainer = Color(0xFF224970),
onPrimaryContainer = Color(0xFFD2E4FF),
inversePrimary = Color(0xFF3D608A),
secondary = Color(0xFFBBC7DB),
onSecondary = Color(0xFF253141),
secondaryContainer = Color(0xFF3C4858),
onSecondaryContainer = Color(0xFFD7E3F8),
tertiary = Color(0xFFD6BEE4),
onTertiary = Color(0xFF3B2947),
tertiaryContainer = Color(0xFF523F5F),
onTertiaryContainer = Color(0xFFF3DAFF),
surface = Color(0xFF1A1C1E),
onSurface = Color(0xFFE3E2E5),
onSurfaceVariant = Color(0xFFC3C6CF),
inverseSurface = Color(0xFFE3E2E5),
inverseOnSurface = Color(0xFF2F3033),
error = Color(0xFFFFB4AB),
onError = Color(0xFF690004),
errorContainer = Color(0xFF930009),
onErrorContainer = Color(0xFFFFB4AB),
outline = Color(0xFF8D9199),
outlineVariant = Color(0xFF43474E),
scrim = Color(0xFF000000),
background = Color(0xFF1A1C1E),
onBackground = Color(0xFFE3E2E5),
surfaceVariant = Color(0xFF43474E),
)

View File

@ -1,67 +0,0 @@
package de.mm20.launcher2.ui.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
val LightEasterEggColorScheme = lightColorScheme(
primary = Color(0xFFB40180),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFFFD8E9),
onPrimaryContainer = Color(0xFF3C0029),
inversePrimary = Color(0xFFFFAFD7),
secondary = Color(0xFF725763),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFFDD9E7),
onSecondaryContainer = Color(0xFF29151F),
tertiary = Color(0xFF7F543B),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFDBC9),
onTertiaryContainer = Color(0xFF311302),
surface = Color(0xFFFCF8FC),
onSurface = Color(0xFF1C1B1E),
onSurfaceVariant = Color(0xFF4F4448),
inverseSurface = Color(0xFF313033),
inverseOnSurface = Color(0xFFF3EFF3),
error = Color(0xFFBA1A1A),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFFFFDAD5),
onErrorContainer = Color(0xFF410002),
outline = Color(0xFF817379),
outlineVariant = Color(0xFFD3C2C8),
scrim = Color(0xFF000000),
background = Color(0xFFFFFBFF),
onBackground = Color(0xFF1C1B1E),
surfaceVariant = Color(0xFFF0DEE4),
)
val DarkEasterEggColorScheme = darkColorScheme(
primary = Color(0xFFFFAFD7),
onPrimary = Color(0xFF620044),
primaryContainer = Color(0xFF8A0061),
onPrimaryContainer = Color(0xFFFFD8E9),
inversePrimary = Color(0xFFB40180),
secondary = Color(0xFFE0BDCB),
onSecondary = Color(0xFF402A35),
secondaryContainer = Color(0xFF59404B),
onSecondaryContainer = Color(0xFFFDD9E7),
tertiary = Color(0xFFF3BA9B),
onTertiary = Color(0xFF4A2812),
tertiaryContainer = Color(0xFF643D26),
onTertiaryContainer = Color(0xFFFFDBC9),
surface = Color(0xFF1C1B1E),
onSurface = Color(0xFFE5E1E5),
onSurfaceVariant = Color(0xFFD3C2C8),
inverseSurface = Color(0xFFE5E1E5),
inverseOnSurface = Color(0xFF313033),
error = Color(0xFFFFB4AB),
onError = Color(0xFF690004),
errorContainer = Color(0xFF930009),
onErrorContainer = Color(0xFFFFB4AB),
outline = Color(0xFF9C8D92),
outlineVariant = Color(0xFF4F4448),
scrim = Color(0xFF000000),
background = Color(0xFF1C1B1E),
onBackground = Color(0xFFE5E1E5),
surfaceVariant = Color(0xFF4F4448),
)

View File

@ -1,56 +0,0 @@
package de.mm20.launcher2.ui.theme.colorscheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import de.mm20.launcher2.ui.theme.WallpaperColors
import palettes.TonalPalette
import scheme.Scheme
fun MaterialYouCompatScheme(wallpaperColors: WallpaperColors, darkTheme: Boolean): ColorScheme {
val scheme = if (darkTheme) {
Scheme.dark(wallpaperColors.primary.toArgb())
} else {
Scheme.light(wallpaperColors.primary.toArgb())
}
return ColorScheme(
primary = Color(scheme.primary),
onPrimary = Color(scheme.onPrimary),
primaryContainer = Color(scheme.primaryContainer),
onPrimaryContainer = Color(scheme.onPrimaryContainer),
secondary = Color(scheme.secondary),
onSecondary = Color(scheme.onSecondary),
secondaryContainer = Color(scheme.secondaryContainer),
onSecondaryContainer = Color(scheme.onSecondaryContainer),
tertiary = Color(scheme.tertiary),
onTertiary = Color(scheme.onTertiary),
tertiaryContainer = Color(scheme.tertiaryContainer),
onTertiaryContainer = Color(scheme.onTertiaryContainer),
background = Color(scheme.background),
onBackground = Color(scheme.onBackground),
surface = Color(scheme.surface),
onSurface = Color(scheme.onSurface),
surfaceVariant = Color(scheme.surfaceVariant),
onSurfaceVariant = Color(scheme.onSurfaceVariant),
outline = Color(scheme.outline),
inverseSurface = Color(scheme.inverseSurface),
inverseOnSurface = Color(scheme.inverseOnSurface),
inversePrimary = Color(scheme.inversePrimary),
surfaceTint = Color(scheme.primary),
error = Color(scheme.error),
onError = Color(scheme.onError),
errorContainer = Color(scheme.errorContainer),
onErrorContainer = Color(scheme.onErrorContainer),
scrim = Color(scheme.scrim),
outlineVariant = Color(scheme.outlineVariant),
surfaceBright = Color(scheme.surfaceBright),
surfaceContainer = Color(scheme.surfaceContainer),
surfaceContainerHigh = Color(scheme.surfaceContainerHigh),
surfaceContainerHighest = Color(scheme.surfaceContainerHighest),
surfaceContainerLow = Color(scheme.surfaceContainerLow),
surfaceContainerLowest = Color(scheme.surfaceContainerLowest),
surfaceDim = Color(scheme.surfaceDim),
)
}

View File

@ -0,0 +1,64 @@
package de.mm20.launcher2.ui.theme.shapes
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.themes.CornerStyle
import de.mm20.launcher2.themes.Shape as ThemeShape
import de.mm20.launcher2.themes.Shapes as ThemeShapes
@Composable
fun shapesOf(shapes: ThemeShapes): Shapes {
return remember(shapes) {
Shapes(
extraSmall = fromShape(shapes.extraSmall, shapes.baseShape, 1f / 3f),
small = fromShape(shapes.small, shapes.baseShape, 2f / 3f),
medium = fromShape(shapes.medium, shapes.baseShape, 1f),
large = fromShape(shapes.large, shapes.baseShape, 4f / 3f),
largeIncreased = fromShape(shapes.largeIncreased, shapes.baseShape, 5f / 3f),
extraLarge = fromShape(shapes.extraLarge, shapes.baseShape, 7f / 3f),
extraLargeIncreased = fromShape(shapes.extraLargeIncreased, shapes.baseShape, 8f / 3f),
extraExtraLarge = fromShape(shapes.extraExtraLarge, shapes.baseShape, 12f / 3f),
)
}
}
private fun fromShape(shape: ThemeShape?, baseShape: ThemeShape, factor: Float): CornerBasedShape {
val topStart = getCornerRadius(shape, baseShape, factor, 0)
val topEnd = getCornerRadius(shape, baseShape, factor, 1)
val bottomEnd = getCornerRadius(shape, baseShape, factor, 2)
val bottomStart = getCornerRadius(shape, baseShape, factor, 3)
return if ((shape?.corners ?: baseShape.corners) == CornerStyle.Cut) {
CutCornerShape(
topStart = topStart,
topEnd = topEnd,
bottomEnd = bottomEnd,
bottomStart = bottomStart
)
} else {
RoundedCornerShape(
topStart = topStart,
topEnd = topEnd,
bottomEnd = bottomEnd,
bottomStart = bottomStart
)
}
}
private fun getCornerRadius(
shape: ThemeShape?,
baseShape: ThemeShape,
factor: Float,
index: Int
): CornerSize {
return CornerSize(
(shape?.radii?.get(index)?.toFloat() ?: ((baseShape.radii?.get(index)?.toFloat()
?: 12f) * factor)).dp
)
}

View File

@ -1740,4 +1740,44 @@ private val _BreezyWeather = materialIcon("Icons.Rounded.BreezyWeather") {
}
val Icons.Rounded.BreezyWeather
get() = _BreezyWeather
get() = _BreezyWeather
val _CutCorner = materialIcon("Icons.Rounded.CutCorner") {
materialPath {
moveTo(2f, 3f)
verticalLineTo(22f)
horizontalLineTo(21f)
verticalLineTo(11.585938f)
lineTo(12.414063f, 3f)
close()
moveToRelative(2f, 2f)
horizontalLineToRelative(7.585938f)
lineTo(19f, 12.414063f)
verticalLineTo(20f)
horizontalLineTo(4f)
close()
}
}
val Icons.Rounded.CutCorner
get() = _CutCorner
val _RoundedCornerAlt = materialIcon("Icons.Rounded.RoundedCornerAlt") {
materialPath {
moveTo(2f, 3f)
verticalLineTo(22f)
horizontalLineTo(21f)
verticalLineTo(12f)
curveTo(21f, 7.0412819f, 16.958718f, 3f, 12f, 3f)
close()
moveToRelative(2f, 2f)
horizontalLineToRelative(8f)
curveToRelative(3.877838f, 0f, 7f, 3.1221621f, 7f, 7f)
verticalLineToRelative(8f)
horizontalLineTo(4f)
close()
}
}
val Icons.Rounded.RoundedCornerAlt
get() = _RoundedCornerAlt

View File

@ -438,6 +438,11 @@
<string name="preference_mdy_color_source">Source for dynamic colors</string>
<string name="preference_mdy_color_source_system">System</string>
<string name="preference_mdy_color_source_wallpaper">Wallpaper</string>
<string name="preference_screen_shapes">Shapes</string>
<string name="preference_shapes_default">Default</string>
<string name="preference_shapes_extra_round">Extra round</string>
<string name="preference_shapes_rect">Rectangualar</string>
<string name="preference_shapes_base">Base shape</string>
<string name="preference_font">Font</string>
<string name="preference_font_system">System default</string>
<string name="preference_screen_about">About</string>
@ -819,7 +824,9 @@
<string name="note_widget_file_write_error">Error saving note</string>
<string name="note_widget_file_write_error_description">The note could not be written to the linked file. Possibly, it has been moved or deleted. A copy has been saved to the launcher\'s internal storage.</string>
<string name="confirmation_delete_color_scheme">Do you really want to delete the color scheme %1$s\?</string>
<string name="confirmation_delete_shapes_scheme">Do you really want to delete the shapes scheme %1$s\?</string>
<string name="new_color_scheme_name">New color scheme</string>
<string name="new_shapes_name">New shapes</string>
<string name="theme_color_scheme_system_default">Use system default</string>
<string name="theme_color_scheme_autogenerate">From primary color</string>
<string name="theme_color_scheme_palette_color">Palette</string>

View File

@ -11,7 +11,10 @@ data class LauncherSettingsData internal constructor(
val schemaVersion: Int = 5,
val uiColorScheme: ColorScheme = ColorScheme.System,
val uiTheme: ThemeDescriptor = ThemeDescriptor.Default,
@JsonNames("uiTheme")
val uiColors: ColorsDescriptor = ColorsDescriptor.Default,
val uiShapes: ShapesDescriptor = ShapesDescriptor.Default,
val uiCompatModeColors: Boolean = false,
val uiFont: Font = Font.Outfit,
@Deprecated("No longer in use, only used for migration")
@ -121,8 +124,10 @@ data class LauncherSettingsData internal constructor(
val systemBarsNavColors: SystemBarColors = SystemBarColors.Auto,
val surfacesOpacity: Float = 1f,
@Deprecated("Replaces with shape schemes")
val surfacesRadius: Int = 24,
val surfacesBorderWidth: Int = 0,
@Deprecated("Replaces with shape schemes")
val surfacesShape: SurfaceShape = SurfaceShape.Rounded,
val widgetsEditButton: Boolean = true,
@ -201,20 +206,45 @@ enum class Font {
@Serializable
sealed interface ThemeDescriptor {
sealed interface ColorsDescriptor {
@Serializable
@SerialName("default")
data object Default : ThemeDescriptor
data object Default : ColorsDescriptor
@Serializable
@SerialName("bw")
data object BlackAndWhite : ThemeDescriptor
data object BlackAndWhite : ColorsDescriptor
@Serializable
@SerialName("custom")
data class Custom(
val id: String,
) : ThemeDescriptor
) : ColorsDescriptor
}
@Serializable
sealed interface ShapesDescriptor {
@Serializable
@SerialName("default")
data object Default : ShapesDescriptor
@Serializable
@SerialName("cut")
data object Cut : ShapesDescriptor
@Serializable
@SerialName("extra_round")
data object ExtraRound : ShapesDescriptor
@Serializable
@SerialName("rect")
data object Rect : ShapesDescriptor
@Serializable
@SerialName("custom")
data class Custom(
val id: String,
) : ShapesDescriptor
}
internal enum class ClockWidgetStyleEnum {

View File

@ -7,16 +7,14 @@ import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.ScreenOrientation
import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.SurfaceShape
import de.mm20.launcher2.preferences.SystemBarColors
import de.mm20.launcher2.preferences.ThemeDescriptor
import de.mm20.launcher2.preferences.ColorsDescriptor
import de.mm20.launcher2.preferences.ShapesDescriptor
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
data class CardStyle(
val opacity: Float = 1f,
val cornerRadius: Int = 0,
val shape: SurfaceShape = SurfaceShape.Rounded,
val borderWidth: Int = 0,
)
@ -90,8 +88,6 @@ class UiSettings internal constructor(
get() = launcherDataStore.data.map {
CardStyle(
opacity = it.surfacesOpacity,
cornerRadius = it.surfacesRadius,
shape = it.surfacesShape,
borderWidth = it.surfacesBorderWidth,
)
}
@ -102,24 +98,12 @@ class UiSettings internal constructor(
}
}
fun setCardRadius(radius: Int) {
launcherDataStore.update {
it.copy(surfacesRadius = radius)
}
}
fun setCardBorderWidth(borderWidth: Int) {
launcherDataStore.update {
it.copy(surfacesBorderWidth = borderWidth)
}
}
fun setCardShape(shape: SurfaceShape) {
launcherDataStore.update {
it.copy(surfacesShape = shape)
}
}
val dimWallpaper
get() = launcherDataStore.data.map {
it.wallpaperDim
@ -302,14 +286,25 @@ class UiSettings internal constructor(
}
val theme
val colors
get() = launcherDataStore.data.map {
it.uiTheme
it.uiColors
}.distinctUntilChanged()
fun setTheme(theme: ThemeDescriptor) {
fun setColors(colors: ColorsDescriptor) {
launcherDataStore.update {
it.copy(uiTheme = theme)
it.copy(uiColors = colors)
}
}
val shapes
get() = launcherDataStore.data.map {
it.uiShapes
}.distinctUntilChanged()
fun setShapes(shapes: ShapesDescriptor) {
launcherDataStore.update {
it.copy(uiShapes = shapes)
}
}

View File

@ -18,7 +18,8 @@ import de.mm20.launcher2.database.entities.IconPackEntity
import de.mm20.launcher2.database.entities.PluginEntity
import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.database.entities.SearchActionEntity
import de.mm20.launcher2.database.entities.ThemeEntity
import de.mm20.launcher2.database.entities.ColorsEntity
import de.mm20.launcher2.database.entities.ShapesEntity
import de.mm20.launcher2.database.entities.WidgetEntity
import de.mm20.launcher2.database.migrations.Migration_10_11
import de.mm20.launcher2.database.migrations.Migration_11_12
@ -37,6 +38,7 @@ import de.mm20.launcher2.database.migrations.Migration_23_24
import de.mm20.launcher2.database.migrations.Migration_24_25
import de.mm20.launcher2.database.migrations.Migration_25_26
import de.mm20.launcher2.database.migrations.Migration_26_27
import de.mm20.launcher2.database.migrations.Migration_27_28
import de.mm20.launcher2.database.migrations.Migration_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9
@ -54,9 +56,10 @@ import java.util.UUID
WidgetEntity::class,
CustomAttributeEntity::class,
SearchActionEntity::class,
ThemeEntity::class,
ColorsEntity::class,
PluginEntity::class,
], version = 27, exportSchema = true
ShapesEntity::class,
], version = 28, exportSchema = true
)
@TypeConverters(ComponentNameConverter::class)
abstract class AppDatabase : RoomDatabase() {
@ -156,6 +159,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_24_25(),
Migration_25_26(),
Migration_26_27(),
Migration_27_28(),
).build()
if (_instance == null) _instance = instance
return instance

View File

@ -4,30 +4,52 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import de.mm20.launcher2.database.entities.ThemeEntity
import de.mm20.launcher2.database.entities.ColorsEntity
import de.mm20.launcher2.database.entities.ShapesEntity
import kotlinx.coroutines.flow.Flow
import java.util.UUID
@Dao
interface ThemeDao {
@Query("SELECT * FROM Theme")
fun getAll(): Flow<List<ThemeEntity>>
fun getAllColors(): Flow<List<ColorsEntity>>
@Query("SELECT * FROM Shapes")
fun getAllShapes(): Flow<List<ShapesEntity>>
@Query("SELECT * FROM Theme WHERE id = :id LIMIT 1")
fun get(id: UUID): Flow<ThemeEntity?>
fun getColors(id: UUID): Flow<ColorsEntity?>
@Query("SELECT * FROM Shapes WHERE id = :id LIMIT 1")
fun getShapes(id: UUID): Flow<ShapesEntity?>
@Insert
suspend fun insert(theme: ThemeEntity)
suspend fun insertColors(colors: ColorsEntity)
@Insert
suspend fun insertShapes(shapes: ShapesEntity)
@Update
suspend fun update(theme: ThemeEntity)
suspend fun updateColors(colors: ColorsEntity)
@Update
suspend fun updateShapes(shapes: ShapesEntity)
@Query("DELETE FROM Theme WHERE id = :id")
suspend fun delete(id: UUID)
suspend fun deleteColors(id: UUID)
@Query("DELETE FROM Shapes WHERE id = :id")
suspend fun deleteShapes(id: UUID)
@Query("DELETE FROM Theme")
suspend fun deleteAll()
suspend fun deleteAllColors()
@Query("DELETE FROM Shapes")
suspend fun deleteAllShapes()
@Insert
fun insertAll(themes: List<ThemeEntity>)
fun insertAllColors(colors: List<ColorsEntity>)
@Insert
fun insertAllShapes(shapes: List<ShapesEntity>)
}

View File

@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "Theme")
data class ThemeEntity(
data class ColorsEntity(
@PrimaryKey val id: UUID,
val name: String,

View File

@ -0,0 +1,22 @@
package de.mm20.launcher2.database.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "Shapes")
data class ShapesEntity(
@PrimaryKey val id: UUID,
val name: String,
val baseShape: String,
val extraSmall: String? = null,
val small: String? = null,
val medium: String? = null,
val large: String? = null,
val largeIncreased: String? = null,
val extraLarge: String? = null,
val extraLargeIncreased: String? = null,
val extraExtraLarge: String? = null,
)

View File

@ -0,0 +1,27 @@
package de.mm20.launcher2.database.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration_27_28: Migration(27, 28) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Shapes` (
`id` BLOB NOT NULL PRIMARY KEY,
`name` TEXT NOT NULL,
`baseShape` TEXT NOT NULL,
`extraSmall` TEXT,
`small` TEXT,
`medium` TEXT,
`large` TEXT,
`largeIncreased` TEXT,
`extraLarge` TEXT,
`extraLargeIncreased` TEXT,
`extraExtraLarge` TEXT
)
""".trimIndent()
)
}
}

View File

@ -1,138 +1,13 @@
package de.mm20.launcher2.themes
import de.mm20.launcher2.database.entities.ThemeEntity
import de.mm20.launcher2.database.entities.ColorsEntity
import hct.Hct
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.UUID
enum class CorePaletteColor {
Primary,
Secondary,
Tertiary,
Neutral,
NeutralVariant,
Error;
override fun toString(): String {
return when (this) {
Primary -> "p"
Secondary -> "s"
Tertiary -> "t"
Neutral -> "n"
NeutralVariant -> "nv"
Error -> "e"
}
}
}
fun CorePaletteColor(color: String): CorePaletteColor? {
return when (color) {
"p" -> CorePaletteColor.Primary
"s" -> CorePaletteColor.Secondary
"t" -> CorePaletteColor.Tertiary
"n" -> CorePaletteColor.Neutral
"nv" -> CorePaletteColor.NeutralVariant
"e" -> CorePaletteColor.Error
else -> null
}
}
sealed interface Color
internal fun Color(string: String?): Color? {
if (string == null) return null
if (string.startsWith("#")) {
return StaticColor(string.substring(1).toLongOrNull(16)?.toInt() ?: return null)
}
if (string.startsWith("$")) {
val parts = string.substring(1).split(".").takeIf { it.size == 2 } ?: return null
val color = CorePaletteColor(parts[0]) ?: return null
return ColorRef(
color = color,
tone = parts[1].toIntOrNull() ?: return null,
)
}
return null
}
data class ColorRef(
val color: CorePaletteColor,
val tone: Int,
) : Color {
override fun toString(): String {
return "\$$color.$tone"
}
}
@JvmInline
value class StaticColor(val color: Int) : Color {
override fun toString(): String {
return "#${color.toUInt().toString(16).padStart(8, '0')}"
}
}
@Serializable
data class CorePalette<out T : Int?>(
val primary: T,
val secondary: T,
val tertiary: T,
val neutral: T,
val neutralVariant: T,
val error: T,
)
val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
typealias FullCorePalette = CorePalette<Int>
typealias PartialCorePalette = CorePalette<Int?>
@Serializable
data class ColorScheme<out T : Color?>(
val primary: T,
val onPrimary: T,
val primaryContainer: T,
val onPrimaryContainer: T,
val secondary: T,
val onSecondary: T,
val secondaryContainer: T,
val onSecondaryContainer: T,
val tertiary: T,
val onTertiary: T,
val tertiaryContainer: T,
val onTertiaryContainer: T,
val error: T,
val onError: T,
val errorContainer: T,
val onErrorContainer: T,
val surface: T,
val onSurface: T,
val onSurfaceVariant: T,
val outline: T,
val outlineVariant: T,
val inverseSurface: T,
val inverseOnSurface: T,
val inversePrimary: T,
val surfaceDim: T,
val surfaceBright: T,
val surfaceContainerLowest: T,
val surfaceContainerLow: T,
val surfaceContainer: T,
val surfaceContainerHigh: T,
val surfaceContainerHighest: T,
val background: T,
val onBackground: T,
val surfaceTint: T,
val scrim: T,
val surfaceVariant: T,
)
typealias FullColorScheme = ColorScheme<Color>
typealias PartialColorScheme = ColorScheme<Color?>
@Serializable
data class Theme(
data class Colors(
@Transient val id: UUID = UUID.randomUUID(),
val builtIn: Boolean = false,
val name: String,
@ -141,7 +16,7 @@ data class Theme(
val darkColorScheme: PartialColorScheme = DefaultDarkColorScheme,
) {
constructor(entity: ThemeEntity) : this(
constructor(entity: ColorsEntity) : this(
id = entity.id,
builtIn = false,
name = entity.name,
@ -154,86 +29,86 @@ data class Theme(
error = entity.corePaletteE,
),
lightColorScheme = ColorScheme(
primary = Color(entity.lightPrimary),
onPrimary = Color(entity.lightOnPrimary),
primaryContainer = Color(entity.lightPrimaryContainer),
onPrimaryContainer = Color(entity.lightOnPrimaryContainer),
secondary = Color(entity.lightSecondary),
onSecondary = Color(entity.lightOnSecondary),
secondaryContainer = Color(entity.lightSecondaryContainer),
onSecondaryContainer = Color(entity.lightOnSecondaryContainer),
tertiary = Color(entity.lightTertiary),
onTertiary = Color(entity.lightOnTertiary),
tertiaryContainer = Color(entity.lightTertiaryContainer),
onTertiaryContainer = Color(entity.lightOnTertiaryContainer),
error = Color(entity.lightError),
onError = Color(entity.lightOnError),
errorContainer = Color(entity.lightErrorContainer),
onErrorContainer = Color(entity.lightOnErrorContainer),
surface = Color(entity.lightSurface),
onSurface = Color(entity.lightOnSurface),
onSurfaceVariant = Color(entity.lightOnSurfaceVariant),
outline = Color(entity.lightOutline),
outlineVariant = Color(entity.lightOutlineVariant),
inverseSurface = Color(entity.lightInverseSurface),
inverseOnSurface = Color(entity.lightInverseOnSurface),
inversePrimary = Color(entity.lightInversePrimary),
surfaceDim = Color(entity.lightSurfaceDim),
surfaceBright = Color(entity.lightSurfaceBright),
surfaceContainerLowest = Color(entity.lightSurfaceContainerLowest),
surfaceContainerLow = Color(entity.lightSurfaceContainerLow),
surfaceContainer = Color(entity.lightSurfaceContainer),
surfaceContainerHigh = Color(entity.lightSurfaceContainerHigh),
surfaceContainerHighest = Color(entity.lightSurfaceContainerHighest),
background = Color(entity.lightBackground),
onBackground = Color(entity.lightOnBackground),
surfaceTint = Color(entity.lightSurfaceTint),
scrim = Color(entity.lightScrim),
surfaceVariant = Color(entity.lightSurfaceVariant),
primary = Color.fromString(entity.lightPrimary),
onPrimary = Color.fromString(entity.lightOnPrimary),
primaryContainer = Color.fromString(entity.lightPrimaryContainer),
onPrimaryContainer = Color.fromString(entity.lightOnPrimaryContainer),
secondary = Color.fromString(entity.lightSecondary),
onSecondary = Color.fromString(entity.lightOnSecondary),
secondaryContainer = Color.fromString(entity.lightSecondaryContainer),
onSecondaryContainer = Color.fromString(entity.lightOnSecondaryContainer),
tertiary = Color.fromString(entity.lightTertiary),
onTertiary = Color.fromString(entity.lightOnTertiary),
tertiaryContainer = Color.fromString(entity.lightTertiaryContainer),
onTertiaryContainer = Color.fromString(entity.lightOnTertiaryContainer),
error = Color.fromString(entity.lightError),
onError = Color.fromString(entity.lightOnError),
errorContainer = Color.fromString(entity.lightErrorContainer),
onErrorContainer = Color.fromString(entity.lightOnErrorContainer),
surface = Color.fromString(entity.lightSurface),
onSurface = Color.fromString(entity.lightOnSurface),
onSurfaceVariant = Color.fromString(entity.lightOnSurfaceVariant),
outline = Color.fromString(entity.lightOutline),
outlineVariant = Color.fromString(entity.lightOutlineVariant),
inverseSurface = Color.fromString(entity.lightInverseSurface),
inverseOnSurface = Color.fromString(entity.lightInverseOnSurface),
inversePrimary = Color.fromString(entity.lightInversePrimary),
surfaceDim = Color.fromString(entity.lightSurfaceDim),
surfaceBright = Color.fromString(entity.lightSurfaceBright),
surfaceContainerLowest = Color.fromString(entity.lightSurfaceContainerLowest),
surfaceContainerLow = Color.fromString(entity.lightSurfaceContainerLow),
surfaceContainer = Color.fromString(entity.lightSurfaceContainer),
surfaceContainerHigh = Color.fromString(entity.lightSurfaceContainerHigh),
surfaceContainerHighest = Color.fromString(entity.lightSurfaceContainerHighest),
background = Color.fromString(entity.lightBackground),
onBackground = Color.fromString(entity.lightOnBackground),
surfaceTint = Color.fromString(entity.lightSurfaceTint),
scrim = Color.fromString(entity.lightScrim),
surfaceVariant = Color.fromString(entity.lightSurfaceVariant),
),
darkColorScheme = ColorScheme(
primary = Color(entity.darkPrimary),
onPrimary = Color(entity.darkOnPrimary),
primaryContainer = Color(entity.darkPrimaryContainer),
onPrimaryContainer = Color(entity.darkOnPrimaryContainer),
secondary = Color(entity.darkSecondary),
onSecondary = Color(entity.darkOnSecondary),
secondaryContainer = Color(entity.darkSecondaryContainer),
onSecondaryContainer = Color(entity.darkOnSecondaryContainer),
tertiary = Color(entity.darkTertiary),
onTertiary = Color(entity.darkOnTertiary),
tertiaryContainer = Color(entity.darkTertiaryContainer),
onTertiaryContainer = Color(entity.darkOnTertiaryContainer),
error = Color(entity.darkError),
onError = Color(entity.darkOnError),
errorContainer = Color(entity.darkErrorContainer),
onErrorContainer = Color(entity.darkOnErrorContainer),
surface = Color(entity.darkSurface),
onSurface = Color(entity.darkOnSurface),
onSurfaceVariant = Color(entity.darkOnSurfaceVariant),
outline = Color(entity.darkOutline),
outlineVariant = Color(entity.darkOutlineVariant),
inverseSurface = Color(entity.darkInverseSurface),
inverseOnSurface = Color(entity.darkInverseOnSurface),
inversePrimary = Color(entity.darkInversePrimary),
surfaceDim = Color(entity.darkSurfaceDim),
surfaceBright = Color(entity.darkSurfaceBright),
surfaceContainerLowest = Color(entity.darkSurfaceContainerLowest),
surfaceContainerLow = Color(entity.darkSurfaceContainerLow),
surfaceContainer = Color(entity.darkSurfaceContainer),
surfaceContainerHigh = Color(entity.darkSurfaceContainerHigh),
surfaceContainerHighest = Color(entity.darkSurfaceContainerHighest),
background = Color(entity.darkBackground),
onBackground = Color(entity.darkOnBackground),
surfaceTint = Color(entity.darkSurfaceTint),
scrim = Color(entity.darkScrim),
surfaceVariant = Color(entity.darkSurfaceVariant),
primary = Color.fromString(entity.darkPrimary),
onPrimary = Color.fromString(entity.darkOnPrimary),
primaryContainer = Color.fromString(entity.darkPrimaryContainer),
onPrimaryContainer = Color.fromString(entity.darkOnPrimaryContainer),
secondary = Color.fromString(entity.darkSecondary),
onSecondary = Color.fromString(entity.darkOnSecondary),
secondaryContainer = Color.fromString(entity.darkSecondaryContainer),
onSecondaryContainer = Color.fromString(entity.darkOnSecondaryContainer),
tertiary = Color.fromString(entity.darkTertiary),
onTertiary = Color.fromString(entity.darkOnTertiary),
tertiaryContainer = Color.fromString(entity.darkTertiaryContainer),
onTertiaryContainer = Color.fromString(entity.darkOnTertiaryContainer),
error = Color.fromString(entity.darkError),
onError = Color.fromString(entity.darkOnError),
errorContainer = Color.fromString(entity.darkErrorContainer),
onErrorContainer = Color.fromString(entity.darkOnErrorContainer),
surface = Color.fromString(entity.darkSurface),
onSurface = Color.fromString(entity.darkOnSurface),
onSurfaceVariant = Color.fromString(entity.darkOnSurfaceVariant),
outline = Color.fromString(entity.darkOutline),
outlineVariant = Color.fromString(entity.darkOutlineVariant),
inverseSurface = Color.fromString(entity.darkInverseSurface),
inverseOnSurface = Color.fromString(entity.darkInverseOnSurface),
inversePrimary = Color.fromString(entity.darkInversePrimary),
surfaceDim = Color.fromString(entity.darkSurfaceDim),
surfaceBright = Color.fromString(entity.darkSurfaceBright),
surfaceContainerLowest = Color.fromString(entity.darkSurfaceContainerLowest),
surfaceContainerLow = Color.fromString(entity.darkSurfaceContainerLow),
surfaceContainer = Color.fromString(entity.darkSurfaceContainer),
surfaceContainerHigh = Color.fromString(entity.darkSurfaceContainerHigh),
surfaceContainerHighest = Color.fromString(entity.darkSurfaceContainerHighest),
background = Color.fromString(entity.darkBackground),
onBackground = Color.fromString(entity.darkOnBackground),
surfaceTint = Color.fromString(entity.darkSurfaceTint),
scrim = Color.fromString(entity.darkScrim),
surfaceVariant = Color.fromString(entity.darkSurfaceVariant),
),
)
internal fun toEntity(): ThemeEntity {
return ThemeEntity(
internal fun toEntity(): ColorsEntity {
return ColorsEntity(
id = id,
name = name,
corePaletteA1 = corePalette.primary,
@ -320,6 +195,137 @@ data class Theme(
}
}
enum class CorePaletteColor {
Primary,
Secondary,
Tertiary,
Neutral,
NeutralVariant,
Error;
override fun toString(): String {
return when (this) {
Primary -> "p"
Secondary -> "s"
Tertiary -> "t"
Neutral -> "n"
NeutralVariant -> "nv"
Error -> "e"
}
}
companion object {
fun fromString(string: String): CorePaletteColor? {
return when (string) {
"p" -> Primary
"s" -> Secondary
"t" -> Tertiary
"n" -> Neutral
"nv" -> NeutralVariant
"e" -> Error
else -> null
}
}
}
}
@Serializable(with = ColorSerializer::class)
sealed interface Color {
companion object {
fun fromString(string: String?): Color? {
if (string == null) return null
if (string.startsWith("#")) {
return StaticColor(string.substring(1).toLongOrNull(16)?.toInt() ?: return null)
}
if (string.startsWith("$")) {
val parts = string.substring(1).split(".").takeIf { it.size == 2 } ?: return null
val color = CorePaletteColor.fromString(parts[0]) ?: return null
return ColorRef(
color = color,
tone = parts[1].toIntOrNull() ?: return null,
)
}
return null
}
}
}
data class ColorRef(
val color: CorePaletteColor,
val tone: Int,
) : Color {
override fun toString(): String {
return "$$color.$tone"
}
}
@JvmInline
value class StaticColor(val color: Int) : Color {
override fun toString(): String {
return "#${color.toUInt().toString(16).padStart(8, '0')}"
}
}
@Serializable
data class CorePalette<out T : Int?>(
val primary: T,
val secondary: T,
val tertiary: T,
val neutral: T,
val neutralVariant: T,
val error: T,
)
val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
typealias FullCorePalette = CorePalette<Int>
typealias PartialCorePalette = CorePalette<Int?>
@Serializable
data class ColorScheme<out T : Color?>(
val primary: T,
val onPrimary: T,
val primaryContainer: T,
val onPrimaryContainer: T,
val secondary: T,
val onSecondary: T,
val secondaryContainer: T,
val onSecondaryContainer: T,
val tertiary: T,
val onTertiary: T,
val tertiaryContainer: T,
val onTertiaryContainer: T,
val error: T,
val onError: T,
val errorContainer: T,
val onErrorContainer: T,
val surface: T,
val onSurface: T,
val onSurfaceVariant: T,
val outline: T,
val outlineVariant: T,
val inverseSurface: T,
val inverseOnSurface: T,
val inversePrimary: T,
val surfaceDim: T,
val surfaceBright: T,
val surfaceContainerLowest: T,
val surfaceContainerLow: T,
val surfaceContainer: T,
val surfaceContainerHigh: T,
val surfaceContainerHighest: T,
val background: T,
val onBackground: T,
val surfaceTint: T,
val scrim: T,
val surfaceVariant: T,
)
typealias FullColorScheme = ColorScheme<Color>
typealias PartialColorScheme = ColorScheme<Color?>
fun <T : Int?> CorePalette<T>.get(color: CorePaletteColor): T {
return when (color) {
CorePaletteColor.Primary -> primary

View File

@ -4,6 +4,10 @@ import java.util.UUID
val DefaultThemeId = UUID(0L, 0L)
val BlackAndWhiteThemeId = UUID(0L, 1L)
val ExtraRoundShapesId = UUID(0L, 1L)
val CutShapesId = UUID(0L, 2L)
val RectShapesId = UUID(0L, 3L)
val DefaultLightColorScheme = ColorScheme<Color>(
primary = ColorRef(CorePaletteColor.Primary, 40),
@ -83,7 +87,6 @@ val DefaultDarkColorScheme = ColorScheme<Color>(
scrim = ColorRef(CorePaletteColor.Neutral, 0),
)
val BlackAndWhiteThemeId = UUID(0L, 1L)
val BlackAndWhiteLightColorScheme = ColorScheme<Color?>(
primary = StaticColor(0xFF000000.toInt()),

View File

@ -0,0 +1,67 @@
package de.mm20.launcher2.themes
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
internal class LegacyColorRefSerializer: KSerializer<ColorRef> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("$", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): ColorRef {
return Color.fromString(decoder.decodeString()) as ColorRef
}
override fun serialize(encoder: Encoder, value: ColorRef) {
encoder.encodeString(value.toString())
}
}
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
internal class LegacyStaticColorSerializer: KSerializer<StaticColor> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("#", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): StaticColor {
return Color.fromString(decoder.decodeString()) as StaticColor
}
override fun serialize(encoder: Encoder, value: StaticColor) {
encoder.encodeString(value.toString())
}
}
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
internal val legacyModule = SerializersModule {
polymorphic(Color::class) {
subclass(ColorRef::class, LegacyColorRefSerializer())
subclass(StaticColor::class, LegacyStaticColorSerializer())
}
}
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
val LegacyThemeJson = Json {
serializersModule = legacyModule
useArrayPolymorphism = true
}
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
fun Colors.toLegacyJson(): String {
return LegacyThemeJson.encodeToString(this)
}
@Deprecated("Only used for backwards compatibility with old themes. New themes should use the new serialization format.")
fun Colors.Companion.fromLegacyJson(json: String): Colors {
return try {
LegacyThemeJson.decodeFromString(json)
} catch (e: SerializationException) {
throw IllegalArgumentException(e)
}
}

View File

@ -1,62 +1,39 @@
package de.mm20.launcher2.themes
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import kotlinx.serialization.modules.polymorphic
internal class ColorRefSerializer: KSerializer<ColorRef> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("$", PrimitiveKind.STRING)
internal class ColorSerializer: KSerializer<Color> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): ColorRef {
return Color(decoder.decodeString()) as ColorRef
}
override fun serialize(encoder: Encoder, value: ColorRef) {
override fun serialize(
encoder: Encoder,
value: Color
) {
encoder.encodeString(value.toString())
}
}
internal class StaticColorSerializer: KSerializer<StaticColor> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("#", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): StaticColor {
return Color(decoder.decodeString()) as StaticColor
override fun deserialize(decoder: Decoder): Color {
TODO("Not yet implemented")
}
override fun serialize(encoder: Encoder, value: StaticColor) {
}
internal class ShapeSerializer: KSerializer<Shape> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ShapeSerializer", PrimitiveKind.STRING)
override fun serialize(
encoder: Encoder,
value: Shape
) {
encoder.encodeString(value.toString())
}
}
internal val module = SerializersModule {
polymorphic(Color::class) {
subclass(ColorRef::class, ColorRefSerializer())
subclass(StaticColor::class, StaticColorSerializer())
}
}
val ThemeJson = Json {
serializersModule = module
useArrayPolymorphism = true
}
fun Theme.toJson(): String {
return ThemeJson.encodeToString(this)
}
fun Theme.Companion.fromJson(json: String): Theme {
return try {
ThemeJson.decodeFromString(json)
} catch (e: SerializationException) {
throw IllegalArgumentException(e)
override fun deserialize(decoder: Decoder): Shape {
return Shape.fromString(decoder.decodeString())!!
}
}

View File

@ -0,0 +1,120 @@
package de.mm20.launcher2.themes
import de.mm20.launcher2.database.entities.ShapesEntity
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.UUID
@Serializable
data class Shapes(
@Transient val id: UUID = UUID.randomUUID(),
val builtIn: Boolean = false,
val name: String,
val baseShape: Shape = Shape(
corners = CornerStyle.Rounded,
radii = intArrayOf(12, 12, 12, 12),
),
val extraSmall: Shape? = null,
val small: Shape? = null,
val medium: Shape? = null,
val large: Shape? = null,
val largeIncreased: Shape? = null,
val extraLarge: Shape? = null,
val extraLargeIncreased: Shape? = null,
val extraExtraLarge: Shape? = null,
) {
constructor(entity: ShapesEntity) : this(
id = entity.id,
builtIn = false,
name = entity.name,
baseShape = Shape.fromString(entity.baseShape) ?: Shape(
corners = CornerStyle.Rounded,
radii = intArrayOf(12, 12, 12, 12)
),
extraSmall = Shape.fromString(entity.extraSmall),
small = Shape.fromString(entity.small),
medium = Shape.fromString(entity.medium),
large = Shape.fromString(entity.large),
largeIncreased = Shape.fromString(entity.largeIncreased),
extraLarge = Shape.fromString(entity.extraLarge),
extraLargeIncreased = Shape.fromString(entity.extraLargeIncreased),
extraExtraLarge = Shape.fromString(entity.extraExtraLarge),
)
internal fun toEntity(): ShapesEntity {
return ShapesEntity(
id = id,
name = name,
baseShape = baseShape.toString(),
extraSmall = extraSmall?.toString(),
small = small?.toString(),
medium = medium?.toString(),
large = large?.toString(),
largeIncreased = largeIncreased?.toString(),
extraLarge = extraLarge?.toString(),
extraLargeIncreased = extraLargeIncreased?.toString(),
extraExtraLarge = extraExtraLarge?.toString(),
)
}
}
@Serializable(with = ShapeSerializer::class)
data class Shape(
/**
* The style of the corners.
* null to inherit the corner style from the base shape.
*/
val corners: CornerStyle? = null,
/**
* Radii in dp, in the order of top-start, top-end, bottom-end, bottom-start.
* null to inherit the radius from the base shape.
*/
val radii: IntArray? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Shape) return false
return corners == other.corners &&
radii.contentEquals(other.radii)
}
override fun hashCode(): Int {
var result = corners.hashCode()
result = 31 * result + radii.contentHashCode()
return result
}
override fun toString(): String {
val type = when (corners) {
CornerStyle.Rounded -> "r"
CornerStyle.Cut -> "c"
null -> "$"
}
val radii = radii?.joinToString("|") ?: ""
return "$type.${radii}"
}
companion object {
fun fromString(string: String?): Shape? {
if (string == null) return null
val parts = string.split('.')
val corners = when (parts[0]) {
"r" -> CornerStyle.Rounded
"c" -> CornerStyle.Cut
else -> null
}
val radii = if (parts.size > 1 && parts[1].isNotEmpty()) {
parts[1].split("|").map { it.toInt() }.toIntArray()
} else {
null
}
return Shape(corners, radii)
}
}
}
enum class CornerStyle {
Rounded,
Cut,
}

View File

@ -4,7 +4,8 @@ import android.content.Context
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.preferences.ThemeDescriptor
import de.mm20.launcher2.preferences.ColorsDescriptor
import de.mm20.launcher2.preferences.ShapesDescriptor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -16,7 +17,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import java.io.File
import java.util.UUID
@ -26,50 +26,50 @@ class ThemeRepository(
) : Backupable {
private val scope = CoroutineScope(Dispatchers.IO + Job())
fun getThemes(): Flow<List<Theme>> {
return database.themeDao().getAll().map {
getBuiltInThemes() + it.map { Theme(it) }
fun getAllColors(): Flow<List<Colors>> {
return database.themeDao().getAllColors().map {
getBuiltInColors() + it.map { Colors(it) }
}
}
fun getTheme(id: UUID): Flow<Theme?> {
if (id == DefaultThemeId) return flowOf(getDefaultTheme())
if (id == BlackAndWhiteThemeId) return flowOf(getBlackAndWhiteTheme())
return database.themeDao().get(id).map { it?.let { Theme(it) } }.flowOn(Dispatchers.Default)
fun getColors(id: UUID): Flow<Colors?> {
if (id == DefaultThemeId) return flowOf(getDefaultColors())
if (id == BlackAndWhiteThemeId) return flowOf(getBlackAndWhiteColors())
return database.themeDao().getColors(id).map { it?.let { Colors(it) } }.flowOn(Dispatchers.Default)
}
fun createTheme(theme: Theme) {
fun createColors(colors: Colors) {
scope.launch {
database.themeDao().insert(theme.toEntity())
database.themeDao().insertColors(colors.toEntity())
}
}
fun updateTheme(theme: Theme) {
fun updateColors(colors: Colors) {
scope.launch {
database.themeDao().update(theme.toEntity())
database.themeDao().updateColors(colors.toEntity())
}
}
fun getThemeOrDefault(theme: ThemeDescriptor?): Flow<Theme> {
fun getColorsOrDefault(theme: ColorsDescriptor?): Flow<Colors> {
return when(theme) {
is ThemeDescriptor.BlackAndWhite -> flowOf(getBlackAndWhiteTheme())
is ThemeDescriptor.Custom -> {
is ColorsDescriptor.BlackAndWhite -> flowOf(getBlackAndWhiteColors())
is ColorsDescriptor.Custom -> {
val id = UUID.fromString(theme.id)
getTheme(id).map { it ?: getDefaultTheme() }
getColors(id).map { it ?: getDefaultColors() }
}
else -> flowOf(getDefaultTheme())
else -> flowOf(getDefaultColors())
}
}
private fun getBuiltInThemes(): List<Theme> {
private fun getBuiltInColors(): List<Colors> {
return listOf(
getDefaultTheme(),
getBlackAndWhiteTheme(),
getDefaultColors(),
getBlackAndWhiteColors(),
)
}
fun getDefaultTheme(): Theme {
return Theme(
private fun getDefaultColors(): Colors {
return Colors(
id = DefaultThemeId,
builtIn = true,
name = context.getString(R.string.preference_colors_default),
@ -79,8 +79,8 @@ class ThemeRepository(
)
}
private fun getBlackAndWhiteTheme(): Theme {
return Theme(
private fun getBlackAndWhiteColors(): Colors {
return Colors(
id = BlackAndWhiteThemeId,
builtIn = true,
name = context.getString(R.string.preference_colors_bw),
@ -90,16 +90,127 @@ class ThemeRepository(
)
}
fun deleteTheme(theme: Theme) {
fun deleteColors(colors: Colors) {
scope.launch {
database.themeDao().delete(theme.id)
database.themeDao().deleteColors(colors.id)
}
}
fun getAllShapes(): Flow<List<Shapes>> {
return database.themeDao().getAllShapes().map {
getBuiltInShapes() + it.map { Shapes(it) }
}
}
fun getShapes(id: UUID): Flow<Shapes?> {
if (id == DefaultThemeId) return flowOf(getDefaultShapes())
if (id == ExtraRoundShapesId) return flowOf(getExtraRoundShapes())
if (id == RectShapesId) return flowOf(getRectShapes())
if (id == CutShapesId) return flowOf(getCutShapes())
return database.themeDao().getShapes(id).map { it?.let { Shapes(it) } }.flowOn(Dispatchers.Default)
}
fun createShapes(shapes: Shapes) {
scope.launch {
database.themeDao().insertShapes(shapes.toEntity())
}
}
fun updateShapes(shapes: Shapes) {
scope.launch {
database.themeDao().updateShapes(shapes.toEntity())
}
}
fun getShapesOrDefault(theme: ShapesDescriptor?): Flow<Shapes> {
return when(theme) {
is ShapesDescriptor.Custom -> {
val id = UUID.fromString(theme.id)
getShapes(id).map { it ?: getDefaultShapes() }
}
is ShapesDescriptor.ExtraRound -> flowOf(getExtraRoundShapes())
is ShapesDescriptor.Rect -> flowOf(getRectShapes())
is ShapesDescriptor.Cut -> flowOf(getCutShapes())
else -> flowOf(getDefaultShapes())
}
}
private fun getBuiltInShapes(): List<Shapes> {
return listOf(
getDefaultShapes(),
getExtraRoundShapes(),
getRectShapes(),
getCutShapes(),
)
}
private fun getDefaultShapes(): Shapes {
return Shapes(
id = DefaultThemeId,
builtIn = true,
name = context.getString(R.string.preference_shapes_default),
baseShape = Shape(
corners = CornerStyle.Rounded,
radii = intArrayOf(12, 12, 12, 12),
)
)
}
private fun getCutShapes(): Shapes {
return Shapes(
id = CutShapesId,
builtIn = true,
name = context.getString(R.string.preference_cards_shape_cut),
baseShape = Shape(
corners = CornerStyle.Cut,
radii = intArrayOf(12, 12, 12, 12),
)
)
}
private fun getExtraRoundShapes(): Shapes {
return Shapes(
id = ExtraRoundShapesId,
builtIn = true,
name = context.getString(R.string.preference_shapes_extra_round),
baseShape = Shape(
corners = CornerStyle.Rounded,
radii = intArrayOf(24, 24, 24, 24),
),
extraLarge = Shape(
radii = intArrayOf(36, 36, 36, 36),
),
extraLargeIncreased = Shape(
radii = intArrayOf(40, 40, 40, 40),
),
extraExtraLarge = Shape(
radii = intArrayOf(56, 56, 56, 56),
)
)
}
private fun getRectShapes(): Shapes {
return Shapes(
id = RectShapesId,
builtIn = true,
name = context.getString(R.string.preference_shapes_rect),
baseShape = Shape(
corners = CornerStyle.Rounded,
radii = intArrayOf(0, 0, 0, 0),
)
)
}
fun deleteShapes(shapes: Shapes) {
scope.launch {
database.themeDao().deleteShapes(shapes.id)
}
}
override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao()
val themes = dao.getAll().first().map { Theme(it) }
val data = ThemeJson.encodeToString(themes)
val colors = dao.getAllColors().first().map { Colors(it) }
val data = LegacyThemeJson.encodeToString(colors)
val file = File(toDir, "themes.0000")
file.bufferedWriter().use {
@ -109,7 +220,7 @@ class ThemeRepository(
override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao()
dao.deleteAll()
dao.deleteAllColors()
val files =
fromDir.listFiles { _, name -> name.startsWith("themes.") }
@ -117,8 +228,8 @@ class ThemeRepository(
for (file in files) {
val data = file.inputStream().reader().readText()
val themes: List<Theme> = try {
ThemeJson.decodeFromString(data)
val colors: List<Colors> = try {
LegacyThemeJson.decodeFromString(data)
} catch (e: SerializationException) {
CrashReporter.logException(e)
continue
@ -126,7 +237,7 @@ class ThemeRepository(
CrashReporter.logException(e)
continue
}
dao.insertAll(themes.map { it.toEntity() })
dao.insertAllColors(colors.map { it.toEntity() })
}
}