add a mini player progress bar and drag-to-expand

This commit is contained in:
austinried
2026-10-04 00:29:54 +00:00
parent 11947893b2
commit 5907ebcb5c
@@ -2,11 +2,15 @@ package com.subtracks.ui.playback
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -18,15 +22,20 @@ import androidx.compose.material.icons.rounded.SkipNext
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.subtracks.data.model.CoverArtRef
import com.subtracks.playback.PlaybackState
@@ -34,6 +43,8 @@ import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.ArtworkTheme
private val EXPAND_DRAG = 48.dp
@Composable
fun MiniPlayer(
state: PlaybackState,
@@ -45,6 +56,10 @@ fun MiniPlayer(
modifier: Modifier = Modifier,
) {
val item = state.item ?: return
val expandThresholdPx = with(LocalDensity.current) { EXPAND_DRAG.toPx() }
var dragUpPx by remember { mutableFloatStateOf(0f) }
val progress = if (state.durationMs > 0) (state.positionMs.toFloat() / state.durationMs).coerceIn(0f, 1f) else 0f
ArtworkTheme(artwork) {
Surface(
color =
@@ -52,67 +67,90 @@ fun MiniPlayer(
?: MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor = MaterialTheme.colorScheme.onSurface,
modifier =
modifier.fillMaxWidth().clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onExpand,
),
modifier
.fillMaxWidth()
.draggable(
orientation = Orientation.Vertical,
onDragStarted = { dragUpPx = 0f },
state =
rememberDraggableState { delta ->
dragUpPx = (dragUpPx - delta).coerceAtLeast(0f)
if (dragUpPx >= expandThresholdPx) {
dragUpPx = 0f
onExpand()
}
},
).clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onExpand,
),
) {
Row(
modifier =
Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CoverArt(
ref = coverArt,
name = item.title,
modifier = Modifier.size(48.dp).clip(RoundedCornerShape(6.dp)),
Column {
LinearProgressIndicator(
progress = { progress },
modifier = Modifier.fillMaxWidth().height(2.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.24f),
gapSize = 0.dp,
drawStopIndicator = {},
)
Column(Modifier.weight(1f)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
modifier = Modifier.basicMarquee(),
Row(
modifier =
Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CoverArt(
ref = coverArt,
name = item.title,
modifier = Modifier.size(48.dp).clip(RoundedCornerShape(6.dp)),
)
Text(
text = item.artist ?: item.album.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
modifier = Modifier.basicMarquee(),
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onPlayPause, modifier = Modifier.size(48.dp)) {
if (state.isBuffering && state.isPlaying) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onSurface,
strokeWidth = 3.dp,
modifier = Modifier.size(24.dp),
)
} else {
Column(Modifier.weight(1f)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
modifier = Modifier.basicMarquee(),
)
Text(
text = item.artist ?: item.album.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
modifier = Modifier.basicMarquee(),
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onPlayPause, modifier = Modifier.size(48.dp)) {
if (state.isBuffering && state.isPlaying) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onSurface,
strokeWidth = 3.dp,
modifier = Modifier.size(24.dp),
)
} else {
Icon(
imageVector = if (state.isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow,
contentDescription = if (state.isPlaying) "Pause" else "Play",
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(36.dp),
)
}
}
IconButton(onClick = onNext, enabled = state.hasNext, modifier = Modifier.size(48.dp)) {
Icon(
imageVector = if (state.isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow,
contentDescription = if (state.isPlaying) "Pause" else "Play",
imageVector = Icons.Rounded.SkipNext,
contentDescription = "Next",
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(36.dp),
)
}
}
IconButton(onClick = onNext, enabled = state.hasNext, modifier = Modifier.size(48.dp)) {
Icon(
imageVector = Icons.Rounded.SkipNext,
contentDescription = "Next",
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(36.dp),
)
}
}
}
}