Library rows: circular teal play button at row left (Android-home affordance) — plays/stops without opening the detail page; DONE files only; Stop square while playing

This commit is contained in:
avi 2026-09-21 19:45:20 -05:00
commit 13aae7818d
2 changed files with 61 additions and 0 deletions

View file

@ -40,6 +40,11 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow 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.onFocusChanged
import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color 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. /** Destructive action: quiet red outline — visible without shouting.
* ink overrides the label/icon color (detail-page Play/Delete use * ink overrides the label/icon color (detail-page Play/Delete use
* TextPrimary = white-on-dark per user directive, Sep 21). */ * TextPrimary = white-on-dark per user directive, Sep 21). */

View file

@ -423,6 +423,25 @@ fun LibraryScreen(state: DesktopState) {
checked = isSelected, checked = isSelected,
onCheckedChange = { state.toggleSelected(e.file.absolutePath) }, 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)) { Column(Modifier.weight(1f)) {
Text(e.file.nameWithoutExtension, Text(e.file.nameWithoutExtension,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,