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.
This commit is contained in:
darken
2026-02-24 18:33:33 +01:00
committed by Matthias Urhahn
parent 2ca8f686c8
commit 06f4115808
20 changed files with 1530 additions and 93 deletions
@@ -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()
}
@@ -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 <reified T> 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 <reified T : Any?> 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)
)
)
@@ -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<Color, Color> = when (color) {
ThemeColor.BLUE -> CapodColorsBlue.primaryLight to CapodColorsBlue.primaryDark
ThemeColor.GREEN -> CapodColorsGreen.primaryLight to CapodColorsGreen.primaryDark
ThemeColor.AMBER -> CapodColorsAmber.primaryLight to CapodColorsAmber.primaryDark
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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(
@@ -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),
}
@@ -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,
)
}
}
}
@@ -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),
}
@@ -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,
)
@@ -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),
}
@@ -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<ThemeState>
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,
)
}
}
@@ -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,
@@ -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 = {},
@@ -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<Any>(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<Any>(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)
}
@@ -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(
@@ -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() })
}
}
+23
View File
@@ -271,6 +271,29 @@
<string name="profiles_key_expected_format">Expected format: %1$s</string>
<string name="profiles_priority_hint">Profile order determines priority. Drag profiles to reorder them - profiles higher in the list take precedence when multiple devices match.</string>
<!-- Theme Mode -->
<string name="ui_theme_mode_label">Theme mode</string>
<string name="ui_theme_mode_system_label">System</string>
<string name="ui_theme_mode_dark_label">Dark</string>
<string name="ui_theme_mode_light_label">Light</string>
<!-- Theme Style -->
<string name="ui_theme_style_label">Theme style</string>
<string name="ui_theme_style_default_label">Default</string>
<string name="ui_theme_style_materialyou_label">Material You</string>
<string name="ui_theme_style_medium_contrast_label">Medium contrast</string>
<string name="ui_theme_style_high_contrast_label">High contrast</string>
<!-- Theme Color -->
<string name="ui_theme_color_label">Theme color</string>
<string name="ui_theme_color_blue_label">Blue</string>
<string name="ui_theme_color_green_label">Green</string>
<string name="ui_theme_color_amber_label">Amber</string>
<string name="ui_theme_color_disabled_materialyou">Uses wallpaper colors</string>
<!-- Settings categories -->
<string name="settings_category_appearance_label">Appearance</string>
<!-- Unsaved changes dialog -->
<string name="general_unsaved_changes_title">Unsaved Changes</string>
<string name="general_unsaved_changes_message">You have unsaved changes. What would you like to do?</string>
@@ -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<JsonDataException> {
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
}
}