mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user