fix cover art sizing and downsampling

This commit is contained in:
austinried
2026-10-04 00:29:54 +00:00
parent 01167fbb4a
commit 11947893b2
3 changed files with 99 additions and 48 deletions
@@ -4,9 +4,11 @@ import android.content.Context
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
@@ -19,12 +21,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.ContentScale
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.AsyncImagePainter
import coil3.compose.LocalPlatformContext
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
@@ -42,52 +44,107 @@ fun CoverArt(
elevation: Dp = 0.dp,
) {
val context = LocalPlatformContext.current
var failed by remember(ref, thumbnailRef) { mutableStateOf(false) }
var thumbnailLoaded by remember(ref, thumbnailRef) { mutableStateOf(false) }
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
val frameModifier =
Modifier
.shadow(elevation, RoundedCornerShape(2.dp), clip = elevation > 0.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
if (square) {
Box(modifier.then(frameModifier)) {
CoverArtContent(
ref = ref,
name = name,
thumbnailRef = thumbnailRef,
showPlaceholder = showPlaceholder,
failed = failed,
thumbnailLoaded = thumbnailLoaded,
onThumbnailLoaded = { thumbnailLoaded = true },
contentScale = ContentScale.Crop,
) {
if (ref != null) {
AsyncImage(
model = model,
contentDescription = null,
contentScale = ContentScale.Crop,
onError = { failed = true },
modifier = Modifier.fillMaxSize(),
)
}
}
}
Box(
modifier =
modifier
.aspectRatio(ratio)
.shadow(elevation, RoundedCornerShape(2.dp), clip = elevation > 0.dp)
.background(MaterialTheme.colorScheme.surfaceVariant),
) {
if (showPlaceholder || (failed && !thumbnailLoaded)) {
Text(
text = name.trim().take(1).uppercase(),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.Center),
)
}
if (thumbnailRef != null && thumbnailRef != ref) {
AsyncImage(
model = remember(thumbnailRef) { imageRequest(context, thumbnailRef, crossfade = false) },
contentDescription = null,
contentScale = if (square) ContentScale.Crop else ContentScale.Fit,
onSuccess = { thumbnailLoaded = true },
modifier = Modifier.fillMaxSize(),
)
}
if (ref != null) {
Image(
painter = painter,
contentDescription = null,
contentScale = if (square) ContentScale.Crop else ContentScale.Fit,
modifier = Modifier.fillMaxSize(),
} else {
val painter =
rememberAsyncImagePainter(
model = model,
onError = { failed = true },
contentScale = ContentScale.Fit,
)
BoxWithConstraints(modifier, contentAlignment = Alignment.Center) {
val ratio = painter.intrinsicSize.ratioOrOne()
val width = if (ratio > maxWidth.value / maxHeight.value) maxWidth else maxHeight * ratio
Box(Modifier.size(width, width / ratio).then(frameModifier)) {
CoverArtContent(
ref = ref,
name = name,
thumbnailRef = thumbnailRef,
showPlaceholder = showPlaceholder,
failed = failed,
thumbnailLoaded = thumbnailLoaded,
onThumbnailLoaded = { thumbnailLoaded = true },
contentScale = ContentScale.Fit,
) {
if (ref != null) {
Image(
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
}
}
@Composable
private fun BoxScope.CoverArtContent(
ref: CoverArtRef?,
name: String,
thumbnailRef: CoverArtRef?,
showPlaceholder: Boolean,
failed: Boolean,
thumbnailLoaded: Boolean,
onThumbnailLoaded: () -> Unit,
contentScale: ContentScale,
main: @Composable () -> Unit,
) {
val context = LocalPlatformContext.current
if (showPlaceholder || (failed && !thumbnailLoaded)) {
Text(
text = name.trim().take(1).uppercase(),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.Center),
)
}
if (thumbnailRef != null && thumbnailRef != ref) {
AsyncImage(
model = remember(thumbnailRef) { imageRequest(context, thumbnailRef, crossfade = false) },
contentDescription = null,
contentScale = contentScale,
onSuccess = { onThumbnailLoaded() },
modifier = Modifier.fillMaxSize(),
)
}
main()
}
private fun Size.ratioOrOne(): Float = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else 1f
private fun imageRequest(
context: Context,
ref: CoverArtRef,
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Download
@@ -47,9 +46,7 @@ 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.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -17,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.QueueMusic
@@ -46,7 +43,6 @@ 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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
@@ -203,7 +199,7 @@ fun NowPlayingScreen(
thumbnailRef = thumbnailRef,
square = false,
elevation = 3.dp,
modifier = Modifier.fillMaxHeight(),
modifier = Modifier.fillMaxSize(),
)
}
val density = LocalDensity.current
@@ -256,6 +252,7 @@ fun NowPlayingScreen(
dragging = false
},
valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f),
enabled = state.durationMs > 0,
modifier = Modifier.padding(top = 28.dp),
)
Row(