make the now playing slider seekable and show full artwork

This commit is contained in:
austinried
2026-10-04 00:29:54 +00:00
parent d7434b80cc
commit 92ef7bfcfb
5 changed files with 74 additions and 14 deletions
@@ -376,7 +376,10 @@ class PlaybackController(
fun previous() = skip(-1) fun previous() = skip(-1)
fun seekTo(positionMs: Long) { fun seekTo(positionMs: Long) {
player?.seekTo(positionMs) val player = player ?: return
player.ensurePrepared()
player.seekTo(positionMs)
player.play()
} }
fun coverArt( fun coverArt(
@@ -1,8 +1,10 @@
package com.subtracks.ui.components package com.subtracks.ui.components
import android.content.Context import android.content.Context
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@@ -19,7 +21,9 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import coil3.compose.LocalPlatformContext import coil3.compose.LocalPlatformContext
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.CoverArtRef
@@ -31,11 +35,21 @@ fun CoverArt(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
thumbnailRef: CoverArtRef? = null, thumbnailRef: CoverArtRef? = null,
showPlaceholder: Boolean = true, showPlaceholder: Boolean = true,
square: Boolean = true,
) { ) {
val context = LocalPlatformContext.current val context = LocalPlatformContext.current
var failed by remember(ref, thumbnailRef) { mutableStateOf(false) }
var thumbnailLoaded by remember(ref, thumbnailRef) { mutableStateOf(false) } var thumbnailLoaded by remember(ref, thumbnailRef) { mutableStateOf(false) }
Box(modifier = modifier.background(MaterialTheme.colorScheme.surfaceVariant)) { val model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null) } }
val painter = rememberAsyncImagePainter(model)
val failed = painter.state is AsyncImagePainter.State.Error
val intrinsic = painter.intrinsicSize
val ratio =
if (!square && intrinsic.width > 0f && intrinsic.height > 0f && intrinsic.width.isFinite() && intrinsic.height.isFinite()) {
intrinsic.width / intrinsic.height
} else {
1f
}
Box(modifier = modifier.aspectRatio(ratio).background(MaterialTheme.colorScheme.surfaceVariant)) {
if (showPlaceholder || (failed && !thumbnailLoaded)) { if (showPlaceholder || (failed && !thumbnailLoaded)) {
Text( Text(
text = name.trim().take(1).uppercase(), text = name.trim().take(1).uppercase(),
@@ -48,17 +62,16 @@ fun CoverArt(
AsyncImage( AsyncImage(
model = remember(thumbnailRef) { imageRequest(context, thumbnailRef, crossfade = false) }, model = remember(thumbnailRef) { imageRequest(context, thumbnailRef, crossfade = false) },
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = if (square) ContentScale.Crop else ContentScale.Fit,
onSuccess = { thumbnailLoaded = true }, onSuccess = { thumbnailLoaded = true },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }
if (ref != null) { if (ref != null) {
AsyncImage( Image(
model = remember(ref, thumbnailRef) { imageRequest(context, ref, crossfade = thumbnailRef != null) }, painter = painter,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = if (square) ContentScale.Crop else ContentScale.Fit,
onError = { failed = true },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }
@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -234,10 +233,10 @@ fun HeroHeader(
ref = art, ref = art,
name = name, name = name,
thumbnailRef = thumbnailRef, thumbnailRef = thumbnailRef,
square = false,
modifier = modifier =
Modifier Modifier
.fillMaxWidth(0.86f) .fillMaxWidth(0.86f)
.aspectRatio(1f)
.shadow(elevation = 3.dp, shape = RoundedCornerShape(2.dp), clip = false) .shadow(elevation = 3.dp, shape = RoundedCornerShape(2.dp), clip = false)
.clip(RoundedCornerShape(2.dp)), .clip(RoundedCornerShape(2.dp)),
) )
@@ -30,6 +30,7 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
@@ -46,6 +47,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -125,6 +127,14 @@ fun NowPlayingScreen(
) { ) {
val playButtonSize = 96.dp val playButtonSize = 96.dp
val playCircleDiameter = playButtonSize * 20f / 24f val playCircleDiameter = playButtonSize * 20f / 24f
val titleStyle =
MaterialTheme.typography.headlineSmall.copy(
platformStyle = PlatformTextStyle(includeFontPadding = false),
)
val subtitleStyle =
MaterialTheme.typography.bodyMedium.copy(
platformStyle = PlatformTextStyle(includeFontPadding = false),
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
ArtworkTheme(artwork) { ArtworkTheme(artwork) {
@@ -172,23 +182,23 @@ fun NowPlayingScreen(
ref = coverArt, ref = coverArt,
name = state.item?.title.orEmpty(), name = state.item?.title.orEmpty(),
thumbnailRef = thumbnailRef, thumbnailRef = thumbnailRef,
square = false,
modifier = modifier =
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()
.aspectRatio(1f)
.shadow(elevation = 3.dp, shape = RoundedCornerShape(2.dp), clip = false) .shadow(elevation = 3.dp, shape = RoundedCornerShape(2.dp), clip = false)
.clip(RoundedCornerShape(2.dp)), .clip(RoundedCornerShape(2.dp)),
) )
} }
Text( Text(
text = state.item?.title.orEmpty(), text = state.item?.title.orEmpty(),
style = MaterialTheme.typography.headlineSmall, style = titleStyle,
maxLines = 1, maxLines = 1,
modifier = Modifier.padding(top = 28.dp).basicMarquee(), modifier = Modifier.padding(top = 28.dp).basicMarquee(),
) )
Text( Text(
text = listOfNotNull(state.item?.artist, state.item?.album).joinToString(" • "), text = listOfNotNull(state.item?.artist, state.item?.album).joinToString(" • "),
style = MaterialTheme.typography.bodyMedium, style = subtitleStyle,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1, maxLines = 1,
modifier = Modifier.padding(top = 6.dp).basicMarquee(), modifier = Modifier.padding(top = 6.dp).basicMarquee(),
@@ -206,7 +216,13 @@ fun NowPlayingScreen(
dragging = false dragging = false
}, },
valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f), valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f),
enabled = state.durationMs > 0, track = { sliderState ->
SliderDefaults.Track(
sliderState = sliderState,
thumbTrackGapSize = 0.dp,
drawStopIndicator = {},
)
},
modifier = Modifier.padding(top = 28.dp), modifier = Modifier.padding(top = 28.dp),
) )
Row( Row(
@@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="299.84"
android:viewportHeight="219.51">
<group android:scaleX="0.43"
android:scaleY="0.3147989"
android:translateX="85.4544"
android:translateY="75.20425">
<path
android:pathData="m23.84,-0c-12.76,0 -23.44,13 -23.65,25.12 -0.42,79.03 0,160.73 0,170.4 0,12.59 9.8,23.99 23.41,23.99l253.58,0c11.4,0 22.66,-12.38 22.66,-22.79L299.84,24.12c0,-10.8 -10.24,-23.81 -21.84,-23.81 -11.6,0 -254.16,-0.31 -254.16,-0.31zM39.05,25.28c0,0 217.71,0.71 223.67,0.71 6.07,0 12.17,6.83 12.17,12.06l0,142.25c0,8.97 -6.97,14.63 -12.48,14.64l-33.42,0.07c-5.32,0.01 -9.93,-7.19 -11.35,-10.61 -1.89,-4.55 -11.08,-25.75 -12.1,-28.1 -0.6,-1.38 -2.9,-2.83 -5.89,-2.83L98.33,153.45c-2.68,0 -4.89,2.87 -5.67,4.71 -1.12,2.66 -9.02,21.39 -11.63,27.57 -2.73,6.46 -4.37,9.06 -11.47,9.06l-30.97,0c-7.44,0 -13.69,-7.17 -13.67,-15.18l0.28,-139.51c0,-6.88 5.54,-14.83 13.85,-14.83z"
android:strokeWidth="0.9"
android:fillColor="#FFFFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M90.09,95.03m-32.1,0a32.42,32.1 90,1 1,64.21 0a32.42,32.1 90,1 1,-64.21 0"
android:strokeWidth="0.86"
android:fillColor="#FFFFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M209.94,95.03m-32.1,0a32.42,32.1 90,1 1,64.21 0a32.42,32.1 90,1 1,-64.21 0"
android:strokeWidth="0.86"
android:fillColor="#FFFFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</group>
</vector>