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()
|
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) }
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue