From 06f41158080a6f0e58ab7a87eabb7a2b3853fe6b Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 24 Feb 2026 18:31:22 +0100 Subject: [PATCH] feat(ui): Add theme mode, style, and color settings Add user-facing theme preferences with three independent axes: - Theme mode (System/Dark/Light) - Theme style (Default/Material You/Medium Contrast/High Contrast) - Theme color (Blue/Green/Amber) Includes safe Moshi enum fallback for corrupted preference values, color palettes for all combinations, and window background sync to prevent flash during navigation transitions. --- .../capod/common/compose/PreviewWrapper.kt | 2 +- .../FlowPreferenceMoshiExtension.kt | 27 +- .../settings/ThemeColorSelectorDialog.kt | 98 +++ .../capod/common/theming/CapodColorsAmber.kt | 173 ++++++ .../capod/common/theming/CapodColorsBlue.kt | 173 ++++++ .../capod/common/theming/CapodColorsGreen.kt | 173 ++++++ .../darken/capod/common/theming/CapodTheme.kt | 87 +-- .../darken/capod/common/theming/ThemeColor.kt | 15 + .../common/theming/ThemeColorProvider.kt | 560 ++++++++++++++++++ .../darken/capod/common/theming/ThemeMode.kt | 15 + .../darken/capod/common/theming/ThemeState.kt | 7 + .../darken/capod/common/theming/ThemeStyle.kt | 16 + .../darken/capod/main/core/GeneralSettings.kt | 33 +- .../eu/darken/capod/main/ui/MainActivity.kt | 19 +- .../settings/general/GeneralSettingsScreen.kt | 68 +++ .../general/GeneralSettingsViewModel.kt | 59 +- .../ui/widget/WidgetConfigurationActivity.kt | 6 +- .../capod/reaction/ui/popup/PopUpWindow.kt | 4 +- app/src/main/res/values/strings.xml | 23 + .../preferences/FlowPreferenceMoshiTest.kt | 65 ++ 20 files changed, 1530 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/eu/darken/capod/common/settings/ThemeColorSelectorDialog.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/CapodColorsAmber.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/CapodColorsBlue.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/CapodColorsGreen.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/ThemeColor.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/ThemeColorProvider.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/ThemeMode.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/ThemeState.kt create mode 100644 app/src/main/java/eu/darken/capod/common/theming/ThemeStyle.kt diff --git a/app/src/main/java/eu/darken/capod/common/compose/PreviewWrapper.kt b/app/src/main/java/eu/darken/capod/common/compose/PreviewWrapper.kt index 7e37df0f..1453ac88 100644 --- a/app/src/main/java/eu/darken/capod/common/compose/PreviewWrapper.kt +++ b/app/src/main/java/eu/darken/capod/common/compose/PreviewWrapper.kt @@ -7,7 +7,7 @@ import eu.darken.capod.common.theming.CapodTheme @Composable fun PreviewWrapper(content: @Composable () -> Unit) { - CapodTheme(dynamicColor = false) { + CapodTheme { Surface(color = MaterialTheme.colorScheme.background) { content() } diff --git a/app/src/main/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiExtension.kt b/app/src/main/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiExtension.kt index 2c2ac4a4..584b522d 100644 --- a/app/src/main/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiExtension.kt +++ b/app/src/main/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiExtension.kt @@ -1,16 +1,36 @@ package eu.darken.capod.common.preferences import android.content.SharedPreferences +import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.Moshi +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag inline fun moshiReader( moshi: Moshi, defaultValue: T, + onErrorFallbackToDefault: Boolean = false, ): (Any?) -> T { val adapter = moshi.adapter(T::class.java) return { rawValue -> rawValue as String? - rawValue?.let { adapter.fromJson(it) } ?: defaultValue + if (rawValue == null) { + defaultValue + } else if (onErrorFallbackToDefault) { + try { + adapter.fromJson(rawValue) ?: defaultValue + } catch (e: JsonDataException) { + log(logTag("FlowPreference"), WARN) { "Failed to decode, fallback to default: ${e.message}" } + defaultValue + } catch (e: JsonEncodingException) { + log(logTag("FlowPreference"), WARN) { "Failed to decode, fallback to default: ${e.message}" } + defaultValue + } + } else { + adapter.fromJson(rawValue) ?: defaultValue + } } } @@ -27,9 +47,10 @@ inline fun SharedPreferences.createFlowPreference( key: String, defaultValue: T = null as T, moshi: Moshi, + onErrorFallbackToDefault: Boolean = false, ) = FlowPreference( preferences = this, key = key, - rawReader = moshiReader(moshi, defaultValue), + rawReader = moshiReader(moshi, defaultValue, onErrorFallbackToDefault), rawWriter = moshiWriter(moshi) -) \ No newline at end of file +) diff --git a/app/src/main/java/eu/darken/capod/common/settings/ThemeColorSelectorDialog.kt b/app/src/main/java/eu/darken/capod/common/settings/ThemeColorSelectorDialog.kt new file mode 100644 index 00000000..3169b8ea --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/settings/ThemeColorSelectorDialog.kt @@ -0,0 +1,98 @@ +package eu.darken.capod.common.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import eu.darken.capod.common.theming.CapodColorsAmber +import eu.darken.capod.common.theming.CapodColorsBlue +import eu.darken.capod.common.theming.CapodColorsGreen +import eu.darken.capod.common.theming.ThemeColor + +@Composable +fun ThemeColorSelectorDialog( + selectedColor: ThemeColor, + onColorSelected: (ThemeColor) -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(eu.darken.capod.R.string.ui_theme_color_label)) }, + text = { + Column(Modifier.selectableGroup()) { + ThemeColor.entries.forEach { color -> + val isSelected = color == selectedColor + val (lightPreview, darkPreview) = colorPreviewPair(color) + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onColorSelected(color) }, + role = Role.RadioButton, + ) + .padding(vertical = 12.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = isSelected, + onClick = null, + ) + Text( + text = stringResource(color.labelRes), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .weight(1f) + .padding(start = 16.dp), + ) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background(lightPreview) + ) + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background(darkPreview) + ) + } + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + ) +} + +private fun colorPreviewPair(color: ThemeColor): Pair = when (color) { + ThemeColor.BLUE -> CapodColorsBlue.primaryLight to CapodColorsBlue.primaryDark + ThemeColor.GREEN -> CapodColorsGreen.primaryLight to CapodColorsGreen.primaryDark + ThemeColor.AMBER -> CapodColorsAmber.primaryLight to CapodColorsAmber.primaryDark +} diff --git a/app/src/main/java/eu/darken/capod/common/theming/CapodColorsAmber.kt b/app/src/main/java/eu/darken/capod/common/theming/CapodColorsAmber.kt new file mode 100644 index 00000000..1f25a08b --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/CapodColorsAmber.kt @@ -0,0 +1,173 @@ +package eu.darken.capod.common.theming + +import androidx.compose.ui.graphics.Color + +object CapodColorsAmber { + // Light (seed #E65100) + val primaryLight = Color(0xFF9D4300) + val onPrimaryLight = Color(0xFFFFFFFF) + val primaryContainerLight = Color(0xFFFFDBCA) + val onPrimaryContainerLight = Color(0xFF341100) + val secondaryLight = Color(0xFF765848) + val onSecondaryLight = Color(0xFFFFFFFF) + val secondaryContainerLight = Color(0xFFFFDBCA) + val onSecondaryContainerLight = Color(0xFF2C160A) + val tertiaryLight = Color(0xFF636032) + val onTertiaryLight = Color(0xFFFFFFFF) + val tertiaryContainerLight = Color(0xFFEAE5AB) + val onTertiaryContainerLight = Color(0xFF1E1C00) + val errorLight = Color(0xFFBA1B1B) + val onErrorLight = Color(0xFFFFFFFF) + val errorContainerLight = Color(0xFFFFDAD4) + val onErrorContainerLight = Color(0xFF410001) + val backgroundLight = Color(0xFFFFFBFF) + val onBackgroundLight = Color(0xFF201A17) + val surfaceLight = Color(0xFFFFFBFF) + val onSurfaceLight = Color(0xFF201A17) + val surfaceVariantLight = Color(0xFFF4DED4) + val onSurfaceVariantLight = Color(0xFF52443C) + val outlineLight = Color(0xFF85746B) + val inverseSurfaceLight = Color(0xFF362F2B) + val inverseOnSurfaceLight = Color(0xFFFBEEE8) + val inversePrimaryLight = Color(0xFFFFB68E) + + // Dark + val primaryDark = Color(0xFFFFB68E) + val onPrimaryDark = Color(0xFF552100) + val primaryContainerDark = Color(0xFF783200) + val onPrimaryContainerDark = Color(0xFFFFDBCA) + val secondaryDark = Color(0xFFE6BEAB) + val onSecondaryDark = Color(0xFF432B1D) + val secondaryContainerDark = Color(0xFF5C4132) + val onSecondaryContainerDark = Color(0xFFFFDBCA) + val tertiaryDark = Color(0xFFCEC991) + val onTertiaryDark = Color(0xFF343109) + val tertiaryContainerDark = Color(0xFF4B481D) + val onTertiaryContainerDark = Color(0xFFEAE5AB) + val errorDark = Color(0xFFFFB4A9) + val onErrorDark = Color(0xFF680003) + val errorContainerDark = Color(0xFF930006) + val onErrorContainerDark = Color(0xFFFFDAD4) + val backgroundDark = Color(0xFF201A17) + val onBackgroundDark = Color(0xFFECE0DA) + val surfaceDark = Color(0xFF201A17) + val onSurfaceDark = Color(0xFFECE0DA) + val surfaceVariantDark = Color(0xFF52443C) + val onSurfaceVariantDark = Color(0xFFD7C2B8) + val outlineDark = Color(0xFF9F8D84) + val inverseSurfaceDark = Color(0xFFECE0DA) + val inverseOnSurfaceDark = Color(0xFF201A17) + val inversePrimaryDark = Color(0xFF9D4300) + + // Light Medium Contrast + val primaryLightMediumContrast = Color(0xFF6E2D00) + val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) + val primaryContainerLightMediumContrast = Color(0xFFBB5918) + val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val secondaryLightMediumContrast = Color(0xFF583D2E) + val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) + val secondaryContainerLightMediumContrast = Color(0xFF8E6E5C) + val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val tertiaryLightMediumContrast = Color(0xFF474419) + val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) + val tertiaryContainerLightMediumContrast = Color(0xFF7A7646) + val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val errorLightMediumContrast = Color(0xFF8C0009) + val onErrorLightMediumContrast = Color(0xFFFFFFFF) + val errorContainerLightMediumContrast = Color(0xFFDA342E) + val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) + val backgroundLightMediumContrast = Color(0xFFFFFBFF) + val onBackgroundLightMediumContrast = Color(0xFF201A17) + val surfaceLightMediumContrast = Color(0xFFFFFBFF) + val onSurfaceLightMediumContrast = Color(0xFF201A17) + val surfaceVariantLightMediumContrast = Color(0xFFF4DED4) + val onSurfaceVariantLightMediumContrast = Color(0xFF4E4038) + val outlineLightMediumContrast = Color(0xFF6B5C54) + val inverseSurfaceLightMediumContrast = Color(0xFF362F2B) + val inverseOnSurfaceLightMediumContrast = Color(0xFFFBEEE8) + val inversePrimaryLightMediumContrast = Color(0xFFFFB68E) + + // Dark Medium Contrast + val primaryDarkMediumContrast = Color(0xFFFFBC98) + val onPrimaryDarkMediumContrast = Color(0xFF2B0D00) + val primaryContainerDarkMediumContrast = Color(0xFFC87B49) + val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) + val secondaryDarkMediumContrast = Color(0xFFEBC2AF) + val onSecondaryDarkMediumContrast = Color(0xFF261106) + val secondaryContainerDarkMediumContrast = Color(0xFFAC8978) + val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) + val tertiaryDarkMediumContrast = Color(0xFFD2CD95) + val onTertiaryDarkMediumContrast = Color(0xFF181600) + val tertiaryContainerDarkMediumContrast = Color(0xFF97935F) + val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) + val errorDarkMediumContrast = Color(0xFFFFBAB1) + val onErrorDarkMediumContrast = Color(0xFF370001) + val errorContainerDarkMediumContrast = Color(0xFFFF5449) + val onErrorContainerDarkMediumContrast = Color(0xFF000000) + val backgroundDarkMediumContrast = Color(0xFF201A17) + val onBackgroundDarkMediumContrast = Color(0xFFECE0DA) + val surfaceDarkMediumContrast = Color(0xFF201A17) + val onSurfaceDarkMediumContrast = Color(0xFFFFFAF8) + val surfaceVariantDarkMediumContrast = Color(0xFF52443C) + val onSurfaceVariantDarkMediumContrast = Color(0xFFDCC6BD) + val outlineDarkMediumContrast = Color(0xFFB29F96) + val inverseSurfaceDarkMediumContrast = Color(0xFFECE0DA) + val inverseOnSurfaceDarkMediumContrast = Color(0xFF201A17) + val inversePrimaryDarkMediumContrast = Color(0xFF7A3300) + + // Light High Contrast + val primaryLightHighContrast = Color(0xFF3D1600) + val onPrimaryLightHighContrast = Color(0xFFFFFFFF) + val primaryContainerLightHighContrast = Color(0xFF6E2D00) + val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) + val secondaryLightHighContrast = Color(0xFF331C0F) + val onSecondaryLightHighContrast = Color(0xFFFFFFFF) + val secondaryContainerLightHighContrast = Color(0xFF583D2E) + val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) + val tertiaryLightHighContrast = Color(0xFF252300) + val onTertiaryLightHighContrast = Color(0xFFFFFFFF) + val tertiaryContainerLightHighContrast = Color(0xFF474419) + val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) + val errorLightHighContrast = Color(0xFF4E0002) + val onErrorLightHighContrast = Color(0xFFFFFFFF) + val errorContainerLightHighContrast = Color(0xFF8C0009) + val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) + val backgroundLightHighContrast = Color(0xFFFFFBFF) + val onBackgroundLightHighContrast = Color(0xFF201A17) + val surfaceLightHighContrast = Color(0xFFFFFBFF) + val onSurfaceLightHighContrast = Color(0xFF000000) + val surfaceVariantLightHighContrast = Color(0xFFF4DED4) + val onSurfaceVariantLightHighContrast = Color(0xFF2D211B) + val outlineLightHighContrast = Color(0xFF4E4038) + val inverseSurfaceLightHighContrast = Color(0xFF362F2B) + val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) + val inversePrimaryLightHighContrast = Color(0xFFFFE7DA) + + // Dark High Contrast + val primaryDarkHighContrast = Color(0xFFFFFAF8) + val onPrimaryDarkHighContrast = Color(0xFF000000) + val primaryContainerDarkHighContrast = Color(0xFFFFB68E) + val onPrimaryContainerDarkHighContrast = Color(0xFF000000) + val secondaryDarkHighContrast = Color(0xFFFFFAF8) + val onSecondaryDarkHighContrast = Color(0xFF000000) + val secondaryContainerDarkHighContrast = Color(0xFFEBC2AF) + val onSecondaryContainerDarkHighContrast = Color(0xFF000000) + val tertiaryDarkHighContrast = Color(0xFFFFFBE6) + val onTertiaryDarkHighContrast = Color(0xFF000000) + val tertiaryContainerDarkHighContrast = Color(0xFFD2CD95) + val onTertiaryContainerDarkHighContrast = Color(0xFF000000) + val errorDarkHighContrast = Color(0xFFFFF9F9) + val onErrorDarkHighContrast = Color(0xFF000000) + val errorContainerDarkHighContrast = Color(0xFFFFBAB1) + val onErrorContainerDarkHighContrast = Color(0xFF000000) + val backgroundDarkHighContrast = Color(0xFF201A17) + val onBackgroundDarkHighContrast = Color(0xFFECE0DA) + val surfaceDarkHighContrast = Color(0xFF201A17) + val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) + val surfaceVariantDarkHighContrast = Color(0xFF52443C) + val onSurfaceVariantDarkHighContrast = Color(0xFFFFFAF8) + val outlineDarkHighContrast = Color(0xFFDCC6BD) + val inverseSurfaceDarkHighContrast = Color(0xFFECE0DA) + val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) + val inversePrimaryDarkHighContrast = Color(0xFF4C1D00) +} diff --git a/app/src/main/java/eu/darken/capod/common/theming/CapodColorsBlue.kt b/app/src/main/java/eu/darken/capod/common/theming/CapodColorsBlue.kt new file mode 100644 index 00000000..131c05b4 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/CapodColorsBlue.kt @@ -0,0 +1,173 @@ +package eu.darken.capod.common.theming + +import androidx.compose.ui.graphics.Color + +object CapodColorsBlue { + // Light + val primaryLight = Color(0xFF3F7AFF) + val onPrimaryLight = Color(0xFFFFFFFF) + val primaryContainerLight = Color(0xFFDAE2FF) + val onPrimaryContainerLight = Color(0xFF00174B) + val secondaryLight = Color(0xFF715C00) + val onSecondaryLight = Color(0xFFFFFFFF) + val secondaryContainerLight = Color(0xFFFFE16C) + val onSecondaryContainerLight = Color(0xFF231B00) + val tertiaryLight = Color(0xFF006D39) + val onTertiaryLight = Color(0xFFFFFFFF) + val tertiaryContainerLight = Color(0xFF5CFFA0) + val onTertiaryContainerLight = Color(0xFF00210D) + val errorLight = Color(0xFFBA1B1B) + val onErrorLight = Color(0xFFFFFFFF) + val errorContainerLight = Color(0xFFFFDAD4) + val onErrorContainerLight = Color(0xFF410001) + val backgroundLight = Color(0xFFFEFBFF) + val onBackgroundLight = Color(0xFF1B1B1F) + val surfaceLight = Color(0xFFFEFBFF) + val onSurfaceLight = Color(0xFF1B1B1F) + val surfaceVariantLight = Color(0xFFE2E2EC) + val onSurfaceVariantLight = Color(0xFF44464E) + val outlineLight = Color(0xFF75767F) + val inverseSurfaceLight = Color(0xFF303033) + val inverseOnSurfaceLight = Color(0xFFF2F0F5) + val inversePrimaryLight = Color(0xFFB1C5FF) + + // Dark + val primaryDark = Color(0xFF3F7AFF) + val onPrimaryDark = Color(0xFF002A78) + val primaryContainerDark = Color(0xFF003EA7) + val onPrimaryContainerDark = Color(0xFFDAE2FF) + val secondaryDark = Color(0xFFE9C426) + val onSecondaryDark = Color(0xFF3B2F00) + val secondaryContainerDark = Color(0xFF564500) + val onSecondaryContainerDark = Color(0xFFFFE16C) + val tertiaryDark = Color(0xFF33E286) + val onTertiaryDark = Color(0xFF00391B) + val tertiaryContainerDark = Color(0xFF005229) + val onTertiaryContainerDark = Color(0xFF5CFFA0) + val errorDark = Color(0xFFFFB4A9) + val onErrorDark = Color(0xFF680003) + val errorContainerDark = Color(0xFF930006) + val onErrorContainerDark = Color(0xFFFFDAD4) + val backgroundDark = Color(0xFF1B1B1F) + val onBackgroundDark = Color(0xFFE3E1E6) + val surfaceDark = Color(0xFF1B1B1F) + val onSurfaceDark = Color(0xFFE3E1E6) + val surfaceVariantDark = Color(0xFF44464E) + val onSurfaceVariantDark = Color(0xFFC6C6D0) + val outlineDark = Color(0xFF8F909A) + val inverseSurfaceDark = Color(0xFFE3E1E6) + val inverseOnSurfaceDark = Color(0xFF1B1B1F) + val inversePrimaryDark = Color(0xFF0054D9) + + // Light Medium Contrast + val primaryLightMediumContrast = Color(0xFF003DA3) + val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) + val primaryContainerLightMediumContrast = Color(0xFF5B8FFF) + val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val secondaryLightMediumContrast = Color(0xFF504100) + val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) + val secondaryContainerLightMediumContrast = Color(0xFF8A7320) + val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val tertiaryLightMediumContrast = Color(0xFF004F28) + val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) + val tertiaryContainerLightMediumContrast = Color(0xFF1F864D) + val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val errorLightMediumContrast = Color(0xFF8C0009) + val onErrorLightMediumContrast = Color(0xFFFFFFFF) + val errorContainerLightMediumContrast = Color(0xFFDA342E) + val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) + val backgroundLightMediumContrast = Color(0xFFFEFBFF) + val onBackgroundLightMediumContrast = Color(0xFF1B1B1F) + val surfaceLightMediumContrast = Color(0xFFFEFBFF) + val onSurfaceLightMediumContrast = Color(0xFF1B1B1F) + val surfaceVariantLightMediumContrast = Color(0xFFE2E2EC) + val onSurfaceVariantLightMediumContrast = Color(0xFF40424A) + val outlineLightMediumContrast = Color(0xFF5D5E67) + val inverseSurfaceLightMediumContrast = Color(0xFF303033) + val inverseOnSurfaceLightMediumContrast = Color(0xFFF2F0F5) + val inversePrimaryLightMediumContrast = Color(0xFFB1C5FF) + + // Dark Medium Contrast + val primaryDarkMediumContrast = Color(0xFF7FA3FF) + val onPrimaryDarkMediumContrast = Color(0xFF00133F) + val primaryContainerDarkMediumContrast = Color(0xFF3F7AFF) + val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) + val secondaryDarkMediumContrast = Color(0xFFEDC828) + val onSecondaryDarkMediumContrast = Color(0xFF1C1500) + val secondaryContainerDarkMediumContrast = Color(0xFFAE8F00) + val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) + val tertiaryDarkMediumContrast = Color(0xFF3FE78B) + val onTertiaryDarkMediumContrast = Color(0xFF001B09) + val tertiaryContainerDarkMediumContrast = Color(0xFF00AA55) + val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) + val errorDarkMediumContrast = Color(0xFFFFBAB1) + val onErrorDarkMediumContrast = Color(0xFF370001) + val errorContainerDarkMediumContrast = Color(0xFFFF5449) + val onErrorContainerDarkMediumContrast = Color(0xFF000000) + val backgroundDarkMediumContrast = Color(0xFF1B1B1F) + val onBackgroundDarkMediumContrast = Color(0xFFE3E1E6) + val surfaceDarkMediumContrast = Color(0xFF1B1B1F) + val onSurfaceDarkMediumContrast = Color(0xFFFEFAFF) + val surfaceVariantDarkMediumContrast = Color(0xFF44464E) + val onSurfaceVariantDarkMediumContrast = Color(0xFFCACAD4) + val outlineDarkMediumContrast = Color(0xFFA2A2AC) + val inverseSurfaceDarkMediumContrast = Color(0xFFE3E1E6) + val inverseOnSurfaceDarkMediumContrast = Color(0xFF1B1B1F) + val inversePrimaryDarkMediumContrast = Color(0xFF003FB0) + + // Light High Contrast + val primaryLightHighContrast = Color(0xFF001F5C) + val onPrimaryLightHighContrast = Color(0xFFFFFFFF) + val primaryContainerLightHighContrast = Color(0xFF003DA3) + val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) + val secondaryLightHighContrast = Color(0xFF2A2100) + val onSecondaryLightHighContrast = Color(0xFFFFFFFF) + val secondaryContainerLightHighContrast = Color(0xFF504100) + val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) + val tertiaryLightHighContrast = Color(0xFF002913) + val onTertiaryLightHighContrast = Color(0xFFFFFFFF) + val tertiaryContainerLightHighContrast = Color(0xFF004F28) + val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) + val errorLightHighContrast = Color(0xFF4E0002) + val onErrorLightHighContrast = Color(0xFFFFFFFF) + val errorContainerLightHighContrast = Color(0xFF8C0009) + val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) + val backgroundLightHighContrast = Color(0xFFFEFBFF) + val onBackgroundLightHighContrast = Color(0xFF1B1B1F) + val surfaceLightHighContrast = Color(0xFFFEFBFF) + val onSurfaceLightHighContrast = Color(0xFF000000) + val surfaceVariantLightHighContrast = Color(0xFFE2E2EC) + val onSurfaceVariantLightHighContrast = Color(0xFF21232B) + val outlineLightHighContrast = Color(0xFF40424A) + val inverseSurfaceLightHighContrast = Color(0xFF303033) + val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) + val inversePrimaryLightHighContrast = Color(0xFFE2E8FF) + + // Dark High Contrast + val primaryDarkHighContrast = Color(0xFFFCFAFF) + val onPrimaryDarkHighContrast = Color(0xFF000000) + val primaryContainerDarkHighContrast = Color(0xFFB1C5FF) + val onPrimaryContainerDarkHighContrast = Color(0xFF000000) + val secondaryDarkHighContrast = Color(0xFFFFF0C1) + val onSecondaryDarkHighContrast = Color(0xFF000000) + val secondaryContainerDarkHighContrast = Color(0xFFE9C426) + val onSecondaryContainerDarkHighContrast = Color(0xFF000000) + val tertiaryDarkHighContrast = Color(0xFFEDFFED) + val onTertiaryDarkHighContrast = Color(0xFF000000) + val tertiaryContainerDarkHighContrast = Color(0xFF33E286) + val onTertiaryContainerDarkHighContrast = Color(0xFF000000) + val errorDarkHighContrast = Color(0xFFFFF9F9) + val onErrorDarkHighContrast = Color(0xFF000000) + val errorContainerDarkHighContrast = Color(0xFFFFBAB1) + val onErrorContainerDarkHighContrast = Color(0xFF000000) + val backgroundDarkHighContrast = Color(0xFF1B1B1F) + val onBackgroundDarkHighContrast = Color(0xFFE3E1E6) + val surfaceDarkHighContrast = Color(0xFF1B1B1F) + val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) + val surfaceVariantDarkHighContrast = Color(0xFF44464E) + val onSurfaceVariantDarkHighContrast = Color(0xFFFCFAFF) + val outlineDarkHighContrast = Color(0xFFCACAD4) + val inverseSurfaceDarkHighContrast = Color(0xFFE3E1E6) + val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) + val inversePrimaryDarkHighContrast = Color(0xFF002568) +} diff --git a/app/src/main/java/eu/darken/capod/common/theming/CapodColorsGreen.kt b/app/src/main/java/eu/darken/capod/common/theming/CapodColorsGreen.kt new file mode 100644 index 00000000..4a2715a5 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/CapodColorsGreen.kt @@ -0,0 +1,173 @@ +package eu.darken.capod.common.theming + +import androidx.compose.ui.graphics.Color + +object CapodColorsGreen { + // Light (seed #006D39) + val primaryLight = Color(0xFF006D39) + val onPrimaryLight = Color(0xFFFFFFFF) + val primaryContainerLight = Color(0xFF5CFFA0) + val onPrimaryContainerLight = Color(0xFF00210D) + val secondaryLight = Color(0xFF4E6354) + val onSecondaryLight = Color(0xFFFFFFFF) + val secondaryContainerLight = Color(0xFFD1E8D5) + val onSecondaryContainerLight = Color(0xFF0C1F14) + val tertiaryLight = Color(0xFF3C6472) + val onTertiaryLight = Color(0xFFFFFFFF) + val tertiaryContainerLight = Color(0xFFBFE9F9) + val onTertiaryContainerLight = Color(0xFF001F28) + val errorLight = Color(0xFFBA1B1B) + val onErrorLight = Color(0xFFFFFFFF) + val errorContainerLight = Color(0xFFFFDAD4) + val onErrorContainerLight = Color(0xFF410001) + val backgroundLight = Color(0xFFFBFDF8) + val onBackgroundLight = Color(0xFF191C19) + val surfaceLight = Color(0xFFFBFDF8) + val onSurfaceLight = Color(0xFF191C19) + val surfaceVariantLight = Color(0xFFDDE5DB) + val onSurfaceVariantLight = Color(0xFF414942) + val outlineLight = Color(0xFF717971) + val inverseSurfaceLight = Color(0xFF2E312E) + val inverseOnSurfaceLight = Color(0xFFF0F1EC) + val inversePrimaryLight = Color(0xFF33E286) + + // Dark + val primaryDark = Color(0xFF33E286) + val onPrimaryDark = Color(0xFF00391B) + val primaryContainerDark = Color(0xFF005229) + val onPrimaryContainerDark = Color(0xFF5CFFA0) + val secondaryDark = Color(0xFFB5CCBA) + val onSecondaryDark = Color(0xFF213528) + val secondaryContainerDark = Color(0xFF374B3D) + val onSecondaryContainerDark = Color(0xFFD1E8D5) + val tertiaryDark = Color(0xFFA4CDDD) + val onTertiaryDark = Color(0xFF053542) + val tertiaryContainerDark = Color(0xFF234C59) + val onTertiaryContainerDark = Color(0xFFBFE9F9) + val errorDark = Color(0xFFFFB4A9) + val onErrorDark = Color(0xFF680003) + val errorContainerDark = Color(0xFF930006) + val onErrorContainerDark = Color(0xFFFFDAD4) + val backgroundDark = Color(0xFF191C19) + val onBackgroundDark = Color(0xFFE1E3DE) + val surfaceDark = Color(0xFF191C19) + val onSurfaceDark = Color(0xFFE1E3DE) + val surfaceVariantDark = Color(0xFF414942) + val onSurfaceVariantDark = Color(0xFFC1C9C0) + val outlineDark = Color(0xFF8B938B) + val inverseSurfaceDark = Color(0xFFE1E3DE) + val inverseOnSurfaceDark = Color(0xFF191C19) + val inversePrimaryDark = Color(0xFF006D39) + + // Light Medium Contrast + val primaryLightMediumContrast = Color(0xFF004F28) + val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) + val primaryContainerLightMediumContrast = Color(0xFF1F864D) + val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val secondaryLightMediumContrast = Color(0xFF334839) + val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) + val secondaryContainerLightMediumContrast = Color(0xFF647A6A) + val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val tertiaryLightMediumContrast = Color(0xFF1E4855) + val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) + val tertiaryContainerLightMediumContrast = Color(0xFF537A88) + val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) + val errorLightMediumContrast = Color(0xFF8C0009) + val onErrorLightMediumContrast = Color(0xFFFFFFFF) + val errorContainerLightMediumContrast = Color(0xFFDA342E) + val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) + val backgroundLightMediumContrast = Color(0xFFFBFDF8) + val onBackgroundLightMediumContrast = Color(0xFF191C19) + val surfaceLightMediumContrast = Color(0xFFFBFDF8) + val onSurfaceLightMediumContrast = Color(0xFF191C19) + val surfaceVariantLightMediumContrast = Color(0xFFDDE5DB) + val onSurfaceVariantLightMediumContrast = Color(0xFF3D453E) + val outlineLightMediumContrast = Color(0xFF596159) + val inverseSurfaceLightMediumContrast = Color(0xFF2E312E) + val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F1EC) + val inversePrimaryLightMediumContrast = Color(0xFF33E286) + + // Dark Medium Contrast + val primaryDarkMediumContrast = Color(0xFF3FE78B) + val onPrimaryDarkMediumContrast = Color(0xFF001B09) + val primaryContainerDarkMediumContrast = Color(0xFF00AA55) + val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) + val secondaryDarkMediumContrast = Color(0xFFBAD0BF) + val onSecondaryDarkMediumContrast = Color(0xFF071A0F) + val secondaryContainerDarkMediumContrast = Color(0xFF809686) + val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) + val tertiaryDarkMediumContrast = Color(0xFFA8D1E1) + val onTertiaryDarkMediumContrast = Color(0xFF001921) + val tertiaryContainerDarkMediumContrast = Color(0xFF6F97A6) + val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) + val errorDarkMediumContrast = Color(0xFFFFBAB1) + val onErrorDarkMediumContrast = Color(0xFF370001) + val errorContainerDarkMediumContrast = Color(0xFFFF5449) + val onErrorContainerDarkMediumContrast = Color(0xFF000000) + val backgroundDarkMediumContrast = Color(0xFF191C19) + val onBackgroundDarkMediumContrast = Color(0xFFE1E3DE) + val surfaceDarkMediumContrast = Color(0xFF191C19) + val onSurfaceDarkMediumContrast = Color(0xFFFCFEF9) + val surfaceVariantDarkMediumContrast = Color(0xFF414942) + val onSurfaceVariantDarkMediumContrast = Color(0xFFC5CDC4) + val outlineDarkMediumContrast = Color(0xFF9DA59D) + val inverseSurfaceDarkMediumContrast = Color(0xFFE1E3DE) + val inverseOnSurfaceDarkMediumContrast = Color(0xFF191C19) + val inversePrimaryDarkMediumContrast = Color(0xFF00542A) + + // Light High Contrast + val primaryLightHighContrast = Color(0xFF002913) + val onPrimaryLightHighContrast = Color(0xFFFFFFFF) + val primaryContainerLightHighContrast = Color(0xFF004F28) + val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) + val secondaryLightHighContrast = Color(0xFF132719) + val onSecondaryLightHighContrast = Color(0xFFFFFFFF) + val secondaryContainerLightHighContrast = Color(0xFF334839) + val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) + val tertiaryLightHighContrast = Color(0xFF002732) + val onTertiaryLightHighContrast = Color(0xFFFFFFFF) + val tertiaryContainerLightHighContrast = Color(0xFF1E4855) + val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) + val errorLightHighContrast = Color(0xFF4E0002) + val onErrorLightHighContrast = Color(0xFFFFFFFF) + val errorContainerLightHighContrast = Color(0xFF8C0009) + val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) + val backgroundLightHighContrast = Color(0xFFFBFDF8) + val onBackgroundLightHighContrast = Color(0xFF191C19) + val surfaceLightHighContrast = Color(0xFFFBFDF8) + val onSurfaceLightHighContrast = Color(0xFF000000) + val surfaceVariantLightHighContrast = Color(0xFFDDE5DB) + val onSurfaceVariantLightHighContrast = Color(0xFF1E2620) + val outlineLightHighContrast = Color(0xFF3D453E) + val inverseSurfaceLightHighContrast = Color(0xFF2E312E) + val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) + val inversePrimaryLightHighContrast = Color(0xFFBBFFCA) + + // Dark High Contrast + val primaryDarkHighContrast = Color(0xFFEDFFED) + val onPrimaryDarkHighContrast = Color(0xFF000000) + val primaryContainerDarkHighContrast = Color(0xFF33E286) + val onPrimaryContainerDarkHighContrast = Color(0xFF000000) + val secondaryDarkHighContrast = Color(0xFFEEFFF0) + val onSecondaryDarkHighContrast = Color(0xFF000000) + val secondaryContainerDarkHighContrast = Color(0xFFBAD0BF) + val onSecondaryContainerDarkHighContrast = Color(0xFF000000) + val tertiaryDarkHighContrast = Color(0xFFF2FCFF) + val onTertiaryDarkHighContrast = Color(0xFF000000) + val tertiaryContainerDarkHighContrast = Color(0xFFA8D1E1) + val onTertiaryContainerDarkHighContrast = Color(0xFF000000) + val errorDarkHighContrast = Color(0xFFFFF9F9) + val onErrorDarkHighContrast = Color(0xFF000000) + val errorContainerDarkHighContrast = Color(0xFFFFBAB1) + val onErrorContainerDarkHighContrast = Color(0xFF000000) + val backgroundDarkHighContrast = Color(0xFF191C19) + val onBackgroundDarkHighContrast = Color(0xFFE1E3DE) + val surfaceDarkHighContrast = Color(0xFF191C19) + val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) + val surfaceVariantDarkHighContrast = Color(0xFF414942) + val onSurfaceVariantDarkHighContrast = Color(0xFFF7FFF4) + val outlineDarkHighContrast = Color(0xFFC5CDC4) + val inverseSurfaceDarkHighContrast = Color(0xFFE1E3DE) + val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) + val inversePrimaryDarkHighContrast = Color(0xFF003118) +} diff --git a/app/src/main/java/eu/darken/capod/common/theming/CapodTheme.kt b/app/src/main/java/eu/darken/capod/common/theming/CapodTheme.kt index 1770cbef..24f5775d 100644 --- a/app/src/main/java/eu/darken/capod/common/theming/CapodTheme.kt +++ b/app/src/main/java/eu/darken/capod/common/theming/CapodTheme.kt @@ -3,86 +3,33 @@ package eu.darken.capod.common.theming import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -private val LightColorScheme = lightColorScheme( - primary = Color(0xFF3F7AFF), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFDAE2FF), - onPrimaryContainer = Color(0xFF00174B), - secondary = Color(0xFF715C00), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFFFE16C), - onSecondaryContainer = Color(0xFF231B00), - tertiary = Color(0xFF006D39), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFF5CFFA0), - onTertiaryContainer = Color(0xFF00210D), - error = Color(0xFFBA1B1B), - errorContainer = Color(0xFFFFDAD4), - onError = Color(0xFFFFFFFF), - onErrorContainer = Color(0xFF410001), - background = Color(0xFFFEFBFF), - onBackground = Color(0xFF1B1B1F), - surface = Color(0xFFFEFBFF), - onSurface = Color(0xFF1B1B1F), - surfaceVariant = Color(0xFFE2E2EC), - onSurfaceVariant = Color(0xFF44464E), - outline = Color(0xFF75767F), - inverseSurface = Color(0xFF303033), - inverseOnSurface = Color(0xFFF2F0F5), - inversePrimary = Color(0xFFB1C5FF), -) - -private val DarkColorScheme = darkColorScheme( - primary = Color(0xFF3F7AFF), - onPrimary = Color(0xFF002A78), - primaryContainer = Color(0xFF003EA7), - onPrimaryContainer = Color(0xFFDAE2FF), - secondary = Color(0xFFE9C426), - onSecondary = Color(0xFF3B2F00), - secondaryContainer = Color(0xFF564500), - onSecondaryContainer = Color(0xFFFFE16C), - tertiary = Color(0xFF33E286), - onTertiary = Color(0xFF00391B), - tertiaryContainer = Color(0xFF005229), - onTertiaryContainer = Color(0xFF5CFFA0), - error = Color(0xFFFFB4A9), - errorContainer = Color(0xFF930006), - onError = Color(0xFF680003), - onErrorContainer = Color(0xFFFFDAD4), - background = Color(0xFF1B1B1F), - onBackground = Color(0xFFE3E1E6), - surface = Color(0xFF1B1B1F), - onSurface = Color(0xFFE3E1E6), - surfaceVariant = Color(0xFF44464E), - onSurfaceVariant = Color(0xFFC6C6D0), - outline = Color(0xFF8F909A), - inverseSurface = Color(0xFFE3E1E6), - inverseOnSurface = Color(0xFF1B1B1F), - inversePrimary = Color(0xFF0054D9), -) - @Composable fun CapodTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - dynamicColor: Boolean = true, + state: ThemeState = ThemeState(), content: @Composable () -> Unit, ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } + val darkTheme = when (state.mode) { + ThemeMode.SYSTEM -> isSystemInDarkTheme() + ThemeMode.DARK -> true + ThemeMode.LIGHT -> false + } - darkTheme -> DarkColorScheme - else -> LightColorScheme + val dynamicColors = state.style == ThemeStyle.MATERIAL_YOU && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + val colorScheme = if (dynamicColors) { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } else { + if (darkTheme) { + ThemeColorProvider.getDarkColorScheme(state.color, state.style) + } else { + ThemeColorProvider.getLightColorScheme(state.color, state.style) + } } MaterialTheme( diff --git a/app/src/main/java/eu/darken/capod/common/theming/ThemeColor.kt b/app/src/main/java/eu/darken/capod/common/theming/ThemeColor.kt new file mode 100644 index 00000000..66452a5c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/ThemeColor.kt @@ -0,0 +1,15 @@ +package eu.darken.capod.common.theming + +import androidx.annotation.StringRes +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import eu.darken.capod.R + +@JsonClass(generateAdapter = false) +enum class ThemeColor( + @StringRes val labelRes: Int +) { + @Json(name = "theme.color.blue") BLUE(R.string.ui_theme_color_blue_label), + @Json(name = "theme.color.green") GREEN(R.string.ui_theme_color_green_label), + @Json(name = "theme.color.amber") AMBER(R.string.ui_theme_color_amber_label), +} diff --git a/app/src/main/java/eu/darken/capod/common/theming/ThemeColorProvider.kt b/app/src/main/java/eu/darken/capod/common/theming/ThemeColorProvider.kt new file mode 100644 index 00000000..e3d809cc --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/ThemeColorProvider.kt @@ -0,0 +1,560 @@ +package eu.darken.capod.common.theming + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme + +object ThemeColorProvider { + + fun getLightColorScheme(color: ThemeColor, style: ThemeStyle): ColorScheme = when (color) { + ThemeColor.BLUE -> blueLightScheme(style) + ThemeColor.GREEN -> greenLightScheme(style) + ThemeColor.AMBER -> amberLightScheme(style) + } + + fun getDarkColorScheme(color: ThemeColor, style: ThemeStyle): ColorScheme = when (color) { + ThemeColor.BLUE -> blueDarkScheme(style) + ThemeColor.GREEN -> greenDarkScheme(style) + ThemeColor.AMBER -> amberDarkScheme(style) + } + + private fun blueLightScheme(style: ThemeStyle): ColorScheme { + val c = CapodColorsBlue + return when (style) { + ThemeStyle.MEDIUM_CONTRAST -> lightColorScheme( + primary = c.primaryLightMediumContrast, + onPrimary = c.onPrimaryLightMediumContrast, + primaryContainer = c.primaryContainerLightMediumContrast, + onPrimaryContainer = c.onPrimaryContainerLightMediumContrast, + secondary = c.secondaryLightMediumContrast, + onSecondary = c.onSecondaryLightMediumContrast, + secondaryContainer = c.secondaryContainerLightMediumContrast, + onSecondaryContainer = c.onSecondaryContainerLightMediumContrast, + tertiary = c.tertiaryLightMediumContrast, + onTertiary = c.onTertiaryLightMediumContrast, + tertiaryContainer = c.tertiaryContainerLightMediumContrast, + onTertiaryContainer = c.onTertiaryContainerLightMediumContrast, + error = c.errorLightMediumContrast, + onError = c.onErrorLightMediumContrast, + errorContainer = c.errorContainerLightMediumContrast, + onErrorContainer = c.onErrorContainerLightMediumContrast, + background = c.backgroundLightMediumContrast, + onBackground = c.onBackgroundLightMediumContrast, + surface = c.surfaceLightMediumContrast, + onSurface = c.onSurfaceLightMediumContrast, + surfaceVariant = c.surfaceVariantLightMediumContrast, + onSurfaceVariant = c.onSurfaceVariantLightMediumContrast, + outline = c.outlineLightMediumContrast, + inverseSurface = c.inverseSurfaceLightMediumContrast, + inverseOnSurface = c.inverseOnSurfaceLightMediumContrast, + inversePrimary = c.inversePrimaryLightMediumContrast, + ) + ThemeStyle.HIGH_CONTRAST -> lightColorScheme( + primary = c.primaryLightHighContrast, + onPrimary = c.onPrimaryLightHighContrast, + primaryContainer = c.primaryContainerLightHighContrast, + onPrimaryContainer = c.onPrimaryContainerLightHighContrast, + secondary = c.secondaryLightHighContrast, + onSecondary = c.onSecondaryLightHighContrast, + secondaryContainer = c.secondaryContainerLightHighContrast, + onSecondaryContainer = c.onSecondaryContainerLightHighContrast, + tertiary = c.tertiaryLightHighContrast, + onTertiary = c.onTertiaryLightHighContrast, + tertiaryContainer = c.tertiaryContainerLightHighContrast, + onTertiaryContainer = c.onTertiaryContainerLightHighContrast, + error = c.errorLightHighContrast, + onError = c.onErrorLightHighContrast, + errorContainer = c.errorContainerLightHighContrast, + onErrorContainer = c.onErrorContainerLightHighContrast, + background = c.backgroundLightHighContrast, + onBackground = c.onBackgroundLightHighContrast, + surface = c.surfaceLightHighContrast, + onSurface = c.onSurfaceLightHighContrast, + surfaceVariant = c.surfaceVariantLightHighContrast, + onSurfaceVariant = c.onSurfaceVariantLightHighContrast, + outline = c.outlineLightHighContrast, + inverseSurface = c.inverseSurfaceLightHighContrast, + inverseOnSurface = c.inverseOnSurfaceLightHighContrast, + inversePrimary = c.inversePrimaryLightHighContrast, + ) + else -> lightColorScheme( + primary = c.primaryLight, + onPrimary = c.onPrimaryLight, + primaryContainer = c.primaryContainerLight, + onPrimaryContainer = c.onPrimaryContainerLight, + secondary = c.secondaryLight, + onSecondary = c.onSecondaryLight, + secondaryContainer = c.secondaryContainerLight, + onSecondaryContainer = c.onSecondaryContainerLight, + tertiary = c.tertiaryLight, + onTertiary = c.onTertiaryLight, + tertiaryContainer = c.tertiaryContainerLight, + onTertiaryContainer = c.onTertiaryContainerLight, + error = c.errorLight, + onError = c.onErrorLight, + errorContainer = c.errorContainerLight, + onErrorContainer = c.onErrorContainerLight, + background = c.backgroundLight, + onBackground = c.onBackgroundLight, + surface = c.surfaceLight, + onSurface = c.onSurfaceLight, + surfaceVariant = c.surfaceVariantLight, + onSurfaceVariant = c.onSurfaceVariantLight, + outline = c.outlineLight, + inverseSurface = c.inverseSurfaceLight, + inverseOnSurface = c.inverseOnSurfaceLight, + inversePrimary = c.inversePrimaryLight, + ) + } + } + + private fun blueDarkScheme(style: ThemeStyle): ColorScheme { + val c = CapodColorsBlue + return when (style) { + ThemeStyle.MEDIUM_CONTRAST -> darkColorScheme( + primary = c.primaryDarkMediumContrast, + onPrimary = c.onPrimaryDarkMediumContrast, + primaryContainer = c.primaryContainerDarkMediumContrast, + onPrimaryContainer = c.onPrimaryContainerDarkMediumContrast, + secondary = c.secondaryDarkMediumContrast, + onSecondary = c.onSecondaryDarkMediumContrast, + secondaryContainer = c.secondaryContainerDarkMediumContrast, + onSecondaryContainer = c.onSecondaryContainerDarkMediumContrast, + tertiary = c.tertiaryDarkMediumContrast, + onTertiary = c.onTertiaryDarkMediumContrast, + tertiaryContainer = c.tertiaryContainerDarkMediumContrast, + onTertiaryContainer = c.onTertiaryContainerDarkMediumContrast, + error = c.errorDarkMediumContrast, + onError = c.onErrorDarkMediumContrast, + errorContainer = c.errorContainerDarkMediumContrast, + onErrorContainer = c.onErrorContainerDarkMediumContrast, + background = c.backgroundDarkMediumContrast, + onBackground = c.onBackgroundDarkMediumContrast, + surface = c.surfaceDarkMediumContrast, + onSurface = c.onSurfaceDarkMediumContrast, + surfaceVariant = c.surfaceVariantDarkMediumContrast, + onSurfaceVariant = c.onSurfaceVariantDarkMediumContrast, + outline = c.outlineDarkMediumContrast, + inverseSurface = c.inverseSurfaceDarkMediumContrast, + inverseOnSurface = c.inverseOnSurfaceDarkMediumContrast, + inversePrimary = c.inversePrimaryDarkMediumContrast, + ) + ThemeStyle.HIGH_CONTRAST -> darkColorScheme( + primary = c.primaryDarkHighContrast, + onPrimary = c.onPrimaryDarkHighContrast, + primaryContainer = c.primaryContainerDarkHighContrast, + onPrimaryContainer = c.onPrimaryContainerDarkHighContrast, + secondary = c.secondaryDarkHighContrast, + onSecondary = c.onSecondaryDarkHighContrast, + secondaryContainer = c.secondaryContainerDarkHighContrast, + onSecondaryContainer = c.onSecondaryContainerDarkHighContrast, + tertiary = c.tertiaryDarkHighContrast, + onTertiary = c.onTertiaryDarkHighContrast, + tertiaryContainer = c.tertiaryContainerDarkHighContrast, + onTertiaryContainer = c.onTertiaryContainerDarkHighContrast, + error = c.errorDarkHighContrast, + onError = c.onErrorDarkHighContrast, + errorContainer = c.errorContainerDarkHighContrast, + onErrorContainer = c.onErrorContainerDarkHighContrast, + background = c.backgroundDarkHighContrast, + onBackground = c.onBackgroundDarkHighContrast, + surface = c.surfaceDarkHighContrast, + onSurface = c.onSurfaceDarkHighContrast, + surfaceVariant = c.surfaceVariantDarkHighContrast, + onSurfaceVariant = c.onSurfaceVariantDarkHighContrast, + outline = c.outlineDarkHighContrast, + inverseSurface = c.inverseSurfaceDarkHighContrast, + inverseOnSurface = c.inverseOnSurfaceDarkHighContrast, + inversePrimary = c.inversePrimaryDarkHighContrast, + ) + else -> darkColorScheme( + primary = c.primaryDark, + onPrimary = c.onPrimaryDark, + primaryContainer = c.primaryContainerDark, + onPrimaryContainer = c.onPrimaryContainerDark, + secondary = c.secondaryDark, + onSecondary = c.onSecondaryDark, + secondaryContainer = c.secondaryContainerDark, + onSecondaryContainer = c.onSecondaryContainerDark, + tertiary = c.tertiaryDark, + onTertiary = c.onTertiaryDark, + tertiaryContainer = c.tertiaryContainerDark, + onTertiaryContainer = c.onTertiaryContainerDark, + error = c.errorDark, + onError = c.onErrorDark, + errorContainer = c.errorContainerDark, + onErrorContainer = c.onErrorContainerDark, + background = c.backgroundDark, + onBackground = c.onBackgroundDark, + surface = c.surfaceDark, + onSurface = c.onSurfaceDark, + surfaceVariant = c.surfaceVariantDark, + onSurfaceVariant = c.onSurfaceVariantDark, + outline = c.outlineDark, + inverseSurface = c.inverseSurfaceDark, + inverseOnSurface = c.inverseOnSurfaceDark, + inversePrimary = c.inversePrimaryDark, + ) + } + } + + private fun greenLightScheme(style: ThemeStyle): ColorScheme { + val c = CapodColorsGreen + return when (style) { + ThemeStyle.MEDIUM_CONTRAST -> lightColorScheme( + primary = c.primaryLightMediumContrast, + onPrimary = c.onPrimaryLightMediumContrast, + primaryContainer = c.primaryContainerLightMediumContrast, + onPrimaryContainer = c.onPrimaryContainerLightMediumContrast, + secondary = c.secondaryLightMediumContrast, + onSecondary = c.onSecondaryLightMediumContrast, + secondaryContainer = c.secondaryContainerLightMediumContrast, + onSecondaryContainer = c.onSecondaryContainerLightMediumContrast, + tertiary = c.tertiaryLightMediumContrast, + onTertiary = c.onTertiaryLightMediumContrast, + tertiaryContainer = c.tertiaryContainerLightMediumContrast, + onTertiaryContainer = c.onTertiaryContainerLightMediumContrast, + error = c.errorLightMediumContrast, + onError = c.onErrorLightMediumContrast, + errorContainer = c.errorContainerLightMediumContrast, + onErrorContainer = c.onErrorContainerLightMediumContrast, + background = c.backgroundLightMediumContrast, + onBackground = c.onBackgroundLightMediumContrast, + surface = c.surfaceLightMediumContrast, + onSurface = c.onSurfaceLightMediumContrast, + surfaceVariant = c.surfaceVariantLightMediumContrast, + onSurfaceVariant = c.onSurfaceVariantLightMediumContrast, + outline = c.outlineLightMediumContrast, + inverseSurface = c.inverseSurfaceLightMediumContrast, + inverseOnSurface = c.inverseOnSurfaceLightMediumContrast, + inversePrimary = c.inversePrimaryLightMediumContrast, + ) + ThemeStyle.HIGH_CONTRAST -> lightColorScheme( + primary = c.primaryLightHighContrast, + onPrimary = c.onPrimaryLightHighContrast, + primaryContainer = c.primaryContainerLightHighContrast, + onPrimaryContainer = c.onPrimaryContainerLightHighContrast, + secondary = c.secondaryLightHighContrast, + onSecondary = c.onSecondaryLightHighContrast, + secondaryContainer = c.secondaryContainerLightHighContrast, + onSecondaryContainer = c.onSecondaryContainerLightHighContrast, + tertiary = c.tertiaryLightHighContrast, + onTertiary = c.onTertiaryLightHighContrast, + tertiaryContainer = c.tertiaryContainerLightHighContrast, + onTertiaryContainer = c.onTertiaryContainerLightHighContrast, + error = c.errorLightHighContrast, + onError = c.onErrorLightHighContrast, + errorContainer = c.errorContainerLightHighContrast, + onErrorContainer = c.onErrorContainerLightHighContrast, + background = c.backgroundLightHighContrast, + onBackground = c.onBackgroundLightHighContrast, + surface = c.surfaceLightHighContrast, + onSurface = c.onSurfaceLightHighContrast, + surfaceVariant = c.surfaceVariantLightHighContrast, + onSurfaceVariant = c.onSurfaceVariantLightHighContrast, + outline = c.outlineLightHighContrast, + inverseSurface = c.inverseSurfaceLightHighContrast, + inverseOnSurface = c.inverseOnSurfaceLightHighContrast, + inversePrimary = c.inversePrimaryLightHighContrast, + ) + else -> lightColorScheme( + primary = c.primaryLight, + onPrimary = c.onPrimaryLight, + primaryContainer = c.primaryContainerLight, + onPrimaryContainer = c.onPrimaryContainerLight, + secondary = c.secondaryLight, + onSecondary = c.onSecondaryLight, + secondaryContainer = c.secondaryContainerLight, + onSecondaryContainer = c.onSecondaryContainerLight, + tertiary = c.tertiaryLight, + onTertiary = c.onTertiaryLight, + tertiaryContainer = c.tertiaryContainerLight, + onTertiaryContainer = c.onTertiaryContainerLight, + error = c.errorLight, + onError = c.onErrorLight, + errorContainer = c.errorContainerLight, + onErrorContainer = c.onErrorContainerLight, + background = c.backgroundLight, + onBackground = c.onBackgroundLight, + surface = c.surfaceLight, + onSurface = c.onSurfaceLight, + surfaceVariant = c.surfaceVariantLight, + onSurfaceVariant = c.onSurfaceVariantLight, + outline = c.outlineLight, + inverseSurface = c.inverseSurfaceLight, + inverseOnSurface = c.inverseOnSurfaceLight, + inversePrimary = c.inversePrimaryLight, + ) + } + } + + private fun greenDarkScheme(style: ThemeStyle): ColorScheme { + val c = CapodColorsGreen + return when (style) { + ThemeStyle.MEDIUM_CONTRAST -> darkColorScheme( + primary = c.primaryDarkMediumContrast, + onPrimary = c.onPrimaryDarkMediumContrast, + primaryContainer = c.primaryContainerDarkMediumContrast, + onPrimaryContainer = c.onPrimaryContainerDarkMediumContrast, + secondary = c.secondaryDarkMediumContrast, + onSecondary = c.onSecondaryDarkMediumContrast, + secondaryContainer = c.secondaryContainerDarkMediumContrast, + onSecondaryContainer = c.onSecondaryContainerDarkMediumContrast, + tertiary = c.tertiaryDarkMediumContrast, + onTertiary = c.onTertiaryDarkMediumContrast, + tertiaryContainer = c.tertiaryContainerDarkMediumContrast, + onTertiaryContainer = c.onTertiaryContainerDarkMediumContrast, + error = c.errorDarkMediumContrast, + onError = c.onErrorDarkMediumContrast, + errorContainer = c.errorContainerDarkMediumContrast, + onErrorContainer = c.onErrorContainerDarkMediumContrast, + background = c.backgroundDarkMediumContrast, + onBackground = c.onBackgroundDarkMediumContrast, + surface = c.surfaceDarkMediumContrast, + onSurface = c.onSurfaceDarkMediumContrast, + surfaceVariant = c.surfaceVariantDarkMediumContrast, + onSurfaceVariant = c.onSurfaceVariantDarkMediumContrast, + outline = c.outlineDarkMediumContrast, + inverseSurface = c.inverseSurfaceDarkMediumContrast, + inverseOnSurface = c.inverseOnSurfaceDarkMediumContrast, + inversePrimary = c.inversePrimaryDarkMediumContrast, + ) + ThemeStyle.HIGH_CONTRAST -> darkColorScheme( + primary = c.primaryDarkHighContrast, + onPrimary = c.onPrimaryDarkHighContrast, + primaryContainer = c.primaryContainerDarkHighContrast, + onPrimaryContainer = c.onPrimaryContainerDarkHighContrast, + secondary = c.secondaryDarkHighContrast, + onSecondary = c.onSecondaryDarkHighContrast, + secondaryContainer = c.secondaryContainerDarkHighContrast, + onSecondaryContainer = c.onSecondaryContainerDarkHighContrast, + tertiary = c.tertiaryDarkHighContrast, + onTertiary = c.onTertiaryDarkHighContrast, + tertiaryContainer = c.tertiaryContainerDarkHighContrast, + onTertiaryContainer = c.onTertiaryContainerDarkHighContrast, + error = c.errorDarkHighContrast, + onError = c.onErrorDarkHighContrast, + errorContainer = c.errorContainerDarkHighContrast, + onErrorContainer = c.onErrorContainerDarkHighContrast, + background = c.backgroundDarkHighContrast, + onBackground = c.onBackgroundDarkHighContrast, + surface = c.surfaceDarkHighContrast, + onSurface = c.onSurfaceDarkHighContrast, + surfaceVariant = c.surfaceVariantDarkHighContrast, + onSurfaceVariant = c.onSurfaceVariantDarkHighContrast, + outline = c.outlineDarkHighContrast, + inverseSurface = c.inverseSurfaceDarkHighContrast, + inverseOnSurface = c.inverseOnSurfaceDarkHighContrast, + inversePrimary = c.inversePrimaryDarkHighContrast, + ) + else -> darkColorScheme( + primary = c.primaryDark, + onPrimary = c.onPrimaryDark, + primaryContainer = c.primaryContainerDark, + onPrimaryContainer = c.onPrimaryContainerDark, + secondary = c.secondaryDark, + onSecondary = c.onSecondaryDark, + secondaryContainer = c.secondaryContainerDark, + onSecondaryContainer = c.onSecondaryContainerDark, + tertiary = c.tertiaryDark, + onTertiary = c.onTertiaryDark, + tertiaryContainer = c.tertiaryContainerDark, + onTertiaryContainer = c.onTertiaryContainerDark, + error = c.errorDark, + onError = c.onErrorDark, + errorContainer = c.errorContainerDark, + onErrorContainer = c.onErrorContainerDark, + background = c.backgroundDark, + onBackground = c.onBackgroundDark, + surface = c.surfaceDark, + onSurface = c.onSurfaceDark, + surfaceVariant = c.surfaceVariantDark, + onSurfaceVariant = c.onSurfaceVariantDark, + outline = c.outlineDark, + inverseSurface = c.inverseSurfaceDark, + inverseOnSurface = c.inverseOnSurfaceDark, + inversePrimary = c.inversePrimaryDark, + ) + } + } + + private fun amberLightScheme(style: ThemeStyle): ColorScheme { + val c = CapodColorsAmber + return when (style) { + ThemeStyle.MEDIUM_CONTRAST -> lightColorScheme( + primary = c.primaryLightMediumContrast, + onPrimary = c.onPrimaryLightMediumContrast, + primaryContainer = c.primaryContainerLightMediumContrast, + onPrimaryContainer = c.onPrimaryContainerLightMediumContrast, + secondary = c.secondaryLightMediumContrast, + onSecondary = c.onSecondaryLightMediumContrast, + secondaryContainer = c.secondaryContainerLightMediumContrast, + onSecondaryContainer = c.onSecondaryContainerLightMediumContrast, + tertiary = c.tertiaryLightMediumContrast, + onTertiary = c.onTertiaryLightMediumContrast, + tertiaryContainer = c.tertiaryContainerLightMediumContrast, + onTertiaryContainer = c.onTertiaryContainerLightMediumContrast, + error = c.errorLightMediumContrast, + onError = c.onErrorLightMediumContrast, + errorContainer = c.errorContainerLightMediumContrast, + onErrorContainer = c.onErrorContainerLightMediumContrast, + background = c.backgroundLightMediumContrast, + onBackground = c.onBackgroundLightMediumContrast, + surface = c.surfaceLightMediumContrast, + onSurface = c.onSurfaceLightMediumContrast, + surfaceVariant = c.surfaceVariantLightMediumContrast, + onSurfaceVariant = c.onSurfaceVariantLightMediumContrast, + outline = c.outlineLightMediumContrast, + inverseSurface = c.inverseSurfaceLightMediumContrast, + inverseOnSurface = c.inverseOnSurfaceLightMediumContrast, + inversePrimary = c.inversePrimaryLightMediumContrast, + ) + ThemeStyle.HIGH_CONTRAST -> lightColorScheme( + primary = c.primaryLightHighContrast, + onPrimary = c.onPrimaryLightHighContrast, + primaryContainer = c.primaryContainerLightHighContrast, + onPrimaryContainer = c.onPrimaryContainerLightHighContrast, + secondary = c.secondaryLightHighContrast, + onSecondary = c.onSecondaryLightHighContrast, + secondaryContainer = c.secondaryContainerLightHighContrast, + onSecondaryContainer = c.onSecondaryContainerLightHighContrast, + tertiary = c.tertiaryLightHighContrast, + onTertiary = c.onTertiaryLightHighContrast, + tertiaryContainer = c.tertiaryContainerLightHighContrast, + onTertiaryContainer = c.onTertiaryContainerLightHighContrast, + error = c.errorLightHighContrast, + onError = c.onErrorLightHighContrast, + errorContainer = c.errorContainerLightHighContrast, + onErrorContainer = c.onErrorContainerLightHighContrast, + background = c.backgroundLightHighContrast, + onBackground = c.onBackgroundLightHighContrast, + surface = c.surfaceLightHighContrast, + onSurface = c.onSurfaceLightHighContrast, + surfaceVariant = c.surfaceVariantLightHighContrast, + onSurfaceVariant = c.onSurfaceVariantLightHighContrast, + outline = c.outlineLightHighContrast, + inverseSurface = c.inverseSurfaceLightHighContrast, + inverseOnSurface = c.inverseOnSurfaceLightHighContrast, + inversePrimary = c.inversePrimaryLightHighContrast, + ) + else -> lightColorScheme( + primary = c.primaryLight, + onPrimary = c.onPrimaryLight, + primaryContainer = c.primaryContainerLight, + onPrimaryContainer = c.onPrimaryContainerLight, + secondary = c.secondaryLight, + onSecondary = c.onSecondaryLight, + secondaryContainer = c.secondaryContainerLight, + onSecondaryContainer = c.onSecondaryContainerLight, + tertiary = c.tertiaryLight, + onTertiary = c.onTertiaryLight, + tertiaryContainer = c.tertiaryContainerLight, + onTertiaryContainer = c.onTertiaryContainerLight, + error = c.errorLight, + onError = c.onErrorLight, + errorContainer = c.errorContainerLight, + onErrorContainer = c.onErrorContainerLight, + background = c.backgroundLight, + onBackground = c.onBackgroundLight, + surface = c.surfaceLight, + onSurface = c.onSurfaceLight, + surfaceVariant = c.surfaceVariantLight, + onSurfaceVariant = c.onSurfaceVariantLight, + outline = c.outlineLight, + inverseSurface = c.inverseSurfaceLight, + inverseOnSurface = c.inverseOnSurfaceLight, + inversePrimary = c.inversePrimaryLight, + ) + } + } + + private fun amberDarkScheme(style: ThemeStyle): ColorScheme { + val c = CapodColorsAmber + return when (style) { + ThemeStyle.MEDIUM_CONTRAST -> darkColorScheme( + primary = c.primaryDarkMediumContrast, + onPrimary = c.onPrimaryDarkMediumContrast, + primaryContainer = c.primaryContainerDarkMediumContrast, + onPrimaryContainer = c.onPrimaryContainerDarkMediumContrast, + secondary = c.secondaryDarkMediumContrast, + onSecondary = c.onSecondaryDarkMediumContrast, + secondaryContainer = c.secondaryContainerDarkMediumContrast, + onSecondaryContainer = c.onSecondaryContainerDarkMediumContrast, + tertiary = c.tertiaryDarkMediumContrast, + onTertiary = c.onTertiaryDarkMediumContrast, + tertiaryContainer = c.tertiaryContainerDarkMediumContrast, + onTertiaryContainer = c.onTertiaryContainerDarkMediumContrast, + error = c.errorDarkMediumContrast, + onError = c.onErrorDarkMediumContrast, + errorContainer = c.errorContainerDarkMediumContrast, + onErrorContainer = c.onErrorContainerDarkMediumContrast, + background = c.backgroundDarkMediumContrast, + onBackground = c.onBackgroundDarkMediumContrast, + surface = c.surfaceDarkMediumContrast, + onSurface = c.onSurfaceDarkMediumContrast, + surfaceVariant = c.surfaceVariantDarkMediumContrast, + onSurfaceVariant = c.onSurfaceVariantDarkMediumContrast, + outline = c.outlineDarkMediumContrast, + inverseSurface = c.inverseSurfaceDarkMediumContrast, + inverseOnSurface = c.inverseOnSurfaceDarkMediumContrast, + inversePrimary = c.inversePrimaryDarkMediumContrast, + ) + ThemeStyle.HIGH_CONTRAST -> darkColorScheme( + primary = c.primaryDarkHighContrast, + onPrimary = c.onPrimaryDarkHighContrast, + primaryContainer = c.primaryContainerDarkHighContrast, + onPrimaryContainer = c.onPrimaryContainerDarkHighContrast, + secondary = c.secondaryDarkHighContrast, + onSecondary = c.onSecondaryDarkHighContrast, + secondaryContainer = c.secondaryContainerDarkHighContrast, + onSecondaryContainer = c.onSecondaryContainerDarkHighContrast, + tertiary = c.tertiaryDarkHighContrast, + onTertiary = c.onTertiaryDarkHighContrast, + tertiaryContainer = c.tertiaryContainerDarkHighContrast, + onTertiaryContainer = c.onTertiaryContainerDarkHighContrast, + error = c.errorDarkHighContrast, + onError = c.onErrorDarkHighContrast, + errorContainer = c.errorContainerDarkHighContrast, + onErrorContainer = c.onErrorContainerDarkHighContrast, + background = c.backgroundDarkHighContrast, + onBackground = c.onBackgroundDarkHighContrast, + surface = c.surfaceDarkHighContrast, + onSurface = c.onSurfaceDarkHighContrast, + surfaceVariant = c.surfaceVariantDarkHighContrast, + onSurfaceVariant = c.onSurfaceVariantDarkHighContrast, + outline = c.outlineDarkHighContrast, + inverseSurface = c.inverseSurfaceDarkHighContrast, + inverseOnSurface = c.inverseOnSurfaceDarkHighContrast, + inversePrimary = c.inversePrimaryDarkHighContrast, + ) + else -> darkColorScheme( + primary = c.primaryDark, + onPrimary = c.onPrimaryDark, + primaryContainer = c.primaryContainerDark, + onPrimaryContainer = c.onPrimaryContainerDark, + secondary = c.secondaryDark, + onSecondary = c.onSecondaryDark, + secondaryContainer = c.secondaryContainerDark, + onSecondaryContainer = c.onSecondaryContainerDark, + tertiary = c.tertiaryDark, + onTertiary = c.onTertiaryDark, + tertiaryContainer = c.tertiaryContainerDark, + onTertiaryContainer = c.onTertiaryContainerDark, + error = c.errorDark, + onError = c.onErrorDark, + errorContainer = c.errorContainerDark, + onErrorContainer = c.onErrorContainerDark, + background = c.backgroundDark, + onBackground = c.onBackgroundDark, + surface = c.surfaceDark, + onSurface = c.onSurfaceDark, + surfaceVariant = c.surfaceVariantDark, + onSurfaceVariant = c.onSurfaceVariantDark, + outline = c.outlineDark, + inverseSurface = c.inverseSurfaceDark, + inverseOnSurface = c.inverseOnSurfaceDark, + inversePrimary = c.inversePrimaryDark, + ) + } + } +} diff --git a/app/src/main/java/eu/darken/capod/common/theming/ThemeMode.kt b/app/src/main/java/eu/darken/capod/common/theming/ThemeMode.kt new file mode 100644 index 00000000..644496d2 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/ThemeMode.kt @@ -0,0 +1,15 @@ +package eu.darken.capod.common.theming + +import androidx.annotation.StringRes +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import eu.darken.capod.R + +@JsonClass(generateAdapter = false) +enum class ThemeMode( + @StringRes val labelRes: Int +) { + @Json(name = "theme.mode.system") SYSTEM(R.string.ui_theme_mode_system_label), + @Json(name = "theme.mode.dark") DARK(R.string.ui_theme_mode_dark_label), + @Json(name = "theme.mode.light") LIGHT(R.string.ui_theme_mode_light_label), +} diff --git a/app/src/main/java/eu/darken/capod/common/theming/ThemeState.kt b/app/src/main/java/eu/darken/capod/common/theming/ThemeState.kt new file mode 100644 index 00000000..d2bc0aed --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/ThemeState.kt @@ -0,0 +1,7 @@ +package eu.darken.capod.common.theming + +data class ThemeState( + val mode: ThemeMode = ThemeMode.SYSTEM, + val style: ThemeStyle = ThemeStyle.DEFAULT, + val color: ThemeColor = ThemeColor.BLUE, +) diff --git a/app/src/main/java/eu/darken/capod/common/theming/ThemeStyle.kt b/app/src/main/java/eu/darken/capod/common/theming/ThemeStyle.kt new file mode 100644 index 00000000..d9b7f75a --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/theming/ThemeStyle.kt @@ -0,0 +1,16 @@ +package eu.darken.capod.common.theming + +import androidx.annotation.StringRes +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import eu.darken.capod.R + +@JsonClass(generateAdapter = false) +enum class ThemeStyle( + @StringRes val labelRes: Int +) { + @Json(name = "theme.style.default") DEFAULT(R.string.ui_theme_style_default_label), + @Json(name = "theme.style.materialyou") MATERIAL_YOU(R.string.ui_theme_style_materialyou_label), + @Json(name = "theme.style.mediumcontrast") MEDIUM_CONTRAST(R.string.ui_theme_style_medium_contrast_label), + @Json(name = "theme.style.highcontrast") HIGH_CONTRAST(R.string.ui_theme_style_high_contrast_label), +} diff --git a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt index 51d0b152..21bdb30a 100644 --- a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt +++ b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt @@ -11,9 +11,15 @@ import eu.darken.capod.common.debug.DebugSettings import eu.darken.capod.common.preferences.PreferenceStoreMapper import eu.darken.capod.common.preferences.Settings import eu.darken.capod.common.preferences.createFlowPreference +import eu.darken.capod.common.theming.ThemeColor +import eu.darken.capod.common.theming.ThemeMode +import eu.darken.capod.common.theming.ThemeState +import eu.darken.capod.common.theming.ThemeStyle import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.protocol.IdentityResolvingKey import eu.darken.capod.pods.core.apple.protocol.ProximityEncryptionKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import javax.inject.Inject import javax.inject.Singleton @@ -62,6 +68,28 @@ class GeneralSettings @Inject constructor( val isOnboardingDone = preferences.createFlowPreference("core.onboarding.done", false) + val themeMode = preferences.createFlowPreference( + "core.ui.theme.mode", ThemeMode.SYSTEM, moshi, onErrorFallbackToDefault = true + ) + val themeStyle = preferences.createFlowPreference( + "core.ui.theme.style", ThemeStyle.DEFAULT, moshi, onErrorFallbackToDefault = true + ) + val themeColor = preferences.createFlowPreference( + "core.ui.theme.color", ThemeColor.BLUE, moshi, onErrorFallbackToDefault = true + ) + + val currentThemeState: ThemeState + get() = ThemeState( + mode = themeMode.value, + style = themeStyle.value, + color = themeColor.value, + ) + + val themeState: Flow + get() = combine(themeMode.flow, themeStyle.flow, themeColor.flow) { mode, style, color -> + ThemeState(mode, style, color) + } + override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper( monitorMode, useExtraMonitorNotification, @@ -70,6 +98,9 @@ class GeneralSettings @Inject constructor( isOffloadedFilteringDisabled, isOffloadedBatchingDisabled, useIndirectScanResultCallback, + themeMode, + themeStyle, + themeColor, debugSettings.isAutoReportingEnabled, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt index 16fbfe61..4eef75a9 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt @@ -3,8 +3,15 @@ package eu.darken.capod.main.ui import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.toArgb import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack @@ -40,10 +47,20 @@ class MainActivity : Activity2() { } setContent { + val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) val backStack = rememberNavBackStack(startDestination) navCtrl.setup(backStack) - CapodTheme { + CapodTheme(state = themeState) { + val backgroundColor = MaterialTheme.colorScheme.background + val useDarkIcons = backgroundColor.luminance() > 0.5f + SideEffect { + window.decorView.setBackgroundColor(backgroundColor.toArgb()) + val insetsController = WindowCompat.getInsetsController(window, window.decorView) + insetsController.isAppearanceLightStatusBars = useDarkIcons + insetsController.isAppearanceLightNavigationBars = useDarkIcons + } + CompositionLocalProvider(LocalNavigationController provides navCtrl) { NavDisplay( backStack = backStack, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt index 5d58ad0b..01c99602 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt @@ -1,5 +1,6 @@ package eu.darken.capod.main.ui.settings.general +import android.os.Build import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -11,10 +12,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.twotone.ArrowBack import androidx.compose.material.icons.twotone.AccountTree import androidx.compose.material.icons.twotone.BugReport +import androidx.compose.material.icons.twotone.Contrast +import androidx.compose.material.icons.twotone.DarkMode import androidx.compose.material.icons.twotone.DisabledVisible import androidx.compose.material.icons.twotone.FilterList import androidx.compose.material.icons.automirrored.twotone.Message import androidx.compose.material.icons.twotone.Notifications +import androidx.compose.material.icons.twotone.Palette import androidx.compose.material.icons.twotone.SettingsBluetooth import androidx.compose.material.icons.automirrored.twotone.ViewList import androidx.compose.material3.AlertDialog @@ -48,6 +52,12 @@ import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.settings.SettingsBaseItem import eu.darken.capod.common.settings.SettingsCategoryHeader +import eu.darken.capod.common.settings.SettingsListPreferenceItem +import eu.darken.capod.common.settings.ThemeColorSelectorDialog +import eu.darken.capod.common.theming.ThemeColor +import eu.darken.capod.common.theming.ThemeMode +import eu.darken.capod.common.theming.ThemeState +import eu.darken.capod.common.theming.ThemeStyle import eu.darken.capod.main.core.MonitorMode @Composable @@ -68,6 +78,9 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) { onOffloadedFilteringDisabledChanged = { disabled -> vm.setOffloadedFilteringDisabled(disabled) }, onOffloadedBatchingDisabledChanged = { disabled -> vm.setOffloadedBatchingDisabled(disabled) }, onUseIndirectScanResultCallbackChanged = { enabled -> vm.setUseIndirectScanResultCallback(enabled) }, + onThemeModeSelected = { mode -> vm.setThemeMode(mode) }, + onThemeStyleSelected = { style -> vm.setThemeStyle(style) }, + onThemeColorSelected = { color -> vm.setThemeColor(color) }, ) } } @@ -85,10 +98,17 @@ fun GeneralSettingsScreen( onOffloadedFilteringDisabledChanged: (Boolean) -> Unit, onOffloadedBatchingDisabledChanged: (Boolean) -> Unit, onUseIndirectScanResultCallbackChanged: (Boolean) -> Unit, + onThemeModeSelected: (ThemeMode) -> Unit = {}, + onThemeStyleSelected: (ThemeStyle) -> Unit = {}, + onThemeColorSelected: (ThemeColor) -> Unit = {}, modifier: Modifier = Modifier, ) { var showMonitorModeDialog by remember { mutableStateOf(false) } var showScannerModeDialog by remember { mutableStateOf(false) } + var showColorDialog by remember { mutableStateOf(false) } + + val isMaterialYouActive = state.themeState.style == ThemeStyle.MATERIAL_YOU && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S Scaffold( modifier = modifier, @@ -107,6 +127,42 @@ fun GeneralSettingsScreen( }, ) { innerPadding -> LazyColumn(modifier = Modifier.padding(innerPadding)) { + item { + SettingsCategoryHeader(text = stringResource(R.string.settings_category_appearance_label)) + } + item { + SettingsListPreferenceItem( + icon = Icons.TwoTone.DarkMode, + title = stringResource(R.string.ui_theme_mode_label), + entries = ThemeMode.entries, + selectedEntry = state.themeState.mode, + onEntrySelected = onThemeModeSelected, + entryLabel = { stringResource(it.labelRes) }, + ) + } + item { + SettingsListPreferenceItem( + icon = Icons.TwoTone.Contrast, + title = stringResource(R.string.ui_theme_style_label), + entries = ThemeStyle.entries, + selectedEntry = state.themeState.style, + onEntrySelected = onThemeStyleSelected, + entryLabel = { stringResource(it.labelRes) }, + ) + } + item { + SettingsBaseItem( + title = stringResource(R.string.ui_theme_color_label), + subtitle = if (isMaterialYouActive) { + stringResource(R.string.ui_theme_color_disabled_materialyou) + } else { + stringResource(state.themeState.color.labelRes) + }, + icon = Icons.TwoTone.Palette, + onClick = { showColorDialog = true }, + enabled = !isMaterialYouActive, + ) + } item { SettingsBaseItem( title = stringResource(R.string.settings_monitor_mode_label), @@ -244,6 +300,17 @@ fun GeneralSettingsScreen( onDismiss = { showScannerModeDialog = false }, ) } + + if (showColorDialog) { + ThemeColorSelectorDialog( + selectedColor = state.themeState.color, + onColorSelected = { + onThemeColorSelected(it) + showColorDialog = false + }, + onDismiss = { showColorDialog = false }, + ) + } } @Preview2 @@ -258,6 +325,7 @@ private fun GeneralSettingsScreenPreview() = PreviewWrapper { isOffloadedFilteringDisabled = false, isOffloadedBatchingDisabled = false, useIndirectScanResultCallback = false, + themeState = ThemeState(), ), onNavigateUp = {}, onMonitorModeSelected = {}, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt index afd7c14e..fc6be237 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt @@ -6,6 +6,10 @@ import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.shareLatest import eu.darken.capod.common.navigation.Nav +import eu.darken.capod.common.theming.ThemeColor +import eu.darken.capod.common.theming.ThemeMode +import eu.darken.capod.common.theming.ThemeState +import eu.darken.capod.common.theming.ThemeStyle import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.MonitorMode @@ -26,25 +30,38 @@ class GeneralSettingsViewModel @Inject constructor( val isOffloadedFilteringDisabled: Boolean, val isOffloadedBatchingDisabled: Boolean, val useIndirectScanResultCallback: Boolean, + val themeState: ThemeState, ) val state = combine( - generalSettings.monitorMode.flow, - generalSettings.scannerMode.flow, - generalSettings.useExtraMonitorNotification.flow, - generalSettings.keepConnectedNotificationAfterDisconnect.flow, - generalSettings.isOffloadedFilteringDisabled.flow, - generalSettings.isOffloadedBatchingDisabled.flow, - generalSettings.useIndirectScanResultCallback.flow, - ) { values -> + combine( + generalSettings.monitorMode.flow, + generalSettings.scannerMode.flow, + generalSettings.useExtraMonitorNotification.flow, + generalSettings.keepConnectedNotificationAfterDisconnect.flow, + ) { monitorMode, scannerMode, showNotif, keepNotif -> + @Suppress("USELESS_CAST") + arrayOf(monitorMode as Any, scannerMode as Any, showNotif as Any, keepNotif as Any) + }, + combine( + generalSettings.isOffloadedFilteringDisabled.flow, + generalSettings.isOffloadedBatchingDisabled.flow, + generalSettings.useIndirectScanResultCallback.flow, + ) { filtering, batching, indirect -> + @Suppress("USELESS_CAST") + arrayOf(filtering as Any, batching as Any, indirect as Any) + }, + generalSettings.themeState, + ) { general, compat, themeState -> State( - monitorMode = values[0] as MonitorMode, - scannerMode = values[1] as ScannerMode, - showConnectedNotification = values[2] as Boolean, - keepNotificationAfterDisconnect = values[3] as Boolean, - isOffloadedFilteringDisabled = values[4] as Boolean, - isOffloadedBatchingDisabled = values[5] as Boolean, - useIndirectScanResultCallback = values[6] as Boolean, + monitorMode = general[0] as MonitorMode, + scannerMode = general[1] as ScannerMode, + showConnectedNotification = general[2] as Boolean, + keepNotificationAfterDisconnect = general[3] as Boolean, + isOffloadedFilteringDisabled = compat[0] as Boolean, + isOffloadedBatchingDisabled = compat[1] as Boolean, + useIndirectScanResultCallback = compat[2] as Boolean, + themeState = themeState, ) }.shareLatest(scope = vmScope) @@ -76,6 +93,18 @@ class GeneralSettingsViewModel @Inject constructor( generalSettings.useIndirectScanResultCallback.value = enabled } + fun setThemeMode(mode: ThemeMode) { + generalSettings.themeMode.value = mode + } + + fun setThemeStyle(style: ThemeStyle) { + generalSettings.themeStyle.value = style + } + + fun setThemeColor(color: ThemeColor) { + generalSettings.themeColor.value = color + } + fun goToDebugSettings() { navTo(Nav.Settings.Debug) } diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt index 4c582a19..7f47774e 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.qualifiers.ApplicationContext @@ -16,6 +17,7 @@ import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.theming.CapodTheme import eu.darken.capod.common.uix.Activity2 import eu.darken.capod.common.upgrade.UpgradeRepo +import eu.darken.capod.main.core.GeneralSettings import javax.inject.Inject @AndroidEntryPoint @@ -24,6 +26,7 @@ class WidgetConfigurationActivity : Activity2() { private val vm: WidgetConfigurationViewModel by viewModels() @Inject lateinit var upgradeRepo: UpgradeRepo + @Inject lateinit var generalSettings: GeneralSettings @ApplicationContext @Inject lateinit var appContext: Context private var widgetId: Int = AppWidgetManager.INVALID_APPWIDGET_ID @@ -48,7 +51,8 @@ class WidgetConfigurationActivity : Activity2() { } setContent { - CapodTheme { + val themeState by generalSettings.themeState.collectAsState(initial = generalSettings.currentThemeState) + CapodTheme(state = themeState) { val state by waitForState(vm.state) state?.let { currentState -> WidgetConfigurationScreen( diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt b/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt index ec817cd2..9bf8a824 100644 --- a/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt +++ b/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpWindow.kt @@ -19,6 +19,7 @@ import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.theming.CapodTheme +import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.pods.core.PodDevice import javax.inject.Inject import javax.inject.Singleton @@ -26,6 +27,7 @@ import javax.inject.Singleton @Singleton class PopUpWindow @Inject constructor( @ApplicationContext private val appContext: Context, + private val generalSettings: GeneralSettings, ) { private val windowManager = appContext.getSystemService(WINDOW_SERVICE) as WindowManager @@ -67,7 +69,7 @@ class PopUpWindow @Inject constructor( setViewTreeLifecycleOwner(owner) setViewTreeSavedStateRegistryOwner(owner) setContent { - CapodTheme { + CapodTheme(state = generalSettings.currentThemeState) { PopUpContent(device = device, onClose = { close() }) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4878668c..907693ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -271,6 +271,29 @@ Expected format: %1$s Profile order determines priority. Drag profiles to reorder them - profiles higher in the list take precedence when multiple devices match. + + Theme mode + System + Dark + Light + + + Theme style + Default + Material You + Medium contrast + High contrast + + + Theme color + Blue + Green + Amber + Uses wallpaper colors + + + Appearance + Unsaved Changes You have unsaved changes. What would you like to do? diff --git a/app/src/test/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiTest.kt b/app/src/test/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiTest.kt index d027baf5..e7edff8f 100644 --- a/app/src/test/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiTest.kt +++ b/app/src/test/java/eu/darken/capod/common/preferences/FlowPreferenceMoshiTest.kt @@ -1,9 +1,12 @@ package eu.darken.capod.common.preferences import com.squareup.moshi.JsonClass +import com.squareup.moshi.JsonDataException import com.squareup.moshi.Moshi +import eu.darken.capod.common.theming.ThemeMode import eu.darken.capod.main.core.MonitorMode import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.assertThrows import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -121,4 +124,66 @@ class FlowPreferenceMoshiTest : BaseTest() { monitorMode.update { MonitorMode.MANUAL } monitorMode.value shouldBe MonitorMode.MANUAL } + + @Test + fun `bad enum value throws without fallback`() = runTest { + val moshi = Moshi.Builder().build() + mockPreferences.edit().putString("theme.mode", "\"theme.mode.bogus\"").apply() + + assertThrows { + mockPreferences.createFlowPreference( + key = "theme.mode", + defaultValue = ThemeMode.SYSTEM, + moshi = moshi, + onErrorFallbackToDefault = false, + ) + } + } + + @Test + fun `bad enum value returns default with fallback`() = runTest { + val moshi = Moshi.Builder().build() + mockPreferences.edit().putString("theme.mode", "\"theme.mode.bogus\"").apply() + + val pref = mockPreferences.createFlowPreference( + key = "theme.mode", + defaultValue = ThemeMode.SYSTEM, + moshi = moshi, + onErrorFallbackToDefault = true, + ) + + pref.value shouldBe ThemeMode.SYSTEM + pref.flow.first() shouldBe ThemeMode.SYSTEM + } + + @Test + fun `corrupt json returns default with fallback`() = runTest { + val moshi = Moshi.Builder().build() + mockPreferences.edit().putString("theme.mode", "not-json-at-all").apply() + + val pref = mockPreferences.createFlowPreference( + key = "theme.mode", + defaultValue = ThemeMode.DARK, + moshi = moshi, + onErrorFallbackToDefault = true, + ) + + pref.value shouldBe ThemeMode.DARK + } + + @Test + fun `valid enum roundtrips with fallback enabled`() = runTest { + val moshi = Moshi.Builder().build() + val pref = mockPreferences.createFlowPreference( + key = "theme.mode", + defaultValue = ThemeMode.SYSTEM, + moshi = moshi, + onErrorFallbackToDefault = true, + ) + + pref.value shouldBe ThemeMode.SYSTEM + pref.update { ThemeMode.DARK } + pref.value shouldBe ThemeMode.DARK + pref.flow.first() shouldBe ThemeMode.DARK + } }