thicker library fab icon, two-line now playing header, cached art ratio

- FAB: use the Rounded Segment icon so the glyph reads thick
- Now Playing: two-line header, an uppercase "Now playing: <type>" label (disc-title style) above the source, both left aligned
- CoverArt: remember the aspect ratio every image loads at and seed the non-square placeholder from it, so the detail hero does not start square and snap
This commit is contained in:
austinried
2026-10-04 00:29:58 +00:00
parent 216990181e
commit f490ca8fa7
3 changed files with 53 additions and 9 deletions
@@ -1,6 +1,7 @@
package com.subtracks.ui.components
import android.content.Context
import androidx.collection.LruCache
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -36,6 +37,21 @@ import coil3.request.ImageRequest
import coil3.request.crossfade
import com.subtracks.data.model.CoverArtRef
private const val MAX_CACHED_RATIOS = 256
private object ArtworkRatioCache {
private val ratios = LruCache<String, Float>(MAX_CACHED_RATIOS)
fun get(cacheKey: String): Float? = ratios.get(cacheKey)
fun put(
cacheKey: String,
ratio: Float,
) {
if (ratio > 0f && ratio.isFinite()) ratios.put(cacheKey, ratio)
}
}
@Composable
fun CoverArt(
ref: CoverArtRef?,
@@ -49,7 +65,7 @@ fun CoverArt(
val context = LocalPlatformContext.current
var failed by remember(ref, thumbnailRef) { mutableStateOf(false) }
var thumbnailLoaded by remember(ref, thumbnailRef) { mutableStateOf(false) }
var thumbnailRatio by remember(ref, thumbnailRef) { mutableStateOf<Float?>(null) }
var thumbnailRatio by remember(ref, thumbnailRef) { mutableStateOf(thumbnailRef?.cacheKey?.let(ArtworkRatioCache::get)) }
val model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null) } }
val frameModifier =
Modifier
@@ -74,6 +90,11 @@ fun CoverArt(
model = model,
contentDescription = null,
contentScale = ContentScale.Crop,
onSuccess = { success ->
success.painter.intrinsicSize
.ratioOrNull()
?.let { ArtworkRatioCache.put(ref.cacheKey, it) }
},
onError = { failed = true },
modifier = Modifier.fillMaxSize(),
)
@@ -150,7 +171,10 @@ private fun BoxScope.CoverArtContent(
onThumbnailLoaded()
success.painter.intrinsicSize
.ratioOrNull()
?.let(onThumbnailRatio)
?.let {
onThumbnailRatio(it)
ArtworkRatioCache.put(thumbnailRef.cacheKey, it)
}
},
modifier = Modifier.fillMaxSize(),
)
@@ -25,8 +25,8 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.automirrored.rounded.PlaylistPlay
import androidx.compose.material.icons.automirrored.rounded.Segment
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Album
import androidx.compose.material.icons.rounded.ArrowDownward
@@ -480,7 +480,7 @@ fun LibraryScreen(
) {
Box {
Icon(
Icons.AutoMirrored.Filled.Sort,
Icons.AutoMirrored.Rounded.Segment,
contentDescription = "List options",
modifier = Modifier.size(28.dp),
)
@@ -51,9 +51,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.SingletonImageLoader
import coil3.compose.LocalPlatformContext
@@ -175,11 +177,21 @@ fun NowPlayingScreen(
topBar = {
TopAppBar(
title = {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Column {
Text(
text = "Now playing: ${state.context?.kind?.label() ?: "library"}".uppercase(),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
)
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
},
navigationIcon = {
IconButton(onClick = onBack) {
@@ -385,6 +397,14 @@ fun NowPlayingScreen(
}
}
private fun QueueKind.label(): String =
when (this) {
QueueKind.Album -> "album"
QueueKind.Playlist -> "playlist"
QueueKind.Song -> "song"
QueueKind.Songs -> "library"
}
private fun formatTime(milliseconds: Long): String {
val totalSeconds = (milliseconds / 1000).coerceAtLeast(0)
val minutes = totalSeconds / 60