mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
add a mini player progress bar and drag-to-expand
This commit is contained in:
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user