feat(ui): mini-player buffering pose + one-tap retry on error

- Equalizer chip dims while BUFFERING/INITIALIZING (no false 'playing'
  signal during reconnect)
- On ERROR the bar's subtitle becomes 'Couldn't reach · Retry' —
  tapping replays the current station without leaving the tab
- NowPlayingViewModel.retry() re-plays current URL
This commit is contained in:
avi 2026-09-08 10:13:06 -05:00
commit e2ed74a47d
2 changed files with 42 additions and 2 deletions

View file

@ -65,6 +65,10 @@ fun NowPlayingBar(
val isLocal by viewModel.isLocal.collectAsState()
val streamMeta by viewModel.streamMeta.collectAsState()
val localMedia by viewModel.localMedia.collectAsState()
val isError = bufferState == PlaybackManager.BufferState.ERROR
val isBuffering =
bufferState == PlaybackManager.BufferState.BUFFERING ||
bufferState == PlaybackManager.BufferState.INITIALIZING
// Swipe-down (or quick drag-down) dismisses the bar until a *new*
// source starts playing — a floating thing should answer to gestures.
var dismissedFor by remember { mutableStateOf<String?>(null) }
@ -145,13 +149,22 @@ fun NowPlayingBar(
.background(LocalTokens.current.accentFill),
contentAlignment = Alignment.Center,
) {
if (isPlaying) {
if (isPlaying && !isBuffering) {
EqualizerBars(
active = true,
color = LocalTokens.current.accentOnFill,
barWidth = 3.dp,
modifier = Modifier.size(22.dp),
)
} else if (isBuffering && !isError) {
EqualizerBars(
active = false,
color = LocalTokens.current.accentOnFill.copy(alpha = 0.7f),
barWidth = 3.dp,
modifier = Modifier
.size(22.dp)
.padding(2.dp),
)
} else {
Icon(
if (isLocal) Icons.Default.MusicNote else Icons.Default.Radio,
@ -186,7 +199,28 @@ fun NowPlayingBar(
color = LocalTokens.current.text,
)
}
if (subtitle.isNotEmpty()) {
if (isError) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { viewModel.retry() },
),
) {
Text(
"Couldn't reach · ",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
Text(
"Retry",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
color = tokens.accentText,
)
}
} else if (subtitle.isNotEmpty()) {
AnimatedContent(
targetState = subtitle,
transitionSpec = { metadataEnter(reduce) togetherWith metadataExit(reduce) },

View file

@ -23,4 +23,10 @@ class NowPlayingViewModel @Inject constructor(
fun togglePlayPause() {
playbackManager.togglePlayPause()
}
/** Reconnect the current station (mini-player error state). */
fun retry() {
val url = playbackManager.currentStationUrl ?: return
playbackManager.playStation(url, playbackManager.currentStationName)
}
}