Theme: default is now 'Sonar' — Android companion's palette (DeepNavy 070D18, NavySurface, coral E76F51 accent, flat bg) so desktop and phone read as one product; previous charcoal/blue look kept as optional 'Meridian'; Settings pills Sonar/Meridian/Light; legacy 'dark' pref loads as Sonar

This commit is contained in:
avi 2026-09-21 18:28:19 -05:00
commit 5526699103
3 changed files with 91 additions and 19 deletions

View file

@ -151,14 +151,18 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
val libraryFilter: StateFlow<String> = _libraryFilter.asStateFlow() val libraryFilter: StateFlow<String> = _libraryFilter.asStateFlow()
fun setLibraryFilter(f: String) { _libraryFilter.value = f } fun setLibraryFilter(f: String) { _libraryFilter.value = f }
/** UI theme: dark is the signature mode; light is opt-in and /** UI theme: Sonar (the Android companion's palette) is the default;
* persisted (an appearance choice, unlike the filter lens). */ * Meridian and Light are opt-in and persisted. */
private val _themeMode = MutableStateFlow(ThemeMode.DARK) private val _themeMode = MutableStateFlow(ThemeMode.SONAR)
val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow() val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow()
fun setThemeMode(mode: ThemeMode) { fun setThemeMode(mode: ThemeMode) {
_themeMode.value = mode _themeMode.value = mode
scope.launch { scope.launch {
prefs.putString(KEY_THEME, if (mode == ThemeMode.LIGHT) "light" else "dark") prefs.putString(KEY_THEME, when (mode) {
ThemeMode.LIGHT -> "light"
ThemeMode.MERIDIAN -> "meridian"
ThemeMode.SONAR -> "sonar"
})
} }
} }
@ -191,8 +195,13 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
prefs.getString(KEY_SORT)?.takeIf { it in SORT_ORDERS } prefs.getString(KEY_SORT)?.takeIf { it in SORT_ORDERS }
?.let { _sortOrder.value = it } ?.let { _sortOrder.value = it }
_summarizer.value = prefs.getString(KEY_SUMMARIZER) ?: "auto" _summarizer.value = prefs.getString(KEY_SUMMARIZER) ?: "auto"
_themeMode.value = if (prefs.getString(KEY_THEME) == "light") _themeMode.value = when (prefs.getString(KEY_THEME)) {
ThemeMode.LIGHT else ThemeMode.DARK // legacy "dark" (meridian-era default) now means Sonar;
// pick Meridian explicitly to get the old look back
"light" -> ThemeMode.LIGHT
"meridian" -> ThemeMode.MERIDIAN
else -> ThemeMode.SONAR
}
loadNotifyState() loadNotifyState()
prefs.getString(KEY_FOLDER)?.let { File(it).takeIf { it.isDirectory } } prefs.getString(KEY_FOLDER)?.let { File(it).takeIf { it.isDirectory } }
?.let { setFolder(it, silent = true) } ?.let { setFolder(it, silent = true) }

View file

@ -1306,15 +1306,20 @@ fun SettingsScreen(state: DesktopState) {
Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2), Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2),
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
val mode = state.themeMode.collectAsState().value val mode = state.themeMode.collectAsState().value
PillChip(selected = mode == ThemeMode.DARK, PillChip(selected = mode == ThemeMode.SONAR,
onClick = { state.setThemeMode(ThemeMode.DARK) }, onClick = { state.setThemeMode(ThemeMode.SONAR) },
label = "Dark") label = "Sonar")
PillChip(selected = mode == ThemeMode.MERIDIAN,
onClick = { state.setThemeMode(ThemeMode.MERIDIAN) },
label = "Meridian")
PillChip(selected = mode == ThemeMode.LIGHT, PillChip(selected = mode == ThemeMode.LIGHT,
onClick = { state.setThemeMode(ThemeMode.LIGHT) }, onClick = { state.setThemeMode(ThemeMode.LIGHT) },
label = "Light") label = "Light")
} }
HelperText("Dark is the app's native look; light mirrors the same " + HelperText("Sonar is the Android companion app's palette (navy + " +
"hierarchy on paper tones. Applied instantly.") "coral) — the default, so desktop and phone read as one " +
"product. Meridian is the charcoal/blue look; Light mirrors " +
"the same hierarchy on paper tones. Applied instantly.")
SectionHeader("Recordings folder") SectionHeader("Recordings folder")
Row(verticalAlignment = Alignment.CenterVertically, Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {

View file

@ -125,14 +125,43 @@ private val LightPalette = ShonarPalette(
highlightTint = Color(0x1AC2185B), // faint rose row wash highlightTint = Color(0x1AC2185B), // faint rose row wash
) )
/** Which palette the app renders in. Persisted as "dark" / "light". */ /** The Android companion app's palette (android/.../ui/theme/Theme.kt):
enum class ThemeMode { DARK, LIGHT } * deep-sea sonar navy + the phone's coral accent (#E76F51), flat
* background (no gradient). Values are 1:1 with the Android tokens:
* DeepNavy/NavySurface/NavyElevated/BorderDark/TextPrimary-Dark etc.
* Android derives ink-on-accent from luminance; coral (0.29) < 0.4 →
* white, matching the phone. Kept in sync by hand — if Android's
* palette changes, change this too. */
private val SonarPalette = ShonarPalette(
background = Color(0xFF070D18), // DeepNavy
surface = Color(0xFF0F1A2B), // NavySurface
surfaceAlt = Color(0xFF16233A), // NavyElevated
border = Color(0xFF22364F), // BorderDark
accent = Color(0xFFE76F51), // phone's accent_color setting
accentStrong = Color(0xFFC95B41), // filled-accent hover depth
accentGlow = Color(0x33E76F51), // soft halo (~20% coral)
inkOnAccent = Color(0xFFF4F8FF), // Android: white on coral
textPrimary = Color(0xFFE6EDF3),
textSecondary = Color(0xFF9FB3C8),
textMuted = Color(0xFF6B7E94),
error = Color(0xFFFF6B6B), // Android ErrorDark
onError = Color(0xFF10141A), // Android Ink
// Android's background is flat — no gradient.
highlight = Color(0xFFFF8A70), // warm coral-tint for title text
highlightTint = Color(0xFF3A1D16), // coral row wash on navy
)
/** Which palette the app renders in. Persisted as "sonar" / "meridian"
* / "light" (legacy "dark" loads as SONAR). SONAR mirrors the Android
* companion app's palette so the two read as one product; MERIDIAN is
* the charcoal/blue look from the Sep design pass, kept optional. */
enum class ThemeMode { SONAR, MERIDIAN, LIGHT }
object Shonar { object Shonar {
// ---- palette routing ---- // ---- palette routing ----
/** Active palette; [ShonarTheme] provides the mode's instance. /** Active palette; [ShonarTheme] provides the mode's instance.
* Defaults to dark so previews/strays get the signature look. */ * Defaults to Sonar so previews/strays match the Android default. */
val LocalPalette = staticCompositionLocalOf { DarkPalette } val LocalPalette = staticCompositionLocalOf { SonarPalette }
// ---- color tokens (theme-aware: readable inside any composable) ---- // ---- color tokens (theme-aware: readable inside any composable) ----
val Background: Color @Composable get() = LocalPalette.current.background val Background: Color @Composable get() = LocalPalette.current.background
@ -260,8 +289,33 @@ private val ShonarTypography = Typography(
/** Derive the full Material role set from a palette so anything still /** Derive the full Material role set from a palette so anything still
* reading MaterialTheme.colorScheme follows the active theme. */ * reading MaterialTheme.colorScheme follows the active theme. */
private fun schemeFor(p: ShonarPalette) = private fun schemeFor(p: ShonarPalette) = when {
if (p === DarkPalette) ShonarDarkScheme else ShonarLightScheme p === SonarPalette -> ShonarSonarScheme
p === DarkPalette -> ShonarDarkScheme
else -> ShonarLightScheme
}
/** Same slot map as ShonarDarkScheme, wired to the Sonar palette. */
private val ShonarSonarScheme = darkColorScheme(
primary = SonarPalette.accent,
onPrimary = SonarPalette.inkOnAccent,
primaryContainer = SonarPalette.accentGlow,
onPrimaryContainer = SonarPalette.accent,
secondary = SonarPalette.accent,
onSecondary = SonarPalette.inkOnAccent,
secondaryContainer = SonarPalette.surfaceAlt,
onSecondaryContainer = SonarPalette.textSecondary,
background = SonarPalette.background,
onBackground = SonarPalette.textPrimary,
surface = SonarPalette.surface,
onSurface = SonarPalette.textPrimary,
surfaceVariant = SonarPalette.surfaceAlt,
onSurfaceVariant = SonarPalette.textSecondary,
error = SonarPalette.error,
onError = SonarPalette.onError,
outline = SonarPalette.border,
outlineVariant = SonarPalette.border,
)
private val ShonarDarkScheme = darkColorScheme( private val ShonarDarkScheme = darkColorScheme(
primary = DarkPalette.accent, primary = DarkPalette.accent,
@ -309,10 +363,14 @@ private val ShonarLightScheme = lightColorScheme(
* (dark is the signature default). */ * (dark is the signature default). */
@Composable @Composable
fun ShonarTheme( fun ShonarTheme(
mode: ThemeMode = ThemeMode.DARK, mode: ThemeMode = ThemeMode.SONAR,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val palette = if (mode == ThemeMode.LIGHT) LightPalette else DarkPalette val palette = when (mode) {
ThemeMode.LIGHT -> LightPalette
ThemeMode.MERIDIAN -> DarkPalette
ThemeMode.SONAR -> SonarPalette
}
// Every typography tier carries explicit per-palette ink: a TextStyle // Every typography tier carries explicit per-palette ink: a TextStyle
// with no color falls back to Compose's default BLACK, which is // with no color falls back to Compose's default BLACK, which is
// invisible on the dark canvas (regression seen Sep 21). Call sites // invisible on the dark canvas (regression seen Sep 21). Call sites