From 3e0e3b50cb2e6e76a432156da2b70d83dc09f4fa Mon Sep 17 00:00:00 2001 From: avi Date: Tue, 15 Sep 2026 06:26:16 -0500 Subject: [PATCH] Desktop: adopt the Android app's companion design (navy+teal sonar skin) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theme.kt now copies the Android ui/theme dark scheme verbatim: deep navy #0B1220 + navy surfaces + teal ping #4FD1C5 — replaces warm-graphite gold. Headers carry the identity: SonarLogo (same geometry as the Android launcher's ic_launcher_foreground: center dot, teal upper-right arcs, teal-dark lower-left) + S.H.O.N.A.R. wordmark + 'Self-hosted notes and audio recording' tagline. Action buttons use the phone's icon vocabulary (Filled PlayArrow/Edit/Settings/Stop/Delete). Row 'Open' button removed — clicking the card already opens the recording. Verified on-screen: navy+teal render, wordmark + tagline, icons, Open gone. Ctrl+=/- zoom was already ported (4944fcd) and confirmed present. --- .../main/kotlin/com/shonar/desktop/Screens.kt | 56 ++++++-- .../main/kotlin/com/shonar/desktop/Theme.kt | 133 +++++++++++------- 2 files changed, 131 insertions(+), 58 deletions(-) diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index 92f3a37..160e59e 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -10,15 +10,23 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -45,8 +53,9 @@ fun EngineScreen(state: DesktopState) { val error by state.engineError.collectAsState() Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) { + ShonarWordmark() Text("Starting the transcription engine…", - style = MaterialTheme.typography.headlineSmall) + style = MaterialTheme.typography.titleMedium) Text( "SHONAR transcribes on this computer with local models — nothing " + "is uploaded anywhere. The engine runs in the background while " + @@ -102,9 +111,13 @@ fun LibraryScreen(state: DesktopState) { Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Text("Recordings", style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.weight(1f)) - TextButton({ state.go(DesktopState.Screen.SETTINGS) }) { Text("Settings") } + ShonarWordmark(modifier = Modifier.weight(1f)) + TextButton({ state.go(DesktopState.Screen.SETTINGS) }) { + Icon(Icons.Filled.Settings, contentDescription = null, + modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(6.dp)) + Text("Settings") + } } if (!connected) { Card(Modifier.fillMaxWidth()) { @@ -249,18 +262,33 @@ fun LibraryScreen(state: DesktopState) { } } if (e.status == FileStatus.NEW || e.status == FileStatus.FAILED) { - TextButton({ state.pumpFile(e.file) }) { Text("Transcribe") } + TextButton({ state.pumpFile(e.file) }) { + Icon(Icons.Filled.PlayArrow, contentDescription = null, + modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Transcribe") + } } if (e.status != FileStatus.RUNNING && e.status != FileStatus.QUEUED) { - TextButton({ renaming = e.file }) { Text("Rename") } + TextButton({ renaming = e.file }) { + Icon(Icons.Filled.Edit, contentDescription = null, + modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(4.dp)) + Text("Rename") + } } - TextButton({ state.openDetail(e.file) }) { Text("Open") } + // No "Open" button: clicking the card opens the + // recording (the button was redundant). if (e.status != FileStatus.RUNNING && e.status != FileStatus.QUEUED) { TextButton( { deleting = e.file }, content = { + Icon(Icons.Filled.Delete, contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.error) + Spacer(Modifier.width(4.dp)) Text("Delete", color = MaterialTheme.colorScheme.error) }, @@ -411,11 +439,21 @@ fun DetailScreen(state: DesktopState) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { if (isPlaying) { - Button({ state.stopPlayback() }) { Text("Stop") } + Button({ state.stopPlayback() }) { + Icon(Icons.Filled.Stop, contentDescription = null, + modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(6.dp)) + Text("Stop") + } Text("playing…", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary) } else { - OutlinedButton({ state.playFrom(detail.file) }) { Text("Play") } + OutlinedButton({ state.playFrom(detail.file) }) { + Icon(Icons.Filled.PlayArrow, contentDescription = null, + modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(6.dp)) + Text("Play") + } } playError?.let { Text(it, color = MaterialTheme.colorScheme.error, diff --git a/app/src/main/kotlin/com/shonar/desktop/Theme.kt b/app/src/main/kotlin/com/shonar/desktop/Theme.kt index 5c200a4..d58c945 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Theme.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Theme.kt @@ -1,69 +1,104 @@ package com.shonar.desktop +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp /** - * SHONAR Desktop theme — "warm graphite": near-black charcoal surfaces - * with an olive-gold accent, inspired by the Skills Hub hero design. - * Desktop-only for now (Android keeps its own ui/theme/Theme.kt). + * SHONAR Desktop theme — the Android companion's "deep-sea sonar" palette, + * copied exactly from android ui/theme/Theme.kt dark scheme: dark navy + + * teal ping. Same seven role overrides the phone app sets; everything else + * falls through to Material3 defaults, same as Android. * * Status semantics in the library list map to scheme roles: - * done -> primary (gold), failed -> error (red), queued/running -> + * done -> primary (teal), failed -> error (red), queued/running -> * onSurfaceVariant with a primary progress bar. */ -private val ShonarGold = Color(0xFFE0B23A) // text-safe gold (brighter for AA on cards) -private val ShonarGoldBright = Color(0xFFF0C419) -private val GoldInk = Color(0xFF1F1800) // text on gold fills (AA) -private val GraphiteBg = Color(0xFF1A1A1A) -private val GraphiteSurface = Color(0xFF222222) -private val GraphiteHigh = Color(0xFF2B2B2B) -private val InkPrimary = Color(0xFFF4F4F4) -private val InkSecondary = Color(0xFFB3B3B3) -private val InkMuted = Color(0xFF8A8A8A) -private val OutlineWarm = Color(0xFF3A3A3A) -private val DangerWarm = Color(0xFFE5544F) +private val Teal = Color(0xFF4FD1C5) +private val TealDark = Color(0xFF2A9D94) +private val DeepNavy = Color(0xFF0B1220) +private val NavySurface = Color(0xFF121C2E) +private val Ink = Color(0xFF10141A) private val ShonarDarkScheme = darkColorScheme( - primary = ShonarGold, - onPrimary = GoldInk, - primaryContainer = Color(0xFF3A3216), - onPrimaryContainer = ShonarGoldBright, - - secondary = Color(0xFFB99B5E), - onSecondary = GoldInk, - secondaryContainer = Color(0xFF332C1A), - onSecondaryContainer = Color(0xFFE8D9AE), - - tertiary = Color(0xFF8FA6B2), // cool counterpoint (info-ish) - onTertiary = Color(0xFF0E1A20), - tertiaryContainer = Color(0xFF2A3B45), - onTertiaryContainer = Color(0xFFCDE2EC), - - background = GraphiteBg, - onBackground = InkPrimary, - - surface = GraphiteSurface, - onSurface = InkPrimary, - surfaceVariant = Color(0xFF2E2A21), - onSurfaceVariant = InkSecondary, - surfaceContainer = GraphiteHigh, // cards lift off the bg - surfaceContainerHigh = Color(0xFF333333), - surfaceContainerHighest = Color(0xFF3A3A3A), - - outline = OutlineWarm, - outlineVariant = Color(0xFF2E2E2E), - - error = DangerWarm, - onError = Color(0xFF2B0A08), - errorContainer = Color(0xFF47201E), - onErrorContainer = Color(0xFFFFB4AF), + primary = Teal, + onPrimary = Ink, + secondary = TealDark, + background = DeepNavy, + surface = NavySurface, + onBackground = Color(0xFFE6EDF3), + onSurface = Color(0xFFE6EDF3), ) -/** Wrap the app content in the SHONAR dark-gold theme. */ +/** Wrap the app content in the SHONAR dark sonar theme. */ @Composable fun ShonarTheme(content: @Composable () -> Unit) { MaterialTheme(colorScheme = ShonarDarkScheme, content = content) } + +/** + * The SHONAR sonar-ping logo — same geometry as the Android launcher's + * ic_launcher_foreground.xml: a center dot with quarter arcs ringing out + * (teal upper-right, teal-dark lower-left), 108-unit viewport. + */ +@Composable +fun SonarLogo(size: Dp = 32.dp, modifier: Modifier = Modifier) { + Canvas(modifier = modifier.size(size)) { + val s = this.size.minDimension / 108f + fun c(x: Float, y: Float) = Offset(x * s, y * s) + // Center dot: r=6 at (54,54) + drawCircle(Teal, radius = 6f * s, center = c(54f, 54f)) + // Upper-right quarter arcs (teal), centered on the dot. + val arcStyle = Stroke(width = 4f * s, cap = StrokeCap.Round) + fun arc(color: Color, startDeg: Float, boxTopLeft: Float) { + val box = when (boxTopLeft) { 38f -> 32f; else -> 56f } + drawArc( + color, startAngle = startDeg, sweepAngle = 90f, useCenter = false, + topLeft = c(boxTopLeft, boxTopLeft), + size = androidx.compose.ui.geometry.Size(box * s, box * s), + style = arcStyle, + ) + } + arc(Teal, -90f, 38f) + arc(Teal, -90f, 26f) + // Lower-left quarter arcs (teal-dark). + arc(TealDark, 90f, 38f) + arc(TealDark, 90f, 26f) + } +} + +/** Wordmark row shared by screen headers: ping logo + S.H.O.N.A.R. + tagline. */ +@Composable +fun ShonarWordmark( + modifier: Modifier = Modifier, + logoSize: Dp = 28.dp, + style: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.headlineSmall, +) { + Row(modifier, verticalAlignment = Alignment.CenterVertically) { + SonarLogo(logoSize) + Spacer(Modifier.width(10.dp)) + Column { + Text("S.H.O.N.A.R.", style = style) + Text( + "Self-hosted notes and audio recording", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +}