(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>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage import de.mm20.launcher2.ui.component.LargeMessage
@ -63,7 +63,7 @@ fun ImportThemeSheet(
viewModel.readTheme(context, uri) viewModel.readTheme(context, uri)
} }
val theme by viewModel.theme val theme by viewModel.colors
val error by viewModel.error val error by viewModel.error
var apply by viewModel.apply var apply by viewModel.apply
@ -132,13 +132,13 @@ fun ImportThemeSheet(
@Composable @Composable
fun ThemePreview( fun ThemePreview(
theme: Theme, colors: Colors,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val darkMode = LocalDarkTheme.current val darkMode = LocalDarkTheme.current
var darkTheme by remember { mutableStateOf(darkMode) } 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) { Column(modifier = modifier) {
Row( Row(
@ -146,7 +146,7 @@ fun ThemePreview(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = theme.name, text = colors.name,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant

View File

@ -5,11 +5,11 @@ import android.net.Uri
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.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.ThemeRepository
import de.mm20.launcher2.themes.fromJson import de.mm20.launcher2.themes.fromLegacyJson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -20,12 +20,12 @@ class ImportThemeSheetVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject() private val themeRepository: ThemeRepository by inject()
private val uiSettings: UiSettings by inject() private val uiSettings: UiSettings by inject()
val theme = mutableStateOf<Theme?>(null) val colors = mutableStateOf<Colors?>(null)
val error = mutableStateOf<Boolean>(false) val error = mutableStateOf<Boolean>(false)
val apply = mutableStateOf<Boolean>(false) val apply = mutableStateOf<Boolean>(false)
fun import() { fun import() {
val theme = theme.value val theme = colors.value
val apply = apply.value val apply = apply.value
if (theme != null) { if (theme != null) {
viewModelScope.launch { viewModelScope.launch {
@ -37,30 +37,30 @@ class ImportThemeSheetVM : ViewModel(), KoinComponent {
fun readTheme(context: Context, uri: Uri) { fun readTheme(context: Context, uri: Uri) {
error.value = false error.value = false
theme.value = null colors.value = null
apply.value = true apply.value = true
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val inputStream = val inputStream =
context.contentResolver.openInputStream(uri) ?: return@launch context.contentResolver.openInputStream(uri) ?: return@launch
val theme = inputStream.use { val colors = inputStream.use {
val json = it.readBytes().toString(Charsets.UTF_8) val json = it.readBytes().toString(Charsets.UTF_8)
try { try {
Theme.fromJson(json) Colors.fromLegacyJson(json)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
null null
} }
} }
this@ImportThemeSheetVM.theme.value = theme this@ImportThemeSheetVM.colors.value = colors
if (theme == null) { if (colors == null) {
error.value = true error.value = true
} }
} }
} }
private fun importTheme(theme: Theme, apply: Boolean) { private fun importTheme(colors: Colors, apply: Boolean) {
themeRepository.createTheme(theme) themeRepository.createColors(colors)
if (apply) { 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(!enabled) return this
if (top == 0.dp && bottom == 0.dp) return this if (top == 0.dp && bottom == 0.dp) return this
return this then drawWithContent { return drawWithContent {
val topColors = if (top > 0.dp) createColors( val topColors = if (top > 0.dp) createColors(
1f - amount, 1f - amount,

View File

@ -27,7 +27,7 @@ fun Modifier.verticalScrims(
if (!enabled) return this if (!enabled) return this
if (top == 0.dp && bottom == 0.dp) return this if (top == 0.dp && bottom == 0.dp) return this
return this then drawWithCache { return drawWithCache {
onDrawWithContent { onDrawWithContent {
val topColors = if (top > 0.dp) createColors( val topColors = if (top > 0.dp) createColors(
1f - amount, 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.CalendarProviderSettingsScreen
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemesSettingsScreen
import de.mm20.launcher2.ui.settings.contacts.ContactsSettingsScreen import de.mm20.launcher2.ui.settings.contacts.ContactsSettingsScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen 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.plugins.PluginsSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen 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.tags.TagsSettingsScreen
import de.mm20.launcher2.ui.settings.tasks.TasksIntegrationSettingsScreen import de.mm20.launcher2.ui.settings.tasks.TasksIntegrationSettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterHelpSettingsScreen import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterHelpSettingsScreen
@ -160,11 +162,11 @@ class SettingsActivity : BaseActivity() {
composable("settings/icons") { composable("settings/icons") {
IconsSettingsScreen() IconsSettingsScreen()
} }
composable("settings/appearance/themes") { composable("settings/appearance/colors") {
ThemesSettingsScreen() ColorSchemesSettingsScreen()
} }
composable( composable(
"settings/appearance/themes/{id}", "settings/appearance/colors/{id}",
arguments = listOf(navArgument("id") { arguments = listOf(navArgument("id") {
nullable = false nullable = false
}) })
@ -172,7 +174,21 @@ class SettingsActivity : BaseActivity() {
val id = it.arguments?.getString("id")?.let { val id = it.arguments?.getString("id")?.let {
UUID.fromString(it) UUID.fromString(it)
} ?: return@composable } ?: 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") { composable("settings/appearance/cards") {
CardsSettingsScreen() CardsSettingsScreen()

View File

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

View File

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

View File

@ -47,28 +47,6 @@ fun CardsSettingsScreen() {
} }
item { item {
PreferenceCategory { 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( SliderPreference(
title = stringResource(R.string.preference_cards_opacity), title = stringResource(R.string.preference_cards_opacity),
icon = Icons.Rounded.Opacity, icon = Icons.Rounded.Opacity,

View File

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

View File

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

View File

@ -1,20 +1,16 @@
package de.mm20.launcher2.ui.settings.colorscheme package de.mm20.launcher2.ui.settings.colorscheme
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons 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.Edit
import androidx.compose.material.icons.rounded.Lock import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.OpenInNew import androidx.compose.material.icons.rounded.OpenInNew
@ -60,6 +56,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer import de.mm20.launcher2.icons.TintedIconLayer
@ -78,8 +75,8 @@ import palettes.CorePalette
import java.util.UUID import java.util.UUID
@Composable @Composable
fun ThemeSettingsScreen(themeId: UUID) { fun ColorSchemeSettingsScreen(themeId: UUID) {
val viewModel: ThemesSettingsScreenVM = viewModel() val viewModel: ColorSchemesSettingsScreenVM = viewModel()
val context = LocalContext.current val context = LocalContext.current
val dark = LocalDarkTheme.current val dark = LocalDarkTheme.current
@ -107,7 +104,13 @@ fun ThemeSettingsScreen(themeId: UUID) {
var name by remember(theme) { mutableStateOf(theme?.name ?: "") } var name by remember(theme) { mutableStateOf(theme?.name ?: "") }
AlertDialog( AlertDialog(
onDismissRequest = { editName = false }, onDismissRequest = { editName = false },
text = { OutlinedTextField(value = name, onValueChange = { name = it }, singleLine = true) }, text = {
OutlinedTextField(
value = name,
onValueChange = { name = it },
singleLine = true
)
},
confirmButton = { confirmButton = {
Button( Button(
onClick = { onClick = {
@ -146,15 +149,10 @@ fun ThemeSettingsScreen(themeId: UUID) {
stringResource(R.string.preference_custom_colors_corepalette), stringResource(R.string.preference_custom_colors_corepalette),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(16.dp)
) {
CorePaletteColorPreference( CorePaletteColorPreference(
title = "Primary", title = "Primary",
value = theme?.corePalette?.primary, value = theme?.corePalette?.primary,
@ -269,12 +267,11 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
}, },
) )
} HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
HorizontalDivider()
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Primary colors", title = "Primary colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -302,7 +299,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.primary, defaultValue = selectedDefaultScheme.primary,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Primary", title = "On Primary",
@ -326,7 +322,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onPrimary, defaultValue = selectedDefaultScheme.onPrimary,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Primary Container", title = "Primary Container",
@ -350,7 +345,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.primaryContainer, defaultValue = selectedDefaultScheme.primaryContainer,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Primary Container", title = "On Primary Container",
@ -374,7 +368,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onPrimaryContainer, defaultValue = selectedDefaultScheme.onPrimaryContainer,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
@ -419,7 +412,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Secondary colors", title = "Secondary colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -447,7 +440,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.secondary, defaultValue = selectedDefaultScheme.secondary,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Secondary", title = "On Secondary",
@ -471,7 +463,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onSecondary, defaultValue = selectedDefaultScheme.onSecondary,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Secondary Container", title = "Secondary Container",
@ -495,7 +486,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.secondaryContainer, defaultValue = selectedDefaultScheme.secondaryContainer,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Secondary Container", title = "On Secondary Container",
@ -519,7 +509,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onSecondaryContainer, defaultValue = selectedDefaultScheme.onSecondaryContainer,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
) { ) {
@ -557,7 +546,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Tertiary colors", title = "Tertiary colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -585,7 +574,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.tertiary, defaultValue = selectedDefaultScheme.tertiary,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Tertiary", title = "On Tertiary",
@ -609,7 +597,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onTertiary, defaultValue = selectedDefaultScheme.onTertiary,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Tertiary Container", title = "Tertiary Container",
@ -633,7 +620,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.tertiaryContainer, defaultValue = selectedDefaultScheme.tertiaryContainer,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Tertiary Container", title = "On Tertiary Container",
@ -657,14 +643,17 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onTertiaryContainer, defaultValue = selectedDefaultScheme.onTertiaryContainer,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
) { ) {
ShapedLauncherIcon(
badge = { Badge() },
size = 48.dp,
)
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Surface colors", title = "Surface colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -716,7 +705,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surface, defaultValue = selectedDefaultScheme.surface,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface Bright", title = "Surface Bright",
@ -740,7 +728,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceBright, defaultValue = selectedDefaultScheme.surfaceBright,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface Tint", title = "Surface Tint",
@ -764,7 +751,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceTint, defaultValue = selectedDefaultScheme.surfaceTint,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
) { ) {
@ -824,7 +810,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Surface container colors", title = "Surface container colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -852,7 +838,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceContainerLowest, defaultValue = selectedDefaultScheme.surfaceContainerLowest,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface Container Low", title = "Surface Container Low",
@ -876,7 +861,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceContainerLow, defaultValue = selectedDefaultScheme.surfaceContainerLow,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface Container", title = "Surface Container",
@ -900,7 +884,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceContainer, defaultValue = selectedDefaultScheme.surfaceContainer,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface Container High", title = "Surface Container High",
@ -924,7 +907,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceContainerHigh, defaultValue = selectedDefaultScheme.surfaceContainerHigh,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface Container Highest", title = "Surface Container Highest",
@ -948,7 +930,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceContainerHighest, defaultValue = selectedDefaultScheme.surfaceContainerHighest,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface Variant", title = "Surface Variant",
@ -972,7 +953,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceVariant, defaultValue = selectedDefaultScheme.surfaceVariant,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
) { ) {
@ -994,7 +974,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Content colors", title = "Content colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -1022,7 +1002,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onSurface, defaultValue = selectedDefaultScheme.onSurface,
modifier = Modifier.padding(end = 12.dp),
) )
@ -1048,7 +1027,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onSurfaceVariant, defaultValue = selectedDefaultScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
@ -1073,7 +1051,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onSurfaceVariant, defaultValue = selectedDefaultScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
) { ) {
@ -1126,7 +1103,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
Surface( Surface(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.extraSmall, shape = MaterialTheme.shapes.extraSmall,
tonalElevation = 3.dp, tonalElevation = 3.dp,
shadowElevation = 3.dp, shadowElevation = 3.dp,
@ -1148,7 +1125,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Outline colors", title = "Outline colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -1176,7 +1153,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.outline, defaultValue = selectedDefaultScheme.outline,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Outline Variant", title = "Outline Variant",
@ -1200,7 +1176,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.outlineVariant, defaultValue = selectedDefaultScheme.outlineVariant,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
) { ) {
@ -1240,7 +1215,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Error colors", title = "Error colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -1268,7 +1243,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.error, defaultValue = selectedDefaultScheme.error,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Error", title = "On Error",
@ -1292,7 +1266,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onError, defaultValue = selectedDefaultScheme.onError,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Error Container", title = "Error Container",
@ -1316,7 +1289,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.errorContainer, defaultValue = selectedDefaultScheme.errorContainer,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "On Error Container", title = "On Error Container",
@ -1340,7 +1312,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.onErrorContainer, defaultValue = selectedDefaultScheme.onErrorContainer,
modifier = Modifier.padding(end = 12.dp),
) )
}, },
) { ) {
@ -1354,7 +1325,7 @@ fun ThemeSettingsScreen(themeId: UUID) {
} }
} }
item { item {
ThemePreferenceCategory( ColorSchemePreferenceCategory(
title = "Inverse colors", title = "Inverse colors",
previewColorScheme = previewColorScheme, previewColorScheme = previewColorScheme,
darkMode = previewDarkTheme, darkMode = previewDarkTheme,
@ -1382,7 +1353,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.inverseSurface, defaultValue = selectedDefaultScheme.inverseSurface,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Inverse Surface", title = "Inverse Surface",
@ -1406,7 +1376,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.inverseOnSurface, defaultValue = selectedDefaultScheme.inverseOnSurface,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Inverse Primary", title = "Inverse Primary",
@ -1430,7 +1399,6 @@ fun ThemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.inversePrimary, 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.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.themes.Theme import de.mm20.launcher2.themes.Colors
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.ImportThemeSheet import de.mm20.launcher2.ui.common.ImportThemeSheet
import de.mm20.launcher2.ui.component.preferences.Preference 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 import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
@Composable @Composable
fun ThemesSettingsScreen() { fun ColorSchemesSettingsScreen() {
val viewModel: ThemesSettingsScreenVM = viewModel() val viewModel: ColorSchemesSettingsScreenVM = viewModel()
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
val selectedTheme by viewModel.selectedTheme.collectAsStateWithLifecycle(null) val selectedTheme by viewModel.selectedColors.collectAsStateWithLifecycle(null)
val themes by viewModel.themes.collectAsStateWithLifecycle(emptyList()) 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) } var importThemeUri by remember { mutableStateOf<Uri?>(null) }
@ -114,7 +114,7 @@ fun ThemesSettingsScreen() {
}, },
text = { Text(stringResource(R.string.edit)) }, text = { Text(stringResource(R.string.edit)) },
onClick = { onClick = {
navController?.navigate("settings/appearance/themes/${theme.id}") navController?.navigate("settings/appearance/colors/${theme.id}")
showMenu = false showMenu = false
} }
) )
@ -146,7 +146,7 @@ fun ThemesSettingsScreen() {
}, },
text = { Text(stringResource(R.string.menu_delete)) }, text = { Text(stringResource(R.string.menu_delete)) },
onClick = { onClick = {
deleteTheme = theme deleteColors = theme
showMenu = false showMenu = false
} }
) )
@ -162,22 +162,22 @@ fun ThemesSettingsScreen() {
} }
} }
} }
if (deleteTheme != null) { if (deleteColors != null) {
AlertDialog( AlertDialog(
onDismissRequest = { deleteTheme = null }, onDismissRequest = { deleteColors = null },
text = { text = {
Text( Text(
stringResource( stringResource(
R.string.confirmation_delete_color_scheme, R.string.confirmation_delete_color_scheme,
deleteTheme!!.name deleteColors!!.name
) )
) )
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.delete(deleteTheme!!) viewModel.delete(deleteColors!!)
deleteTheme = null deleteColors = null
} }
) { ) {
Text(stringResource(android.R.string.ok)) Text(stringResource(android.R.string.ok))
@ -185,7 +185,7 @@ fun ThemesSettingsScreen() {
}, },
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { deleteTheme = null } onClick = { deleteColors = null }
) { ) {
Text(stringResource(android.R.string.cancel)) Text(stringResource(android.R.string.cancel))
} }
@ -199,9 +199,9 @@ fun ThemesSettingsScreen() {
} }
@Composable @Composable
fun ColorSchemePreview(theme: Theme) { fun ColorSchemePreview(colors: Colors) {
val dark = LocalDarkTheme.current val dark = LocalDarkTheme.current
val scheme = if (dark) darkColorSchemeOf(theme) else lightColorSchemeOf(theme) val scheme = if (dark) darkColorSchemeOf(colors) else lightColorSchemeOf(colors)
Box( Box(
modifier = Modifier modifier = Modifier
.height(28.dp) .height(28.dp)

View File

@ -6,13 +6,13 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ktx.tryStartActivity 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.preferences.ui.UiSettings
import de.mm20.launcher2.themes.BlackAndWhiteThemeId import de.mm20.launcher2.themes.BlackAndWhiteThemeId
import de.mm20.launcher2.themes.DefaultThemeId 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.ThemeRepository
import de.mm20.launcher2.themes.toJson import de.mm20.launcher2.themes.toLegacyJson
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -24,49 +24,49 @@ import org.koin.core.component.inject
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
class ThemesSettingsScreenVM : ViewModel(), KoinComponent { class ColorSchemesSettingsScreenVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject() private val themeRepository: ThemeRepository by inject()
private val uiSettings: UiSettings by inject() private val uiSettings: UiSettings by inject()
val selectedTheme = uiSettings.theme.map { val selectedColors = uiSettings.colors.map {
when(it) { when(it) {
ThemeDescriptor.Default -> DefaultThemeId ColorsDescriptor.Default -> DefaultThemeId
ThemeDescriptor.BlackAndWhite -> BlackAndWhiteThemeId ColorsDescriptor.BlackAndWhite -> BlackAndWhiteThemeId
is ThemeDescriptor.Custom -> UUID.fromString(it.id) 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?> { fun getTheme(id: UUID): Flow<Colors?> {
return themeRepository.getTheme(id) return themeRepository.getColors(id)
} }
fun updateTheme(theme: Theme) { fun updateTheme(colors: Colors) {
themeRepository.updateTheme(theme) themeRepository.updateColors(colors)
} }
fun selectTheme(theme: Theme) { fun selectTheme(colors: Colors) {
uiSettings.setTheme(when(theme.id) { uiSettings.setColors(when(colors.id) {
DefaultThemeId -> ThemeDescriptor.Default DefaultThemeId -> ColorsDescriptor.Default
BlackAndWhiteThemeId -> ThemeDescriptor.BlackAndWhite BlackAndWhiteThemeId -> ColorsDescriptor.BlackAndWhite
else -> ThemeDescriptor.Custom(theme.id.toString()) else -> ColorsDescriptor.Custom(colors.id.toString())
}) })
} }
fun duplicate(theme: Theme) { fun duplicate(colors: Colors) {
themeRepository.createTheme(theme.copy(id = UUID.randomUUID())) themeRepository.createColors(colors.copy(id = UUID.randomUUID()))
} }
fun delete(theme: Theme) { fun delete(colors: Colors) {
themeRepository.deleteTheme(theme) themeRepository.deleteColors(colors)
} }
fun exportTheme(context: Context, theme: Theme) { fun exportTheme(context: Context, colors: Colors) {
viewModelScope.launch { viewModelScope.launch {
val file = withContext(Dispatchers.IO) { val file = withContext(Dispatchers.IO) {
val file = File(context.cacheDir, "${theme.name}.kvtheme") val file = File(context.cacheDir, "${colors.name}.kvtheme")
file.writeText(theme.toJson()) file.writeText(colors.toLegacyJson())
file file
} }
context.tryStartActivity(Intent().apply { context.tryStartActivity(Intent().apply {
@ -82,8 +82,8 @@ class ThemesSettingsScreenVM : ViewModel(), KoinComponent {
} }
fun createNew(context: Context) { fun createNew(context: Context) {
themeRepository.createTheme( themeRepository.createColors(
Theme( Colors(
id = UUID.randomUUID(), id = UUID.randomUUID(),
name = context.getString(R.string.new_color_scheme_name) 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.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.ButtonDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp
import de.mm20.launcher2.themes.get
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.Tooltip import de.mm20.launcher2.ui.component.Tooltip
@ -47,19 +54,25 @@ fun CorePaletteColorPreference(
) { ) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
Tooltip( Row(
tooltipText = title modifier = modifier.fillMaxWidth()
.clickable(
onClick = { showDialog = true },
)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
ColorSwatch( ColorSwatch(
color = Color(value ?: defaultValue), color = Color(value ?: defaultValue),
modifier = modifier modifier = Modifier.padding(end = 20.dp).size(48.dp),
.size(48.dp) )
.combinedClickable(
onClick = { showDialog = true }, Text(
onLongClick = { title,
onValueChange(null) 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.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState 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.drawscope.Fill
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.themes.ColorRef import de.mm20.launcher2.themes.ColorRef
import de.mm20.launcher2.themes.CorePaletteColor import de.mm20.launcher2.themes.CorePaletteColor
@ -66,7 +70,7 @@ import de.mm20.launcher2.themes.Color as ThemeColor
@Composable @Composable
fun ThemeColorPreference( fun ThemeColorPreference(
title: String, title: String,
value: de.mm20.launcher2.themes.Color?, value: ThemeColor?,
corePalette: FullCorePalette, corePalette: FullCorePalette,
onValueChange: (ThemeColor?) -> Unit, onValueChange: (ThemeColor?) -> Unit,
defaultValue: ThemeColor, defaultValue: ThemeColor,
@ -74,16 +78,25 @@ fun ThemeColorPreference(
) { ) {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
Tooltip( Row(
tooltipText = title modifier = modifier.fillMaxWidth()
.clickable(
onClick = { showDialog = true },
)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
ColorSwatch( ColorSwatch(
color = Color((value ?: defaultValue).get(corePalette)), color = Color((value ?: defaultValue).get(corePalette)),
modifier = modifier modifier = Modifier.padding(end = 20.dp).size(48.dp),
.size(48.dp) )
.clickable(
onClick = { showDialog = true }, 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.themes.ThemeRepository
import de.mm20.launcher2.ui.locals.LocalDarkTheme import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.theme.colorscheme.* 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.DefaultTypography
import de.mm20.launcher2.ui.theme.typography.getDeviceDefaultTypography import de.mm20.launcher2.ui.theme.typography.getDeviceDefaultTypography
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@ -33,11 +34,17 @@ fun LauncherTheme(
val uiSettings: UiSettings = koinInject() val uiSettings: UiSettings = koinInject()
val themeRepository: ThemeRepository = koinInject() val themeRepository: ThemeRepository = koinInject()
val theme by remember { val themeColors by remember {
uiSettings.theme.flatMapLatest { uiSettings.colors.flatMapLatest {
themeRepository.getThemeOrDefault(it) 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( val colorSchemePref by remember { uiSettings.colorScheme }.collectAsState(
ColorSchemePref.System ColorSchemePref.System
@ -45,27 +52,20 @@ fun LauncherTheme(
val darkTheme = val darkTheme =
colorSchemePref == ColorSchemePref.Dark || colorSchemePref == ColorSchemePref.System && isSystemInDarkTheme() colorSchemePref == ColorSchemePref.Dark || colorSchemePref == ColorSchemePref.System && isSystemInDarkTheme()
val cornerRadius by remember { if (themeColors == null || themeShapes == null) {
uiSettings.cardStyle.map { return
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))
val colorScheme = if (darkTheme) { val colorScheme = if (darkTheme) {
darkColorSchemeOf(theme) darkColorSchemeOf(themeColors!!)
} else { } else {
lightColorSchemeOf(theme) lightColorSchemeOf(themeColors!!)
} }
val shapes = shapesOf(themeShapes!!)
val font by remember { uiSettings.font }.collectAsState( val font by remember { uiSettings.font }.collectAsState(
Font.Outfit Font.Outfit
) )
@ -80,13 +80,7 @@ fun LauncherTheme(
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = typography, typography = typography,
shapes = Shapes( 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))),
),
content = content 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.DefaultLightColorScheme
import de.mm20.launcher2.themes.FullColorScheme import de.mm20.launcher2.themes.FullColorScheme
import de.mm20.launcher2.themes.PartialCorePalette 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.get
import de.mm20.launcher2.themes.merge import de.mm20.launcher2.themes.merge
import de.mm20.launcher2.ui.locals.LocalWallpaperColors import de.mm20.launcher2.ui.locals.LocalWallpaperColors
import org.koin.compose.koinInject import org.koin.compose.koinInject
@Composable @Composable
fun lightColorSchemeOf(theme: Theme): ColorScheme { fun lightColorSchemeOf(colors: ThemeColors): ColorScheme {
return colorSchemeOf(theme.lightColorScheme.merge(DefaultLightColorScheme), theme.corePalette) return colorSchemeOf(colors.lightColorScheme.merge(DefaultLightColorScheme), colors.corePalette)
} }
@Composable @Composable
fun darkColorSchemeOf(theme: Theme): ColorScheme { fun darkColorSchemeOf(colors: ThemeColors): ColorScheme {
return colorSchemeOf(theme.darkColorScheme.merge(DefaultDarkColorScheme), theme.corePalette) return colorSchemeOf(colors.darkColorScheme.merge(DefaultDarkColorScheme), colors.corePalette)
} }
@Composable @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

@ -1741,3 +1741,43 @@ private val _BreezyWeather = materialIcon("Icons.Rounded.BreezyWeather") {
val 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">Source for dynamic colors</string>
<string name="preference_mdy_color_source_system">System</string> <string name="preference_mdy_color_source_system">System</string>
<string name="preference_mdy_color_source_wallpaper">Wallpaper</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">Font</string>
<string name="preference_font_system">System default</string> <string name="preference_font_system">System default</string>
<string name="preference_screen_about">About</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">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="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_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_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_system_default">Use system default</string>
<string name="theme_color_scheme_autogenerate">From primary color</string> <string name="theme_color_scheme_autogenerate">From primary color</string>
<string name="theme_color_scheme_palette_color">Palette</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 schemaVersion: Int = 5,
val uiColorScheme: ColorScheme = ColorScheme.System, 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 uiCompatModeColors: Boolean = false,
val uiFont: Font = Font.Outfit, val uiFont: Font = Font.Outfit,
@Deprecated("No longer in use, only used for migration") @Deprecated("No longer in use, only used for migration")
@ -121,8 +124,10 @@ data class LauncherSettingsData internal constructor(
val systemBarsNavColors: SystemBarColors = SystemBarColors.Auto, val systemBarsNavColors: SystemBarColors = SystemBarColors.Auto,
val surfacesOpacity: Float = 1f, val surfacesOpacity: Float = 1f,
@Deprecated("Replaces with shape schemes")
val surfacesRadius: Int = 24, val surfacesRadius: Int = 24,
val surfacesBorderWidth: Int = 0, val surfacesBorderWidth: Int = 0,
@Deprecated("Replaces with shape schemes")
val surfacesShape: SurfaceShape = SurfaceShape.Rounded, val surfacesShape: SurfaceShape = SurfaceShape.Rounded,
val widgetsEditButton: Boolean = true, val widgetsEditButton: Boolean = true,
@ -201,20 +206,45 @@ enum class Font {
@Serializable @Serializable
sealed interface ThemeDescriptor { sealed interface ColorsDescriptor {
@Serializable @Serializable
@SerialName("default") @SerialName("default")
data object Default : ThemeDescriptor data object Default : ColorsDescriptor
@Serializable @Serializable
@SerialName("bw") @SerialName("bw")
data object BlackAndWhite : ThemeDescriptor data object BlackAndWhite : ColorsDescriptor
@Serializable @Serializable
@SerialName("custom") @SerialName("custom")
data class Custom( data class Custom(
val id: String, 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 { 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.ScreenOrientation
import de.mm20.launcher2.preferences.SearchBarColors import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.SurfaceShape
import de.mm20.launcher2.preferences.SystemBarColors 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.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
data class CardStyle( data class CardStyle(
val opacity: Float = 1f, val opacity: Float = 1f,
val cornerRadius: Int = 0,
val shape: SurfaceShape = SurfaceShape.Rounded,
val borderWidth: Int = 0, val borderWidth: Int = 0,
) )
@ -90,8 +88,6 @@ class UiSettings internal constructor(
get() = launcherDataStore.data.map { get() = launcherDataStore.data.map {
CardStyle( CardStyle(
opacity = it.surfacesOpacity, opacity = it.surfacesOpacity,
cornerRadius = it.surfacesRadius,
shape = it.surfacesShape,
borderWidth = it.surfacesBorderWidth, 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) { fun setCardBorderWidth(borderWidth: Int) {
launcherDataStore.update { launcherDataStore.update {
it.copy(surfacesBorderWidth = borderWidth) it.copy(surfacesBorderWidth = borderWidth)
} }
} }
fun setCardShape(shape: SurfaceShape) {
launcherDataStore.update {
it.copy(surfacesShape = shape)
}
}
val dimWallpaper val dimWallpaper
get() = launcherDataStore.data.map { get() = launcherDataStore.data.map {
it.wallpaperDim it.wallpaperDim
@ -302,14 +286,25 @@ class UiSettings internal constructor(
} }
val theme val colors
get() = launcherDataStore.data.map { get() = launcherDataStore.data.map {
it.uiTheme it.uiColors
}.distinctUntilChanged() }.distinctUntilChanged()
fun setTheme(theme: ThemeDescriptor) { fun setColors(colors: ColorsDescriptor) {
launcherDataStore.update { 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.PluginEntity
import de.mm20.launcher2.database.entities.SavedSearchableEntity import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.database.entities.SearchActionEntity 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.entities.WidgetEntity
import de.mm20.launcher2.database.migrations.Migration_10_11 import de.mm20.launcher2.database.migrations.Migration_10_11
import de.mm20.launcher2.database.migrations.Migration_11_12 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_24_25
import de.mm20.launcher2.database.migrations.Migration_25_26 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_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_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8 import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9 import de.mm20.launcher2.database.migrations.Migration_8_9
@ -54,9 +56,10 @@ import java.util.UUID
WidgetEntity::class, WidgetEntity::class,
CustomAttributeEntity::class, CustomAttributeEntity::class,
SearchActionEntity::class, SearchActionEntity::class,
ThemeEntity::class, ColorsEntity::class,
PluginEntity::class, PluginEntity::class,
], version = 27, exportSchema = true ShapesEntity::class,
], version = 28, exportSchema = true
) )
@TypeConverters(ComponentNameConverter::class) @TypeConverters(ComponentNameConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -156,6 +159,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_24_25(), Migration_24_25(),
Migration_25_26(), Migration_25_26(),
Migration_26_27(), Migration_26_27(),
Migration_27_28(),
).build() ).build()
if (_instance == null) _instance = instance if (_instance == null) _instance = instance
return instance return instance

View File

@ -4,30 +4,52 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Update 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 kotlinx.coroutines.flow.Flow
import java.util.UUID import java.util.UUID
@Dao @Dao
interface ThemeDao { interface ThemeDao {
@Query("SELECT * FROM Theme") @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") @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 @Insert
suspend fun insert(theme: ThemeEntity) suspend fun insertColors(colors: ColorsEntity)
@Insert
suspend fun insertShapes(shapes: ShapesEntity)
@Update @Update
suspend fun update(theme: ThemeEntity) suspend fun updateColors(colors: ColorsEntity)
@Update
suspend fun updateShapes(shapes: ShapesEntity)
@Query("DELETE FROM Theme WHERE id = :id") @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") @Query("DELETE FROM Theme")
suspend fun deleteAll() suspend fun deleteAllColors()
@Query("DELETE FROM Shapes")
suspend fun deleteAllShapes()
@Insert @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 import java.util.UUID
@Entity(tableName = "Theme") @Entity(tableName = "Theme")
data class ThemeEntity( data class ColorsEntity(
@PrimaryKey val id: UUID, @PrimaryKey val id: UUID,
val name: String, 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 package de.mm20.launcher2.themes
import de.mm20.launcher2.database.entities.ThemeEntity import de.mm20.launcher2.database.entities.ColorsEntity
import hct.Hct import hct.Hct
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import java.util.UUID 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 @Serializable
data class CorePalette<out T : Int?>( data class Colors(
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(
@Transient val id: UUID = UUID.randomUUID(), @Transient val id: UUID = UUID.randomUUID(),
val builtIn: Boolean = false, val builtIn: Boolean = false,
val name: String, val name: String,
@ -141,7 +16,7 @@ data class Theme(
val darkColorScheme: PartialColorScheme = DefaultDarkColorScheme, val darkColorScheme: PartialColorScheme = DefaultDarkColorScheme,
) { ) {
constructor(entity: ThemeEntity) : this( constructor(entity: ColorsEntity) : this(
id = entity.id, id = entity.id,
builtIn = false, builtIn = false,
name = entity.name, name = entity.name,
@ -154,86 +29,86 @@ data class Theme(
error = entity.corePaletteE, error = entity.corePaletteE,
), ),
lightColorScheme = ColorScheme( lightColorScheme = ColorScheme(
primary = Color(entity.lightPrimary), primary = Color.fromString(entity.lightPrimary),
onPrimary = Color(entity.lightOnPrimary), onPrimary = Color.fromString(entity.lightOnPrimary),
primaryContainer = Color(entity.lightPrimaryContainer), primaryContainer = Color.fromString(entity.lightPrimaryContainer),
onPrimaryContainer = Color(entity.lightOnPrimaryContainer), onPrimaryContainer = Color.fromString(entity.lightOnPrimaryContainer),
secondary = Color(entity.lightSecondary), secondary = Color.fromString(entity.lightSecondary),
onSecondary = Color(entity.lightOnSecondary), onSecondary = Color.fromString(entity.lightOnSecondary),
secondaryContainer = Color(entity.lightSecondaryContainer), secondaryContainer = Color.fromString(entity.lightSecondaryContainer),
onSecondaryContainer = Color(entity.lightOnSecondaryContainer), onSecondaryContainer = Color.fromString(entity.lightOnSecondaryContainer),
tertiary = Color(entity.lightTertiary), tertiary = Color.fromString(entity.lightTertiary),
onTertiary = Color(entity.lightOnTertiary), onTertiary = Color.fromString(entity.lightOnTertiary),
tertiaryContainer = Color(entity.lightTertiaryContainer), tertiaryContainer = Color.fromString(entity.lightTertiaryContainer),
onTertiaryContainer = Color(entity.lightOnTertiaryContainer), onTertiaryContainer = Color.fromString(entity.lightOnTertiaryContainer),
error = Color(entity.lightError), error = Color.fromString(entity.lightError),
onError = Color(entity.lightOnError), onError = Color.fromString(entity.lightOnError),
errorContainer = Color(entity.lightErrorContainer), errorContainer = Color.fromString(entity.lightErrorContainer),
onErrorContainer = Color(entity.lightOnErrorContainer), onErrorContainer = Color.fromString(entity.lightOnErrorContainer),
surface = Color(entity.lightSurface), surface = Color.fromString(entity.lightSurface),
onSurface = Color(entity.lightOnSurface), onSurface = Color.fromString(entity.lightOnSurface),
onSurfaceVariant = Color(entity.lightOnSurfaceVariant), onSurfaceVariant = Color.fromString(entity.lightOnSurfaceVariant),
outline = Color(entity.lightOutline), outline = Color.fromString(entity.lightOutline),
outlineVariant = Color(entity.lightOutlineVariant), outlineVariant = Color.fromString(entity.lightOutlineVariant),
inverseSurface = Color(entity.lightInverseSurface), inverseSurface = Color.fromString(entity.lightInverseSurface),
inverseOnSurface = Color(entity.lightInverseOnSurface), inverseOnSurface = Color.fromString(entity.lightInverseOnSurface),
inversePrimary = Color(entity.lightInversePrimary), inversePrimary = Color.fromString(entity.lightInversePrimary),
surfaceDim = Color(entity.lightSurfaceDim), surfaceDim = Color.fromString(entity.lightSurfaceDim),
surfaceBright = Color(entity.lightSurfaceBright), surfaceBright = Color.fromString(entity.lightSurfaceBright),
surfaceContainerLowest = Color(entity.lightSurfaceContainerLowest), surfaceContainerLowest = Color.fromString(entity.lightSurfaceContainerLowest),
surfaceContainerLow = Color(entity.lightSurfaceContainerLow), surfaceContainerLow = Color.fromString(entity.lightSurfaceContainerLow),
surfaceContainer = Color(entity.lightSurfaceContainer), surfaceContainer = Color.fromString(entity.lightSurfaceContainer),
surfaceContainerHigh = Color(entity.lightSurfaceContainerHigh), surfaceContainerHigh = Color.fromString(entity.lightSurfaceContainerHigh),
surfaceContainerHighest = Color(entity.lightSurfaceContainerHighest), surfaceContainerHighest = Color.fromString(entity.lightSurfaceContainerHighest),
background = Color(entity.lightBackground), background = Color.fromString(entity.lightBackground),
onBackground = Color(entity.lightOnBackground), onBackground = Color.fromString(entity.lightOnBackground),
surfaceTint = Color(entity.lightSurfaceTint), surfaceTint = Color.fromString(entity.lightSurfaceTint),
scrim = Color(entity.lightScrim), scrim = Color.fromString(entity.lightScrim),
surfaceVariant = Color(entity.lightSurfaceVariant), surfaceVariant = Color.fromString(entity.lightSurfaceVariant),
), ),
darkColorScheme = ColorScheme( darkColorScheme = ColorScheme(
primary = Color(entity.darkPrimary), primary = Color.fromString(entity.darkPrimary),
onPrimary = Color(entity.darkOnPrimary), onPrimary = Color.fromString(entity.darkOnPrimary),
primaryContainer = Color(entity.darkPrimaryContainer), primaryContainer = Color.fromString(entity.darkPrimaryContainer),
onPrimaryContainer = Color(entity.darkOnPrimaryContainer), onPrimaryContainer = Color.fromString(entity.darkOnPrimaryContainer),
secondary = Color(entity.darkSecondary), secondary = Color.fromString(entity.darkSecondary),
onSecondary = Color(entity.darkOnSecondary), onSecondary = Color.fromString(entity.darkOnSecondary),
secondaryContainer = Color(entity.darkSecondaryContainer), secondaryContainer = Color.fromString(entity.darkSecondaryContainer),
onSecondaryContainer = Color(entity.darkOnSecondaryContainer), onSecondaryContainer = Color.fromString(entity.darkOnSecondaryContainer),
tertiary = Color(entity.darkTertiary), tertiary = Color.fromString(entity.darkTertiary),
onTertiary = Color(entity.darkOnTertiary), onTertiary = Color.fromString(entity.darkOnTertiary),
tertiaryContainer = Color(entity.darkTertiaryContainer), tertiaryContainer = Color.fromString(entity.darkTertiaryContainer),
onTertiaryContainer = Color(entity.darkOnTertiaryContainer), onTertiaryContainer = Color.fromString(entity.darkOnTertiaryContainer),
error = Color(entity.darkError), error = Color.fromString(entity.darkError),
onError = Color(entity.darkOnError), onError = Color.fromString(entity.darkOnError),
errorContainer = Color(entity.darkErrorContainer), errorContainer = Color.fromString(entity.darkErrorContainer),
onErrorContainer = Color(entity.darkOnErrorContainer), onErrorContainer = Color.fromString(entity.darkOnErrorContainer),
surface = Color(entity.darkSurface), surface = Color.fromString(entity.darkSurface),
onSurface = Color(entity.darkOnSurface), onSurface = Color.fromString(entity.darkOnSurface),
onSurfaceVariant = Color(entity.darkOnSurfaceVariant), onSurfaceVariant = Color.fromString(entity.darkOnSurfaceVariant),
outline = Color(entity.darkOutline), outline = Color.fromString(entity.darkOutline),
outlineVariant = Color(entity.darkOutlineVariant), outlineVariant = Color.fromString(entity.darkOutlineVariant),
inverseSurface = Color(entity.darkInverseSurface), inverseSurface = Color.fromString(entity.darkInverseSurface),
inverseOnSurface = Color(entity.darkInverseOnSurface), inverseOnSurface = Color.fromString(entity.darkInverseOnSurface),
inversePrimary = Color(entity.darkInversePrimary), inversePrimary = Color.fromString(entity.darkInversePrimary),
surfaceDim = Color(entity.darkSurfaceDim), surfaceDim = Color.fromString(entity.darkSurfaceDim),
surfaceBright = Color(entity.darkSurfaceBright), surfaceBright = Color.fromString(entity.darkSurfaceBright),
surfaceContainerLowest = Color(entity.darkSurfaceContainerLowest), surfaceContainerLowest = Color.fromString(entity.darkSurfaceContainerLowest),
surfaceContainerLow = Color(entity.darkSurfaceContainerLow), surfaceContainerLow = Color.fromString(entity.darkSurfaceContainerLow),
surfaceContainer = Color(entity.darkSurfaceContainer), surfaceContainer = Color.fromString(entity.darkSurfaceContainer),
surfaceContainerHigh = Color(entity.darkSurfaceContainerHigh), surfaceContainerHigh = Color.fromString(entity.darkSurfaceContainerHigh),
surfaceContainerHighest = Color(entity.darkSurfaceContainerHighest), surfaceContainerHighest = Color.fromString(entity.darkSurfaceContainerHighest),
background = Color(entity.darkBackground), background = Color.fromString(entity.darkBackground),
onBackground = Color(entity.darkOnBackground), onBackground = Color.fromString(entity.darkOnBackground),
surfaceTint = Color(entity.darkSurfaceTint), surfaceTint = Color.fromString(entity.darkSurfaceTint),
scrim = Color(entity.darkScrim), scrim = Color.fromString(entity.darkScrim),
surfaceVariant = Color(entity.darkSurfaceVariant), surfaceVariant = Color.fromString(entity.darkSurfaceVariant),
), ),
) )
internal fun toEntity(): ThemeEntity { internal fun toEntity(): ColorsEntity {
return ThemeEntity( return ColorsEntity(
id = id, id = id,
name = name, name = name,
corePaletteA1 = corePalette.primary, 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 { fun <T : Int?> CorePalette<T>.get(color: CorePaletteColor): T {
return when (color) { return when (color) {
CorePaletteColor.Primary -> primary CorePaletteColor.Primary -> primary

View File

@ -4,6 +4,10 @@ import java.util.UUID
val DefaultThemeId = UUID(0L, 0L) 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>( val DefaultLightColorScheme = ColorScheme<Color>(
primary = ColorRef(CorePaletteColor.Primary, 40), primary = ColorRef(CorePaletteColor.Primary, 40),
@ -83,7 +87,6 @@ val DefaultDarkColorScheme = ColorScheme<Color>(
scrim = ColorRef(CorePaletteColor.Neutral, 0), scrim = ColorRef(CorePaletteColor.Neutral, 0),
) )
val BlackAndWhiteThemeId = UUID(0L, 1L)
val BlackAndWhiteLightColorScheme = ColorScheme<Color?>( val BlackAndWhiteLightColorScheme = ColorScheme<Color?>(
primary = StaticColor(0xFF000000.toInt()), 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 package de.mm20.launcher2.themes
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder 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> { internal class ColorSerializer: KSerializer<Color> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("$", PrimitiveKind.STRING) override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): ColorRef { override fun serialize(
return Color(decoder.decodeString()) as ColorRef encoder: Encoder,
} value: Color
) {
override fun serialize(encoder: Encoder, value: ColorRef) {
encoder.encodeString(value.toString()) encoder.encodeString(value.toString())
} }
override fun deserialize(decoder: Decoder): Color {
TODO("Not yet implemented")
} }
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 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()) encoder.encodeString(value.toString())
} }
}
internal val module = SerializersModule { override fun deserialize(decoder: Decoder): Shape {
polymorphic(Color::class) { return Shape.fromString(decoder.decodeString())!!
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)
} }
} }

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.backup.Backupable
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -16,7 +17,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
@ -26,50 +26,50 @@ class ThemeRepository(
) : Backupable { ) : Backupable {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
fun getThemes(): Flow<List<Theme>> { fun getAllColors(): Flow<List<Colors>> {
return database.themeDao().getAll().map { return database.themeDao().getAllColors().map {
getBuiltInThemes() + it.map { Theme(it) } getBuiltInColors() + it.map { Colors(it) }
} }
} }
fun getTheme(id: UUID): Flow<Theme?> { fun getColors(id: UUID): Flow<Colors?> {
if (id == DefaultThemeId) return flowOf(getDefaultTheme()) if (id == DefaultThemeId) return flowOf(getDefaultColors())
if (id == BlackAndWhiteThemeId) return flowOf(getBlackAndWhiteTheme()) if (id == BlackAndWhiteThemeId) return flowOf(getBlackAndWhiteColors())
return database.themeDao().get(id).map { it?.let { Theme(it) } }.flowOn(Dispatchers.Default) return database.themeDao().getColors(id).map { it?.let { Colors(it) } }.flowOn(Dispatchers.Default)
} }
fun createTheme(theme: Theme) { fun createColors(colors: Colors) {
scope.launch { scope.launch {
database.themeDao().insert(theme.toEntity()) database.themeDao().insertColors(colors.toEntity())
} }
} }
fun updateTheme(theme: Theme) { fun updateColors(colors: Colors) {
scope.launch { 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) { return when(theme) {
is ThemeDescriptor.BlackAndWhite -> flowOf(getBlackAndWhiteTheme()) is ColorsDescriptor.BlackAndWhite -> flowOf(getBlackAndWhiteColors())
is ThemeDescriptor.Custom -> { is ColorsDescriptor.Custom -> {
val id = UUID.fromString(theme.id) 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( return listOf(
getDefaultTheme(), getDefaultColors(),
getBlackAndWhiteTheme(), getBlackAndWhiteColors(),
) )
} }
fun getDefaultTheme(): Theme { private fun getDefaultColors(): Colors {
return Theme( return Colors(
id = DefaultThemeId, id = DefaultThemeId,
builtIn = true, builtIn = true,
name = context.getString(R.string.preference_colors_default), name = context.getString(R.string.preference_colors_default),
@ -79,8 +79,8 @@ class ThemeRepository(
) )
} }
private fun getBlackAndWhiteTheme(): Theme { private fun getBlackAndWhiteColors(): Colors {
return Theme( return Colors(
id = BlackAndWhiteThemeId, id = BlackAndWhiteThemeId,
builtIn = true, builtIn = true,
name = context.getString(R.string.preference_colors_bw), name = context.getString(R.string.preference_colors_bw),
@ -90,16 +90,127 @@ class ThemeRepository(
) )
} }
fun deleteTheme(theme: Theme) { fun deleteColors(colors: Colors) {
scope.launch { 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) { override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao() val dao = database.themeDao()
val themes = dao.getAll().first().map { Theme(it) } val colors = dao.getAllColors().first().map { Colors(it) }
val data = ThemeJson.encodeToString(themes) val data = LegacyThemeJson.encodeToString(colors)
val file = File(toDir, "themes.0000") val file = File(toDir, "themes.0000")
file.bufferedWriter().use { file.bufferedWriter().use {
@ -109,7 +220,7 @@ class ThemeRepository(
override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) { override suspend fun restore(fromDir: File) = withContext(Dispatchers.IO) {
val dao = database.themeDao() val dao = database.themeDao()
dao.deleteAll() dao.deleteAllColors()
val files = val files =
fromDir.listFiles { _, name -> name.startsWith("themes.") } fromDir.listFiles { _, name -> name.startsWith("themes.") }
@ -117,8 +228,8 @@ class ThemeRepository(
for (file in files) { for (file in files) {
val data = file.inputStream().reader().readText() val data = file.inputStream().reader().readText()
val themes: List<Theme> = try { val colors: List<Colors> = try {
ThemeJson.decodeFromString(data) LegacyThemeJson.decodeFromString(data)
} catch (e: SerializationException) { } catch (e: SerializationException) {
CrashReporter.logException(e) CrashReporter.logException(e)
continue continue
@ -126,7 +237,7 @@ class ThemeRepository(
CrashReporter.logException(e) CrashReporter.logException(e)
continue continue
} }
dao.insertAll(themes.map { it.toEntity() }) dao.insertAllColors(colors.map { it.toEntity() })
} }
} }