From 5852101a371269804bd4e72be6c65b92e45f04b0 Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 21 Sep 2026 12:57:12 -0500 Subject: [PATCH] Revert "Shell: persistent sidebar (views, engine status, theme) + library top bar with search/sort/batch; responsive rail and drawer modes; Ctrl+F search" This reverts commit ceb3be3dda4a5eaed5724419f39ceaf61037d894. --- .../kotlin/com/shonar/desktop/Components.kt | 4 +- .../main/kotlin/com/shonar/desktop/Main.kt | 58 +--- .../main/kotlin/com/shonar/desktop/Screens.kt | 65 ++++ .../main/kotlin/com/shonar/desktop/Shell.kt | 301 ------------------ 4 files changed, 67 insertions(+), 361 deletions(-) delete mode 100644 app/src/main/kotlin/com/shonar/desktop/Shell.kt diff --git a/app/src/main/kotlin/com/shonar/desktop/Components.kt b/app/src/main/kotlin/com/shonar/desktop/Components.kt index 4a991d9..321cb2b 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Components.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Components.kt @@ -248,8 +248,6 @@ fun SonarField( leadingIcon: (@Composable () -> Unit)? = null, imeAction: ImeAction = ImeAction.Done, onDone: () -> Unit = {}, - /** Extra modifier for the text input itself (focus requester). */ - inputModifier: Modifier = Modifier, ) { var isFocused by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) @@ -280,7 +278,7 @@ fun SonarField( cursorBrush = SolidColor(Shonar.Accent), keyboardOptions = KeyboardOptions(imeAction = imeAction), keyboardActions = KeyboardActions(onDone = { onDone() }), - modifier = inputModifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), decorationBox = { inner -> if (value.isEmpty()) { Text(placeholder, diff --git a/app/src/main/kotlin/com/shonar/desktop/Main.kt b/app/src/main/kotlin/com/shonar/desktop/Main.kt index cdafecd..c26fd56 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Main.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Main.kt @@ -20,9 +20,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.ui.graphics.Color import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key @@ -102,7 +99,6 @@ fun main() = application { ) { val baseDensity = LocalDensity.current val themeMode by state.themeMode.collectAsState() - val searchFocus = androidx.compose.ui.focus.FocusRequester() ShonarTheme(mode = themeMode) { Surface( Modifier.fillMaxSize().onPreviewKeyEvent { e -> @@ -129,11 +125,6 @@ fun main() = application { } else false } else if (!e.isCtrlPressed) false else when (e.key) { - Key.F -> { - if (screen == DesktopState.Screen.LIBRARY) { - searchFocus.requestFocus(); true - } else false - } Key.Equals, Key.Plus, Key.NumPadAdd, Key.DirectionUp -> { zoom = (zoom * 1.1f).coerceAtMost(3f); true } @@ -154,35 +145,7 @@ fun main() = application { CompositionLocalProvider( LocalDensity provides ScaledDensity(baseDensity, zoom), ) { - androidx.compose.foundation.layout.BoxWithConstraints( - Modifier.fillMaxSize(), - ) { - val wide = maxWidth >= 1100.dp - val medium = maxWidth >= 760.dp - var drawerOpen by androidx.compose.runtime.remember { - mutableStateOf(false) - } - Row(Modifier.fillMaxSize().padding( - top = Shonar.Space4, bottom = Shonar.Space4, - start = 0.dp, end = Shonar.Space4)) { - if (medium) { - ShonarSidebar( - state = state, - collapsed = !wide, - ) - Spacer(Modifier.width(Shonar.Space4)) - } - Column(Modifier.fillMaxSize().weight(1f)) { - if (screen == DesktopState.Screen.LIBRARY) { - LibraryTopBar( - state = state, - showMenuButton = !medium, - onMenu = { drawerOpen = true }, - searchFocus = searchFocus, - ) - Spacer(Modifier.height(Shonar.Space3)) - } - + Box(Modifier.fillMaxSize().padding(20.dp)) { // Undo-delete toast sits above Library AND Detail // (a delete from Detail lands back on Library, // but the toast must appear either way). @@ -231,25 +194,6 @@ fun main() = application { DesktopState.Screen.SETTINGS -> SettingsScreen(state) } } - } // content column - } // shell row - // Narrow screens: sidebar as an overlay drawer. - if (!medium && drawerOpen) { - androidx.compose.foundation.layout.Box( - Modifier.fillMaxSize() - .background(Color(0x99000000)) - .clickable { drawerOpen = false }, - ) { - ShonarSidebar( - state = state, - collapsed = false, - onNavigate = { drawerOpen = false }, - modifier = Modifier - .align(Alignment.CenterStart) - .padding(Shonar.Space2), - ) - } - } } } } diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index c5e2e02..ce06b7d 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -218,6 +218,15 @@ fun LibraryScreen(state: DesktopState) { val selected by state.selected.collectAsState() Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(Shonar.Space3)) { + Row(verticalAlignment = Alignment.CenterVertically) { + ShonarWordmark(modifier = Modifier.weight(1f)) + ShonarGhostButton({ state.go(DesktopState.Screen.SETTINGS) }) { + Icon(Icons.Filled.Settings, contentDescription = "Settings", + modifier = Modifier.size(Shonar.IconSm)) + Spacer(Modifier.width(Shonar.Space2)) + Text("Settings") + } + } if (!connected) { SonarCard(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth().padding(Shonar.CardPadding), @@ -230,6 +239,25 @@ fun LibraryScreen(state: DesktopState) { } } } + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(Modifier.weight(1f)) + Row(verticalAlignment = Alignment.CenterVertically) { + val auto = state.autoTranscribe.collectAsState().value + Checkbox(auto, { state.setAutoTranscribe(it) }) + Text("Auto-transcribe new files", + style = MaterialTheme.typography.bodySmall) + } + val batch = state.batch.collectAsState().value + val untranscribed = entries.count { + !it.hasReport && it.status != FileStatus.QUEUED && + it.status != FileStatus.RUNNING + } + if (batch == null && untranscribed > 0) { + ShonarOutlinedButton({ state.transcribeAll() }) { + Text("Transcribe all ($untranscribed)") + } + } + } // Batch progress: settled count + current file's own fraction. val batch = state.batch.collectAsState().value if (batch != null) { @@ -277,6 +305,43 @@ fun LibraryScreen(state: DesktopState) { } } } + SonarField(query, { state.setQuery(it) }, + placeholder = "Search saved transcripts…", + singleLine = true, modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon(Icons.Filled.Search, contentDescription = null, + tint = Shonar.TextSecondary, + modifier = Modifier.size(Shonar.IconSm)) + }) + // Sort order (persisted): name A–Z, newest first, oldest first. + // Date modes also surface the file date on each row below. + if (folder != null && query.isBlank()) { + Row(verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { + Text("Sort", style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant) + val order = state.sortOrder.collectAsState().value + listOf("name" to "Name", "new" to "Newest", + "old" to "Oldest").forEach { (value, label) -> + PillChip( + selected = order == value, + onClick = { state.setSortOrder(value) }, + label = label, + ) + } + Spacer(Modifier.width(Shonar.Space3)) + Text("Show", style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant) + listOf("all" to "All", "new" to "Untranscribed") + .forEach { (value, label) -> + PillChip( + selected = statusFilter == value, + onClick = { state.setLibraryFilter(value) }, + label = label, + ) + } + } + } if (selected.isNotEmpty() && query.isBlank()) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { diff --git a/app/src/main/kotlin/com/shonar/desktop/Shell.kt b/app/src/main/kotlin/com/shonar/desktop/Shell.kt deleted file mode 100644 index b6120cb..0000000 --- a/app/src/main/kotlin/com/shonar/desktop/Shell.kt +++ /dev/null @@ -1,301 +0,0 @@ -package com.shonar.desktop - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Folder -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.Mic -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.WbSunny -import androidx.compose.material.icons.filled.Nightlight -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.focus.focusRequester -import androidx.compose.material.icons.filled.Search -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp - -/** - * The workspace shell: a persistent sidebar (library views, engine - * status, settings, theme) plus a context top bar (search, sort, - * batch actions) above the content region. - * - * Responsive: - * - ≥1100dp: full sidebar (labels + counts) - * - 760–1100dp: icon rail (labels move to accessibility names) - * - <760dp: sidebar hides; a menu button in the top bar opens it as - * an overlay drawer over the content. - * - * The sidebar's library views ARE the session filter lens (state. - * libraryFilter) — never persisted, so a stale lens can't hide files - * at startup. Detail / Engine / Settings keep their own headers; the - * top bar only carries library chrome. - */ - -/** Library views offered by the sidebar; value = libraryFilter. */ -private val LIBRARY_VIEWS = listOf( - "all" to "All recordings", - "new" to "Needs transcription", - "failed" to "Failed", - "done" to "Ready", -) - -@Composable -fun ShonarSidebar( - state: DesktopState, - collapsed: Boolean, // icon rail mode - onNavigate: () -> Unit = {}, // closes the drawer on narrow screens - modifier: Modifier = Modifier, -) { - val screen by state.screen.collectAsState() - val filter by state.libraryFilter.collectAsState() - val connected by state.connected.collectAsState() - val allEntries by state.entries.collectAsState() - - val counts = mapOf( - "all" to allEntries.size, - "new" to allEntries.count { - !it.hasReport && it.status != FileStatus.QUEUED && - it.status != FileStatus.RUNNING && it.status != FileStatus.DONE - }, - "failed" to allEntries.count { it.status == FileStatus.FAILED }, - "done" to allEntries.count { - it.hasReport || it.status == FileStatus.DONE - }, - ) - - Surface( - modifier = modifier.fillMaxHeight() - .width(if (collapsed) Shonar.RailW else Shonar.SidebarW), - color = Shonar.SurfaceAlt, - ) { - Column(Modifier.fillMaxSize().padding(vertical = Shonar.Space3)) { - // Brand block: logo tile (+ wordmark when expanded). - if (collapsed) { - Box(Modifier.padding(horizontal = Shonar.Space3) - .padding(bottom = Shonar.Space3)) { - SonarLogo(size = Shonar.ControlH) - } - } else { - Row( - Modifier.padding(horizontal = Shonar.Space3) - .padding(bottom = Shonar.Space3), - verticalAlignment = Alignment.CenterVertically, - ) { - SonarLogo(size = 26.dp) - Spacer(Modifier.width(Shonar.Space2)) - Text("SHONAR", - style = MaterialTheme.typography.titleSmall, - color = Shonar.TextPrimary) - } - } - // Library views — the nav core. - LIBRARY_VIEWS.forEach { (value, label) -> - val selected = screen == DesktopState.Screen.LIBRARY && - filter == value - NavRow( - selected = selected, - onClick = { - state.setLibraryFilter(value) - state.go(DesktopState.Screen.LIBRARY) - onNavigate() - }, - label = label, - modifier = Modifier.padding( - horizontal = Shonar.Space2), - icon = { - val (img, name) = when (value) { - "new" -> Icons.Filled.Mic to "mic" - "failed" -> Icons.Filled.Error to "error" - "done" -> Icons.Filled.CheckCircle to "check" - else -> Icons.Filled.Folder to "folder" - } - Icon(img, - contentDescription = if (collapsed) label else null, - tint = if (selected) Shonar.Accent - else Shonar.TextMuted, - modifier = Modifier.size(Shonar.IconSm)) - }, - count = if (collapsed) null else counts[value], - ) - } - Spacer(Modifier.height(Shonar.Space3)) - // Engine status: live dot + label; click opens the engine - // screen (start/stop/logs). Honest state, always visible. - NavRow( - selected = screen == DesktopState.Screen.ENGINE, - onClick = { - state.go(DesktopState.Screen.ENGINE); onNavigate() - }, - label = if (collapsed) "Engine" else - if (connected) "Engine · running" else "Engine · stopped", - modifier = Modifier.padding(horizontal = Shonar.Space2), - icon = { - StatusDot( - color = if (connected) Shonar.Accent - else Shonar.Error, - modifier = Modifier.padding( - horizontal = (Shonar.IconSm - 8.dp) / 2), - ) - }, - ) - Spacer(Modifier.weight(1f)) - // Settings + theme toggle pinned at the bottom. - NavRow( - selected = screen == DesktopState.Screen.SETTINGS, - onClick = { - state.go(DesktopState.Screen.SETTINGS); onNavigate() - }, - label = "Settings", - modifier = Modifier.padding(horizontal = Shonar.Space2), - icon = { - Icon(Icons.Filled.Settings, - contentDescription = - if (collapsed) "Settings" else null, - tint = if (screen == DesktopState.Screen.SETTINGS) - Shonar.Accent else Shonar.TextMuted, - modifier = Modifier.size(Shonar.IconSm)) - }, - ) - Row( - Modifier.fillMaxWidth() - .padding(horizontal = Shonar.Space2) - .padding(top = Shonar.Space1), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (collapsed) - Arrangement.Center else Arrangement.Start, - ) { - val mode = state.themeMode.collectAsState().value - ShonarIconButton( - onClick = { - state.setThemeMode( - if (mode == ThemeMode.DARK) ThemeMode.LIGHT - else ThemeMode.DARK) - }, - label = if (mode == ThemeMode.DARK) - "Switch to light theme" else "Switch to dark theme", - ) { - Icon( - if (mode == ThemeMode.DARK) Icons.Filled.WbSunny - else Icons.Filled.Nightlight, - contentDescription = null, - tint = Shonar.TextSecondary, - modifier = Modifier.size(Shonar.IconSm)) - } - } - } - } -} - -/** Library toolbar: search, sort, batch action, status meta. Only - * drawn over the LIBRARY screen; Detail/Engine/Settings carry their - * own headers. [searchFocus] lets Ctrl+F jump into the field. */ -@Composable -fun LibraryTopBar( - state: DesktopState, - showMenuButton: Boolean, - onMenu: () -> Unit, - searchFocus: FocusRequester, - modifier: Modifier = Modifier, -) { - val query by state.query.collectAsState() - val connected by state.connected.collectAsState() - Column(modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(Shonar.Space2)) { - Row(verticalAlignment = Alignment.CenterVertically) { - if (showMenuButton) { - ShonarIconButton(onClick = onMenu, label = "Open navigation") { - Icon(Icons.Filled.Menu, contentDescription = null, - tint = Shonar.TextSecondary, - modifier = Modifier.size(Shonar.IconSm)) - } - Spacer(Modifier.width(Shonar.Space2)) - } - SonarField(query, { state.setQuery(it) }, - placeholder = "Search saved transcripts…", - singleLine = true, - modifier = Modifier.fillMaxWidth().weight(1f), - inputModifier = Modifier.focusRequester(searchFocus), - leadingIcon = { - Icon(Icons.Filled.Search, contentDescription = null, - tint = Shonar.TextSecondary, - modifier = Modifier.size(Shonar.IconSm)) - }) - Spacer(Modifier.width(Shonar.Space2)) - ShonarIconButton( - onClick = { - state.go(DesktopState.Screen.SETTINGS) - }, - label = "Settings", - ) { - Icon(Icons.Filled.Settings, contentDescription = null, - tint = Shonar.TextSecondary, - modifier = Modifier.size(Shonar.IconSm)) - } - } - // Sort + auto-transcribe + batch: second row, dense. - val batch = state.batch.collectAsState().value - val allEntries by state.entries.collectAsState() - val remaining = allEntries.count { - !it.hasReport && it.status != FileStatus.QUEUED && - it.status != FileStatus.RUNNING - } - Row(verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) { - Text("Sort", style = MaterialTheme.typography.labelMedium, - color = Shonar.TextSecondary) - val order = state.sortOrder.collectAsState().value - listOf("name" to "Name", "new" to "Newest", - "old" to "Oldest").forEach { (value, label) -> - PillChip( - selected = order == value, - onClick = { state.setSortOrder(value) }, - label = label, - ) - } - Spacer(Modifier.weight(1f)) - Row(verticalAlignment = Alignment.CenterVertically) { - val auto = state.autoTranscribe.collectAsState().value - Checkbox(auto, { state.setAutoTranscribe(it) }, - modifier = Modifier.size(28.dp)) - Text("Auto-transcribe", - style = MaterialTheme.typography.bodySmall, - color = Shonar.TextSecondary) - } - if (batch == null && remaining > 0 && connected) { - ShonarOutlinedButton({ state.transcribeAll() }) { - Text("Transcribe all ($remaining)") - } - } - } - } -} - -/** Page title row for non-library screens is owned by those screens; - * the shell adds only the sidebar divider hairline via Surface. */