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:
parent
7cc3737f92
commit
5526699103
3 changed files with 91 additions and 19 deletions
|
|
@ -151,14 +151,18 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
|
|||
val libraryFilter: StateFlow<String> = _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> = _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) }
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue