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.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.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
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.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -19,12 +21,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.ContentScale 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 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.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest import coil3.request.ImageRequest
@@ -42,24 +44,85 @@ fun CoverArt(
elevation: Dp = 0.dp, elevation: Dp = 0.dp,
) { ) {
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) }
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 painter = rememberAsyncImagePainter(model) val frameModifier =
val failed = painter.state is AsyncImagePainter.State.Error Modifier
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)
.shadow(elevation, RoundedCornerShape(2.dp), clip = elevation > 0.dp) .shadow(elevation, RoundedCornerShape(2.dp), clip = elevation > 0.dp)
.background(MaterialTheme.colorScheme.surfaceVariant), .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(),
)
}
}
}
} 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)) { if (showPlaceholder || (failed && !thumbnailLoaded)) {
Text( Text(
text = name.trim().take(1).uppercase(), text = name.trim().take(1).uppercase(),
@@ -72,22 +135,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 = if (square) ContentScale.Crop else ContentScale.Fit, contentScale = contentScale,
onSuccess = { thumbnailLoaded = true }, onSuccess = { onThumbnailLoaded() },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }
if (ref != null) { main()
Image(
painter = painter,
contentDescription = null,
contentScale = if (square) ContentScale.Crop else ContentScale.Fit,
modifier = Modifier.fillMaxSize(),
)
}
}
} }
private fun Size.ratioOrOne(): Float = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else 1f
private fun imageRequest( private fun imageRequest(
context: Context, context: Context,
ref: CoverArtRef, ref: CoverArtRef,
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Download
@@ -47,9 +46,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets 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.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
@@ -17,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.QueueMusic 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.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@@ -203,7 +199,7 @@ fun NowPlayingScreen(
thumbnailRef = thumbnailRef, thumbnailRef = thumbnailRef,
square = false, square = false,
elevation = 3.dp, elevation = 3.dp,
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxSize(),
) )
} }
val density = LocalDensity.current val density = LocalDensity.current
@@ -256,6 +252,7 @@ fun NowPlayingScreen(
dragging = false dragging = false
}, },
valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f), valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f),
enabled = state.durationMs > 0,
modifier = Modifier.padding(top = 28.dp), modifier = Modifier.padding(top = 28.dp),
) )
Row( Row(