diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index d53fff1..a916033 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -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") } }, ) }