From 5526699103eb437def35160d5da96c165f29e1f3 Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 21 Sep 2026 18:28:19 -0500 Subject: [PATCH] =?UTF-8?q?Theme:=20default=20is=20now=20'Sonar'=20?= =?UTF-8?q?=E2=80=94=20Android=20companion's=20palette=20(DeepNavy=20070D1?= =?UTF-8?q?8,=20NavySurface,=20coral=20E76F51=20accent,=20flat=20bg)=20so?= =?UTF-8?q?=20desktop=20and=20phone=20read=20as=20one=20product;=20previou?= =?UTF-8?q?s=20charcoal/blue=20look=20kept=20as=20optional=20'Meridian';?= =?UTF-8?q?=20Settings=20pills=20Sonar/Meridian/Light;=20legacy=20'dark'?= =?UTF-8?q?=20pref=20loads=20as=20Sonar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/shonar/desktop/DesktopState.kt | 21 ++++-- .../main/kotlin/com/shonar/desktop/Screens.kt | 15 ++-- .../main/kotlin/com/shonar/desktop/Theme.kt | 74 +++++++++++++++++-- 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt index 213bd51..5361cef 100644 --- a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt +++ b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt @@ -151,14 +151,18 @@ class DesktopState(private val appDir: File = defaultAppDir()) { val libraryFilter: StateFlow = _libraryFilter.asStateFlow() fun setLibraryFilter(f: String) { _libraryFilter.value = f } - /** UI theme: dark is the signature mode; light is opt-in and - * persisted (an appearance choice, unlike the filter lens). */ - private val _themeMode = MutableStateFlow(ThemeMode.DARK) + /** UI theme: Sonar (the Android companion's palette) is the default; + * Meridian and Light are opt-in and persisted. */ + private val _themeMode = MutableStateFlow(ThemeMode.SONAR) val themeMode: StateFlow = _themeMode.asStateFlow() fun setThemeMode(mode: ThemeMode) { _themeMode.value = mode 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 } ?.let { _sortOrder.value = it } _summarizer.value = prefs.getString(KEY_SUMMARIZER) ?: "auto" - _themeMode.value = if (prefs.getString(KEY_THEME) == "light") - ThemeMode.LIGHT else ThemeMode.DARK + _themeMode.value = when (prefs.getString(KEY_THEME)) { + // 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() prefs.getString(KEY_FOLDER)?.let { File(it).takeIf { it.isDirectory } } ?.let { setFolder(it, silent = true) } diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index 5997322..67182b5 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -1306,15 +1306,20 @@ fun SettingsScreen(state: DesktopState) { Row(horizontalArrangement = Arrangement.spacedBy(Shonar.Space2), verticalAlignment = Alignment.CenterVertically) { val mode = state.themeMode.collectAsState().value - PillChip(selected = mode == ThemeMode.DARK, - onClick = { state.setThemeMode(ThemeMode.DARK) }, - label = "Dark") + PillChip(selected = mode == ThemeMode.SONAR, + onClick = { state.setThemeMode(ThemeMode.SONAR) }, + label = "Sonar") + PillChip(selected = mode == ThemeMode.MERIDIAN, + onClick = { state.setThemeMode(ThemeMode.MERIDIAN) }, + label = "Meridian") PillChip(selected = mode == ThemeMode.LIGHT, onClick = { state.setThemeMode(ThemeMode.LIGHT) }, label = "Light") } - HelperText("Dark is the app's native look; light mirrors the same " + - "hierarchy on paper tones. Applied instantly.") + HelperText("Sonar is the Android companion app's palette (navy + " + + "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") Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { diff --git a/app/src/main/kotlin/com/shonar/desktop/Theme.kt b/app/src/main/kotlin/com/shonar/desktop/Theme.kt index a6dc9c0..809ffcd 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Theme.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Theme.kt @@ -125,14 +125,43 @@ private val LightPalette = ShonarPalette( highlightTint = Color(0x1AC2185B), // faint rose row wash ) -/** Which palette the app renders in. Persisted as "dark" / "light". */ -enum class ThemeMode { DARK, LIGHT } +/** The Android companion app's palette (android/.../ui/theme/Theme.kt): + * 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 { // ---- palette routing ---- /** Active palette; [ShonarTheme] provides the mode's instance. - * Defaults to dark so previews/strays get the signature look. */ - val LocalPalette = staticCompositionLocalOf { DarkPalette } + * Defaults to Sonar so previews/strays match the Android default. */ + val LocalPalette = staticCompositionLocalOf { SonarPalette } // ---- color tokens (theme-aware: readable inside any composable) ---- 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 * reading MaterialTheme.colorScheme follows the active theme. */ -private fun schemeFor(p: ShonarPalette) = - if (p === DarkPalette) ShonarDarkScheme else ShonarLightScheme +private fun schemeFor(p: ShonarPalette) = when { + 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( primary = DarkPalette.accent, @@ -309,10 +363,14 @@ private val ShonarLightScheme = lightColorScheme( * (dark is the signature default). */ @Composable fun ShonarTheme( - mode: ThemeMode = ThemeMode.DARK, + mode: ThemeMode = ThemeMode.SONAR, 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 // with no color falls back to Compose's default BLACK, which is // invisible on the dark canvas (regression seen Sep 21). Call sites