diff --git a/app/src/main/kotlin/com/shonar/desktop/Components.kt b/app/src/main/kotlin/com/shonar/desktop/Components.kt index c44f712..d13d711 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Components.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Components.kt @@ -40,6 +40,11 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow +import androidx.compose.ui.draw.clip +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.Icon import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Color @@ -165,6 +170,43 @@ fun ShonarGhostButton( ) } +/** Circular transport button, matching the Android home rows: teal + * hairline ring + teal play triangle; swaps to a filled-look stop + * square while its row is playing. Inner clickable consumes the tap, + * so the enclosing card's open-on-click doesn't fire. */ +@Composable +fun ShonarPlayCircle( + playing: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + size: androidx.compose.ui.unit.Dp = 40.dp, +) { + Surface( + modifier = modifier.size(size) + .clip(CircleShape) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ) + .border(Shonar.BorderW, Shonar.Accent.copy(alpha = 0.7f), CircleShape), + color = androidx.compose.ui.graphics.Color.Transparent, + shape = CircleShape, + ) { + Box(contentAlignment = Alignment.Center) { + if (playing) { + Icon(Icons.Filled.Stop, contentDescription = "Stop", + tint = Shonar.Accent, + modifier = Modifier.size(Shonar.IconSm)) + } else { + Icon(Icons.Filled.PlayArrow, contentDescription = "Play", + tint = Shonar.Accent, + modifier = Modifier.size(Shonar.IconSm)) + } + } + } +} + /** Destructive action: quiet red outline — visible without shouting. * ink overrides the label/icon color (detail-page Play/Delete use * TextPrimary = white-on-dark per user directive, Sep 21). */ diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index 5878a12..691b3c4 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -423,6 +423,25 @@ fun LibraryScreen(state: DesktopState) { checked = isSelected, onCheckedChange = { state.toggleSelected(e.file.absolutePath) }, ) + // Android-home affordance: circular play at the + // left of the row — listen without opening. + // Transcribed files only (nothing to hear yet + // otherwise); shows Stop while playing. + val playingFile by state.playingFile.collectAsState() + val isThisPlaying = + playingFile?.absolutePath == e.file.absolutePath + if (e.status == FileStatus.DONE) { + Spacer(Modifier.width(Shonar.Space1)) + ShonarPlayCircle( + playing = isThisPlaying, + onClick = { + if (isThisPlaying) state.stopPlayback() + else state.playFrom(e.file) + }) + Spacer(Modifier.width(Shonar.Space2)) + } else { + Spacer(Modifier.width(Shonar.Space2)) + } Column(Modifier.weight(1f)) { Text(e.file.nameWithoutExtension, style = MaterialTheme.typography.titleSmall,