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:
parent
0a713879b9
commit
3e0e3b50cb
2 changed files with 131 additions and 58 deletions
|
|
@ -10,15 +10,23 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
|
@ -45,8 +53,9 @@ fun EngineScreen(state: DesktopState) {
|
||||||
val error by state.engineError.collectAsState()
|
val error by state.engineError.collectAsState()
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
ShonarWordmark()
|
||||||
Text("Starting the transcription engine…",
|
Text("Starting the transcription engine…",
|
||||||
style = MaterialTheme.typography.headlineSmall)
|
style = MaterialTheme.typography.titleMedium)
|
||||||
Text(
|
Text(
|
||||||
"SHONAR transcribes on this computer with local models — nothing " +
|
"SHONAR transcribes on this computer with local models — nothing " +
|
||||||
"is uploaded anywhere. The engine runs in the background while " +
|
"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)) {
|
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Recordings", style = MaterialTheme.typography.headlineSmall,
|
ShonarWordmark(modifier = Modifier.weight(1f))
|
||||||
modifier = Modifier.weight(1f))
|
TextButton({ state.go(DesktopState.Screen.SETTINGS) }) {
|
||||||
TextButton({ state.go(DesktopState.Screen.SETTINGS) }) { Text("Settings") }
|
Icon(Icons.Filled.Settings, contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(Modifier.width(6.dp))
|
||||||
|
Text("Settings")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
Card(Modifier.fillMaxWidth()) {
|
Card(Modifier.fillMaxWidth()) {
|
||||||
|
|
@ -249,18 +262,33 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (e.status == FileStatus.NEW || e.status == FileStatus.FAILED) {
|
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 &&
|
if (e.status != FileStatus.RUNNING &&
|
||||||
e.status != FileStatus.QUEUED) {
|
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 &&
|
if (e.status != FileStatus.RUNNING &&
|
||||||
e.status != FileStatus.QUEUED) {
|
e.status != FileStatus.QUEUED) {
|
||||||
TextButton(
|
TextButton(
|
||||||
{ deleting = e.file },
|
{ deleting = e.file },
|
||||||
content = {
|
content = {
|
||||||
|
Icon(Icons.Filled.Delete, contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("Delete",
|
Text("Delete",
|
||||||
color = MaterialTheme.colorScheme.error)
|
color = MaterialTheme.colorScheme.error)
|
||||||
},
|
},
|
||||||
|
|
@ -411,11 +439,21 @@ fun DetailScreen(state: DesktopState) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,
|
Row(verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
if (isPlaying) {
|
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,
|
Text("playing…", style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.primary)
|
color = MaterialTheme.colorScheme.primary)
|
||||||
} else {
|
} 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 {
|
playError?.let {
|
||||||
Text(it, color = MaterialTheme.colorScheme.error,
|
Text(it, color = MaterialTheme.colorScheme.error,
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,104 @@
|
||||||
package com.shonar.desktop
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
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.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
|
* SHONAR Desktop theme — the Android companion's "deep-sea sonar" palette,
|
||||||
* with an olive-gold accent, inspired by the Skills Hub hero design.
|
* copied exactly from android ui/theme/Theme.kt dark scheme: dark navy +
|
||||||
* Desktop-only for now (Android keeps its own ui/theme/Theme.kt).
|
* 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:
|
* 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.
|
* onSurfaceVariant with a primary progress bar.
|
||||||
*/
|
*/
|
||||||
private val ShonarGold = Color(0xFFE0B23A) // text-safe gold (brighter for AA on cards)
|
private val Teal = Color(0xFF4FD1C5)
|
||||||
private val ShonarGoldBright = Color(0xFFF0C419)
|
private val TealDark = Color(0xFF2A9D94)
|
||||||
private val GoldInk = Color(0xFF1F1800) // text on gold fills (AA)
|
private val DeepNavy = Color(0xFF0B1220)
|
||||||
private val GraphiteBg = Color(0xFF1A1A1A)
|
private val NavySurface = Color(0xFF121C2E)
|
||||||
private val GraphiteSurface = Color(0xFF222222)
|
private val Ink = Color(0xFF10141A)
|
||||||
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 ShonarDarkScheme = darkColorScheme(
|
private val ShonarDarkScheme = darkColorScheme(
|
||||||
primary = ShonarGold,
|
primary = Teal,
|
||||||
onPrimary = GoldInk,
|
onPrimary = Ink,
|
||||||
primaryContainer = Color(0xFF3A3216),
|
secondary = TealDark,
|
||||||
onPrimaryContainer = ShonarGoldBright,
|
background = DeepNavy,
|
||||||
|
surface = NavySurface,
|
||||||
secondary = Color(0xFFB99B5E),
|
onBackground = Color(0xFFE6EDF3),
|
||||||
onSecondary = GoldInk,
|
onSurface = Color(0xFFE6EDF3),
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Wrap the app content in the SHONAR dark-gold theme. */
|
/** Wrap the app content in the SHONAR dark sonar theme. */
|
||||||
@Composable
|
@Composable
|
||||||
fun ShonarTheme(content: @Composable () -> Unit) {
|
fun ShonarTheme(content: @Composable () -> Unit) {
|
||||||
MaterialTheme(colorScheme = ShonarDarkScheme, content = content)
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue