From 5907ebcb5c153fbede0c52e1508f3b766741354a Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Thu, 24 Sep 2026 06:02:48 +0000 Subject: [PATCH] add a mini player progress bar and drag-to-expand --- .../com/subtracks/ui/playback/MiniPlayer.kt | 142 +++++++++++------- 1 file changed, 90 insertions(+), 52 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt b/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt index 7419bed9..ba88df33 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt @@ -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), - ) - } } } }