Dashboard on the design system (redesign phase 3)
Library screen now uses the shared primitives: filled SonarField search with leading magnifier, PillChip sort/show filters (selected pill gets the teal glow), SonarCard rows with a "Your recordings — N of M" section header per the reference, ghost icon row-actions, and the quiet-red bulk Delete. Rename dialog rides the same field/buttons. All handlers, selection, filtering, and batch logic untouched.
This commit is contained in:
parent
5c0d1edbef
commit
a3d16a6c18
1 changed files with 58 additions and 55 deletions
|
|
@ -21,6 +21,7 @@ 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.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material3.AlertDialog
|
||||
|
|
@ -222,23 +223,23 @@ fun LibraryScreen(state: DesktopState) {
|
|||
val deleteWarning by state.deleteWarning.collectAsState()
|
||||
val selected by state.selected.collectAsState()
|
||||
|
||||
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(Shonar.Space3)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
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))
|
||||
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) {
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(12.dp),
|
||||
SonarCard(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(Shonar.CardPadding),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Transcription engine isn't running.",
|
||||
modifier = Modifier.weight(1f))
|
||||
TextButton({ state.go(DesktopState.Screen.ENGINE) }) {
|
||||
ShonarGhostButton({ state.go(DesktopState.Screen.ENGINE) }) {
|
||||
Text("Start it")
|
||||
}
|
||||
}
|
||||
|
|
@ -258,7 +259,7 @@ fun LibraryScreen(state: DesktopState) {
|
|||
it.status != FileStatus.RUNNING
|
||||
}
|
||||
if (batch == null && untranscribed > 0) {
|
||||
OutlinedButton({ state.transcribeAll() }) {
|
||||
ShonarOutlinedButton({ state.transcribeAll() }) {
|
||||
Text("Transcribe all ($untranscribed)")
|
||||
}
|
||||
}
|
||||
|
|
@ -269,9 +270,9 @@ fun LibraryScreen(state: DesktopState) {
|
|||
val running = entries.firstOrNull {
|
||||
it.status == FileStatus.RUNNING && it.file.name in batch.names
|
||||
}
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
SonarCard(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(Shonar.CardPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(Shonar.Space1)) {
|
||||
val fileFrac = running?.progress
|
||||
// Overall % = settled files + current file's fraction.
|
||||
val overall = if (fileFrac != null)
|
||||
|
|
@ -310,58 +311,59 @@ fun LibraryScreen(state: DesktopState) {
|
|||
}
|
||||
}
|
||||
}
|
||||
OutlinedTextField(query, { state.setQuery(it) },
|
||||
label = { Text("Search saved transcripts") },
|
||||
singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
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(8.dp)) {
|
||||
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) ->
|
||||
FilterChip(
|
||||
PillChip(
|
||||
selected = order == value,
|
||||
onClick = { state.setSortOrder(value) },
|
||||
label = { Text(label) },
|
||||
label = label,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
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) ->
|
||||
FilterChip(
|
||||
PillChip(
|
||||
selected = statusFilter == value,
|
||||
onClick = { state.setLibraryFilter(value) },
|
||||
label = { Text(label) },
|
||||
label = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selected.isNotEmpty() && query.isBlank()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
|
||||
Text("${selected.size} selected",
|
||||
style = MaterialTheme.typography.titleSmall)
|
||||
TextButton({
|
||||
ShonarGhostButton({
|
||||
state.selectAllOrClear(entries.map { it.file.absolutePath })
|
||||
}) {
|
||||
val allIn = entries.isNotEmpty() &&
|
||||
selected.containsAll(entries.map { it.file.absolutePath })
|
||||
Text(if (allIn) "Clear all" else "Select all")
|
||||
}
|
||||
TextButton({ state.clearSelection() }) { Text("Clear") }
|
||||
ShonarGhostButton({ state.clearSelection() }) { Text("Clear") }
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(
|
||||
ShonarDangerButton(
|
||||
onClick = { deletingMany = true },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
) {
|
||||
Text("Delete ${selected.size}")
|
||||
}
|
||||
|
|
@ -372,17 +374,14 @@ fun LibraryScreen(state: DesktopState) {
|
|||
Text("No matches.", style = MaterialTheme.typography.bodySmall)
|
||||
} else {
|
||||
LazyColumn(Modifier.fillMaxWidth().weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
verticalArrangement = Arrangement.spacedBy(Shonar.Space1)) {
|
||||
items(hits, key = { "${it.audioPath ?: "text"}:${it.report}:${it.line}" }) { h ->
|
||||
val openable = h.audioPath?.let { File(it).exists() } == true
|
||||
Card(
|
||||
Modifier.fillMaxWidth().then(
|
||||
if (openable) Modifier.clickable {
|
||||
state.openDetail(File(h.audioPath))
|
||||
} else Modifier
|
||||
),
|
||||
SonarCard(
|
||||
Modifier.fillMaxWidth(),
|
||||
onClick = { if (openable) state.openDetail(File(h.audioPath)) },
|
||||
) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Column(Modifier.padding(Shonar.CardPadding)) {
|
||||
Text(h.report, style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
if (h.line == 0) h.snippet else "line ${h.line}: …${h.snippet}…",
|
||||
|
|
@ -394,12 +393,12 @@ fun LibraryScreen(state: DesktopState) {
|
|||
}
|
||||
}
|
||||
} else if (folder == null) {
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(12.dp),
|
||||
SonarCard(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(Shonar.CardPadding),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("No folder chosen — pick one in Settings.",
|
||||
modifier = Modifier.weight(1f))
|
||||
Button({ state.go(DesktopState.Screen.SETTINGS) }) {
|
||||
ShonarButton({ state.go(DesktopState.Screen.SETTINGS) }) {
|
||||
Text("Open Settings")
|
||||
}
|
||||
}
|
||||
|
|
@ -408,12 +407,16 @@ fun LibraryScreen(state: DesktopState) {
|
|||
Text("No audio files in this folder.",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
} else {
|
||||
SectionHeader(
|
||||
"Your recordings",
|
||||
meta = "${entries.size} of ${allEntries.size}")
|
||||
LazyColumn(Modifier.fillMaxWidth().weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
verticalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
|
||||
items(entries, key = { it.file.absolutePath }) { e ->
|
||||
val isSelected = e.file.absolutePath in selected
|
||||
Card(Modifier.fillMaxWidth().clickable { state.openDetail(e.file) }) {
|
||||
Row(Modifier.fillMaxWidth().padding(12.dp),
|
||||
SonarCard(Modifier.fillMaxWidth(),
|
||||
onClick = { state.openDetail(e.file) }) {
|
||||
Row(Modifier.fillMaxWidth().padding(Shonar.CardPadding),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
|
|
@ -471,10 +474,10 @@ fun LibraryScreen(state: DesktopState) {
|
|||
}
|
||||
}
|
||||
if (e.status == FileStatus.NEW || e.status == FileStatus.FAILED) {
|
||||
TextButton({ state.pumpFile(e.file) }) {
|
||||
ShonarGhostButton({ state.pumpFile(e.file) }) {
|
||||
Icon(Icons.Filled.PlayArrow, contentDescription = null,
|
||||
modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
modifier = Modifier.size(Shonar.IconSm))
|
||||
Spacer(Modifier.width(Shonar.Space1))
|
||||
Text("Transcribe")
|
||||
}
|
||||
}
|
||||
|
|
@ -482,10 +485,10 @@ fun LibraryScreen(state: DesktopState) {
|
|||
// cancels them out of the pump. Only a file the
|
||||
// engine is actually working is locked.
|
||||
if (e.status != FileStatus.RUNNING) {
|
||||
TextButton({ renaming = e.file }) {
|
||||
ShonarGhostButton({ renaming = e.file }) {
|
||||
Icon(Icons.Filled.Edit, contentDescription = null,
|
||||
modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
modifier = Modifier.size(Shonar.IconSm))
|
||||
Spacer(Modifier.width(Shonar.Space1))
|
||||
Text("Rename")
|
||||
}
|
||||
}
|
||||
|
|
@ -506,26 +509,26 @@ fun LibraryScreen(state: DesktopState) {
|
|||
title = { Text("Rename recording") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
SonarField(
|
||||
value = text,
|
||||
onValueChange = { text = it; error = null },
|
||||
label = { Text("New name") },
|
||||
placeholder = "New name",
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall)
|
||||
HelperText(it, error = true)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton({
|
||||
ShonarGhostButton({
|
||||
error = state.renameFile(target, text)
|
||||
if (error == null) renaming = null
|
||||
}) { Text("Rename") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton({ renaming = null }) { Text("Cancel") }
|
||||
ShonarGhostButton({ renaming = null }) { Text("Cancel") }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue