Desktop: adopt the Android app's companion design (navy+teal sonar skin)

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.
This commit is contained in:
avi 2026-09-15 06:26:16 -05:00
commit 3e0e3b50cb
2 changed files with 131 additions and 58 deletions

View file

@ -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,

View file

@ -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,
)
}
}
}