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 ceb3be3dda.
This commit is contained in:
parent
ceb3be3dda
commit
5852101a37
4 changed files with 67 additions and 361 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
Loading…
Add table
Add a link
Reference in a new issue