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
@@ -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
}
}