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 package com.subtracks.ui.components
import android.content.Context import android.content.Context
import androidx.collection.LruCache
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -36,6 +37,21 @@ import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import com.subtracks.data.model.CoverArtRef 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 @Composable
fun CoverArt( fun CoverArt(
ref: CoverArtRef?, ref: CoverArtRef?,
@@ -49,7 +65,7 @@ fun CoverArt(
val context = LocalPlatformContext.current val context = LocalPlatformContext.current
var failed by remember(ref, thumbnailRef) { mutableStateOf(false) } var failed by remember(ref, thumbnailRef) { mutableStateOf(false) }
var thumbnailLoaded 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 model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null) } }
val frameModifier = val frameModifier =
Modifier Modifier
@@ -74,6 +90,11 @@ fun CoverArt(
model = model, model = model,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
onSuccess = { success ->
success.painter.intrinsicSize
.ratioOrNull()
?.let { ArtworkRatioCache.put(ref.cacheKey, it) }
},
onError = { failed = true }, onError = { failed = true },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
@@ -150,7 +171,10 @@ private fun BoxScope.CoverArtContent(
onThumbnailLoaded() onThumbnailLoaded()
success.painter.intrinsicSize success.painter.intrinsicSize
.ratioOrNull() .ratioOrNull()
?.let(onThumbnailRatio) ?.let {
onThumbnailRatio(it)
ArtworkRatioCache.put(thumbnailRef.cacheKey, it)
}
}, },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
@@ -25,8 +25,8 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.PlaylistPlay
import androidx.compose.material.icons.automirrored.rounded.Segment
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.Album
import androidx.compose.material.icons.rounded.ArrowDownward import androidx.compose.material.icons.rounded.ArrowDownward
@@ -480,7 +480,7 @@ fun LibraryScreen(
) { ) {
Box { Box {
Icon( Icon(
Icons.AutoMirrored.Filled.Sort, Icons.AutoMirrored.Rounded.Segment,
contentDescription = "List options", contentDescription = "List options",
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
) )
@@ -51,9 +51,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.PlatformTextStyle 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.LineHeightStyle
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.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.SingletonImageLoader import coil3.SingletonImageLoader
import coil3.compose.LocalPlatformContext import coil3.compose.LocalPlatformContext
@@ -175,11 +177,21 @@ fun NowPlayingScreen(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
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(
text = title, text = title,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
}
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { 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 { private fun formatTime(milliseconds: Long): String {
val totalSeconds = (milliseconds / 1000).coerceAtLeast(0) val totalSeconds = (milliseconds / 1000).coerceAtLeast(0)
val minutes = totalSeconds / 60 val minutes = totalSeconds / 60