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.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
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.Settings
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
|
@ -222,23 +223,23 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
val deleteWarning by state.deleteWarning.collectAsState()
|
val deleteWarning by state.deleteWarning.collectAsState()
|
||||||
val selected by state.selected.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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
ShonarWordmark(modifier = Modifier.weight(1f))
|
ShonarWordmark(modifier = Modifier.weight(1f))
|
||||||
TextButton({ state.go(DesktopState.Screen.SETTINGS) }) {
|
ShonarGhostButton({ state.go(DesktopState.Screen.SETTINGS) }) {
|
||||||
Icon(Icons.Filled.Settings, contentDescription = null,
|
Icon(Icons.Filled.Settings, contentDescription = "Settings",
|
||||||
modifier = Modifier.size(18.dp))
|
modifier = Modifier.size(Shonar.IconSm))
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(Shonar.Space2))
|
||||||
Text("Settings")
|
Text("Settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
Card(Modifier.fillMaxWidth()) {
|
SonarCard(Modifier.fillMaxWidth()) {
|
||||||
Row(Modifier.fillMaxWidth().padding(12.dp),
|
Row(Modifier.fillMaxWidth().padding(Shonar.CardPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Transcription engine isn't running.",
|
Text("Transcription engine isn't running.",
|
||||||
modifier = Modifier.weight(1f))
|
modifier = Modifier.weight(1f))
|
||||||
TextButton({ state.go(DesktopState.Screen.ENGINE) }) {
|
ShonarGhostButton({ state.go(DesktopState.Screen.ENGINE) }) {
|
||||||
Text("Start it")
|
Text("Start it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -258,7 +259,7 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
it.status != FileStatus.RUNNING
|
it.status != FileStatus.RUNNING
|
||||||
}
|
}
|
||||||
if (batch == null && untranscribed > 0) {
|
if (batch == null && untranscribed > 0) {
|
||||||
OutlinedButton({ state.transcribeAll() }) {
|
ShonarOutlinedButton({ state.transcribeAll() }) {
|
||||||
Text("Transcribe all ($untranscribed)")
|
Text("Transcribe all ($untranscribed)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,9 +270,9 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
val running = entries.firstOrNull {
|
val running = entries.firstOrNull {
|
||||||
it.status == FileStatus.RUNNING && it.file.name in batch.names
|
it.status == FileStatus.RUNNING && it.file.name in batch.names
|
||||||
}
|
}
|
||||||
Card(Modifier.fillMaxWidth()) {
|
SonarCard(Modifier.fillMaxWidth()) {
|
||||||
Column(Modifier.padding(10.dp),
|
Column(Modifier.padding(Shonar.CardPadding),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
verticalArrangement = Arrangement.spacedBy(Shonar.Space1)) {
|
||||||
val fileFrac = running?.progress
|
val fileFrac = running?.progress
|
||||||
// Overall % = settled files + current file's fraction.
|
// Overall % = settled files + current file's fraction.
|
||||||
val overall = if (fileFrac != null)
|
val overall = if (fileFrac != null)
|
||||||
|
|
@ -310,58 +311,59 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutlinedTextField(query, { state.setQuery(it) },
|
SonarField(query, { state.setQuery(it) },
|
||||||
label = { Text("Search saved transcripts") },
|
placeholder = "Search saved transcripts…",
|
||||||
singleLine = true, modifier = Modifier.fillMaxWidth())
|
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.
|
// Sort order (persisted): name A–Z, newest first, oldest first.
|
||||||
// Date modes also surface the file date on each row below.
|
// Date modes also surface the file date on each row below.
|
||||||
if (folder != null && query.isBlank()) {
|
if (folder != null && query.isBlank()) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,
|
Row(verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
|
||||||
Text("Sort", style = MaterialTheme.typography.labelMedium,
|
Text("Sort", style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
val order = state.sortOrder.collectAsState().value
|
val order = state.sortOrder.collectAsState().value
|
||||||
listOf("name" to "Name", "new" to "Newest",
|
listOf("name" to "Name", "new" to "Newest",
|
||||||
"old" to "Oldest").forEach { (value, label) ->
|
"old" to "Oldest").forEach { (value, label) ->
|
||||||
FilterChip(
|
PillChip(
|
||||||
selected = order == value,
|
selected = order == value,
|
||||||
onClick = { state.setSortOrder(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,
|
Text("Show", style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
listOf("all" to "All", "new" to "Untranscribed")
|
listOf("all" to "All", "new" to "Untranscribed")
|
||||||
.forEach { (value, label) ->
|
.forEach { (value, label) ->
|
||||||
FilterChip(
|
PillChip(
|
||||||
selected = statusFilter == value,
|
selected = statusFilter == value,
|
||||||
onClick = { state.setLibraryFilter(value) },
|
onClick = { state.setLibraryFilter(value) },
|
||||||
label = { Text(label) },
|
label = label,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selected.isNotEmpty() && query.isBlank()) {
|
if (selected.isNotEmpty() && query.isBlank()) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,
|
Row(verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
horizontalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
|
||||||
Text("${selected.size} selected",
|
Text("${selected.size} selected",
|
||||||
style = MaterialTheme.typography.titleSmall)
|
style = MaterialTheme.typography.titleSmall)
|
||||||
TextButton({
|
ShonarGhostButton({
|
||||||
state.selectAllOrClear(entries.map { it.file.absolutePath })
|
state.selectAllOrClear(entries.map { it.file.absolutePath })
|
||||||
}) {
|
}) {
|
||||||
val allIn = entries.isNotEmpty() &&
|
val allIn = entries.isNotEmpty() &&
|
||||||
selected.containsAll(entries.map { it.file.absolutePath })
|
selected.containsAll(entries.map { it.file.absolutePath })
|
||||||
Text(if (allIn) "Clear all" else "Select all")
|
Text(if (allIn) "Clear all" else "Select all")
|
||||||
}
|
}
|
||||||
TextButton({ state.clearSelection() }) { Text("Clear") }
|
ShonarGhostButton({ state.clearSelection() }) { Text("Clear") }
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
Button(
|
ShonarDangerButton(
|
||||||
onClick = { deletingMany = true },
|
onClick = { deletingMany = true },
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.error,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onError,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Text("Delete ${selected.size}")
|
Text("Delete ${selected.size}")
|
||||||
}
|
}
|
||||||
|
|
@ -372,17 +374,14 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
Text("No matches.", style = MaterialTheme.typography.bodySmall)
|
Text("No matches.", style = MaterialTheme.typography.bodySmall)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(Modifier.fillMaxWidth().weight(1f),
|
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 ->
|
items(hits, key = { "${it.audioPath ?: "text"}:${it.report}:${it.line}" }) { h ->
|
||||||
val openable = h.audioPath?.let { File(it).exists() } == true
|
val openable = h.audioPath?.let { File(it).exists() } == true
|
||||||
Card(
|
SonarCard(
|
||||||
Modifier.fillMaxWidth().then(
|
Modifier.fillMaxWidth(),
|
||||||
if (openable) Modifier.clickable {
|
onClick = { if (openable) state.openDetail(File(h.audioPath)) },
|
||||||
state.openDetail(File(h.audioPath))
|
|
||||||
} else Modifier
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(10.dp)) {
|
Column(Modifier.padding(Shonar.CardPadding)) {
|
||||||
Text(h.report, style = MaterialTheme.typography.titleSmall)
|
Text(h.report, style = MaterialTheme.typography.titleSmall)
|
||||||
Text(
|
Text(
|
||||||
if (h.line == 0) h.snippet else "line ${h.line}: …${h.snippet}…",
|
if (h.line == 0) h.snippet else "line ${h.line}: …${h.snippet}…",
|
||||||
|
|
@ -394,12 +393,12 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (folder == null) {
|
} else if (folder == null) {
|
||||||
Card(Modifier.fillMaxWidth()) {
|
SonarCard(Modifier.fillMaxWidth()) {
|
||||||
Row(Modifier.fillMaxWidth().padding(12.dp),
|
Row(Modifier.fillMaxWidth().padding(Shonar.CardPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("No folder chosen — pick one in Settings.",
|
Text("No folder chosen — pick one in Settings.",
|
||||||
modifier = Modifier.weight(1f))
|
modifier = Modifier.weight(1f))
|
||||||
Button({ state.go(DesktopState.Screen.SETTINGS) }) {
|
ShonarButton({ state.go(DesktopState.Screen.SETTINGS) }) {
|
||||||
Text("Open Settings")
|
Text("Open Settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -408,12 +407,16 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
Text("No audio files in this folder.",
|
Text("No audio files in this folder.",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
} else {
|
} else {
|
||||||
|
SectionHeader(
|
||||||
|
"Your recordings",
|
||||||
|
meta = "${entries.size} of ${allEntries.size}")
|
||||||
LazyColumn(Modifier.fillMaxWidth().weight(1f),
|
LazyColumn(Modifier.fillMaxWidth().weight(1f),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
verticalArrangement = Arrangement.spacedBy(Shonar.Space2)) {
|
||||||
items(entries, key = { it.file.absolutePath }) { e ->
|
items(entries, key = { it.file.absolutePath }) { e ->
|
||||||
val isSelected = e.file.absolutePath in selected
|
val isSelected = e.file.absolutePath in selected
|
||||||
Card(Modifier.fillMaxWidth().clickable { state.openDetail(e.file) }) {
|
SonarCard(Modifier.fillMaxWidth(),
|
||||||
Row(Modifier.fillMaxWidth().padding(12.dp),
|
onClick = { state.openDetail(e.file) }) {
|
||||||
|
Row(Modifier.fillMaxWidth().padding(Shonar.CardPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
verticalAlignment = Alignment.CenterVertically) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = isSelected,
|
checked = isSelected,
|
||||||
|
|
@ -471,10 +474,10 @@ 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) }) {
|
ShonarGhostButton({ state.pumpFile(e.file) }) {
|
||||||
Icon(Icons.Filled.PlayArrow, contentDescription = null,
|
Icon(Icons.Filled.PlayArrow, contentDescription = null,
|
||||||
modifier = Modifier.size(18.dp))
|
modifier = Modifier.size(Shonar.IconSm))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(Shonar.Space1))
|
||||||
Text("Transcribe")
|
Text("Transcribe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -482,10 +485,10 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
// cancels them out of the pump. Only a file the
|
// cancels them out of the pump. Only a file the
|
||||||
// engine is actually working is locked.
|
// engine is actually working is locked.
|
||||||
if (e.status != FileStatus.RUNNING) {
|
if (e.status != FileStatus.RUNNING) {
|
||||||
TextButton({ renaming = e.file }) {
|
ShonarGhostButton({ renaming = e.file }) {
|
||||||
Icon(Icons.Filled.Edit, contentDescription = null,
|
Icon(Icons.Filled.Edit, contentDescription = null,
|
||||||
modifier = Modifier.size(18.dp))
|
modifier = Modifier.size(Shonar.IconSm))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(Shonar.Space1))
|
||||||
Text("Rename")
|
Text("Rename")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -506,26 +509,26 @@ fun LibraryScreen(state: DesktopState) {
|
||||||
title = { Text("Rename recording") },
|
title = { Text("Rename recording") },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
OutlinedTextField(
|
SonarField(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { text = it; error = null },
|
onValueChange = { text = it; error = null },
|
||||||
label = { Text("New name") },
|
placeholder = "New name",
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
error?.let {
|
error?.let {
|
||||||
Text(it, color = MaterialTheme.colorScheme.error,
|
HelperText(it, error = true)
|
||||||
style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton({
|
ShonarGhostButton({
|
||||||
error = state.renameFile(target, text)
|
error = state.renameFile(target, text)
|
||||||
if (error == null) renaming = null
|
if (error == null) renaming = null
|
||||||
}) { Text("Rename") }
|
}) { Text("Rename") }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton({ renaming = null }) { Text("Cancel") }
|
ShonarGhostButton({ renaming = null }) { Text("Cancel") }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue