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.basicMarquee
import androidx.compose.foundation.clickable 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.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.CoverArtRef
import com.subtracks.playback.PlaybackState 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.ArtworkColors
import com.subtracks.ui.theme.ArtworkTheme import com.subtracks.ui.theme.ArtworkTheme
private val EXPAND_DRAG = 48.dp
@Composable @Composable
fun MiniPlayer( fun MiniPlayer(
state: PlaybackState, state: PlaybackState,
@@ -45,6 +56,10 @@ fun MiniPlayer(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val item = state.item ?: return 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) { ArtworkTheme(artwork) {
Surface( Surface(
color = color =
@@ -52,67 +67,90 @@ fun MiniPlayer(
?: MaterialTheme.colorScheme.surfaceContainerHigh, ?: MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor = MaterialTheme.colorScheme.onSurface, contentColor = MaterialTheme.colorScheme.onSurface,
modifier = modifier =
modifier.fillMaxWidth().clickable( modifier
interactionSource = remember { MutableInteractionSource() }, .fillMaxWidth()
indication = null, .draggable(
onClick = onExpand, 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( Column {
modifier = LinearProgressIndicator(
Modifier progress = { progress },
.fillMaxWidth() modifier = Modifier.fillMaxWidth().height(2.dp),
.navigationBarsPadding() color = MaterialTheme.colorScheme.primary,
.padding(10.dp), trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.24f),
verticalAlignment = Alignment.CenterVertically, gapSize = 0.dp,
horizontalArrangement = Arrangement.spacedBy(12.dp), drawStopIndicator = {},
) {
CoverArt(
ref = coverArt,
name = item.title,
modifier = Modifier.size(48.dp).clip(RoundedCornerShape(6.dp)),
) )
Column(Modifier.weight(1f)) { Row(
Text( modifier =
text = item.title, Modifier
style = MaterialTheme.typography.bodyMedium, .fillMaxWidth()
color = MaterialTheme.colorScheme.onSurface, .navigationBarsPadding()
maxLines = 1, .padding(10.dp),
modifier = Modifier.basicMarquee(), verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CoverArt(
ref = coverArt,
name = item.title,
modifier = Modifier.size(48.dp).clip(RoundedCornerShape(6.dp)),
) )
Text( Column(Modifier.weight(1f)) {
text = item.artist ?: item.album.orEmpty(), Text(
style = MaterialTheme.typography.bodySmall, text = item.title,
color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium,
maxLines = 1, color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.basicMarquee(), maxLines = 1,
) modifier = Modifier.basicMarquee(),
} )
Row(verticalAlignment = Alignment.CenterVertically) { Text(
IconButton(onClick = onPlayPause, modifier = Modifier.size(48.dp)) { text = item.artist ?: item.album.orEmpty(),
if (state.isBuffering && state.isPlaying) { style = MaterialTheme.typography.bodySmall,
CircularProgressIndicator( color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onSurface, maxLines = 1,
strokeWidth = 3.dp, modifier = Modifier.basicMarquee(),
modifier = Modifier.size(24.dp), )
) }
} else { 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( Icon(
imageVector = if (state.isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, imageVector = Icons.Rounded.SkipNext,
contentDescription = if (state.isPlaying) "Pause" else "Play", contentDescription = "Next",
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(36.dp), 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),
)
}
} }
} }
} }