diff --git a/app/src/main/kotlin/com/subtracks/ui/components/Components.kt b/app/src/main/kotlin/com/subtracks/ui/components/Components.kt index 2ef6423f..82e70775 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/Components.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/Components.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.compose.LocalPlatformContext @@ -73,13 +74,13 @@ fun CoverArt( var failed by remember(ref, thumbnailRef) { mutableStateOf(false) } var thumbnailLoaded by remember(ref, thumbnailRef) { mutableStateOf(false) } 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 .shadow(elevation, RoundedCornerShape(2.dp), clip = elevation > 0.dp) .background(MaterialTheme.colorScheme.surfaceVariant) if (square) { + val model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null) } } Box(modifier.then(frameModifier)) { CoverArtContent( ref = ref, @@ -109,13 +110,28 @@ fun CoverArt( } } } else { - val painter = - rememberAsyncImagePainter( - model = model, - onError = { failed = true }, - contentScale = ContentScale.Fit, - ) BoxWithConstraints(modifier, contentAlignment = Alignment.Center) { + val density = LocalDensity.current + val targetSize = + remember(maxWidth, maxHeight, density) { + val width = with(density) { if (maxWidth.value.isFinite()) maxWidth.roundToPx() else 0 } + val height = with(density) { if (maxHeight.value.isFinite()) maxHeight.roundToPx() else 0 } + when { + width > 0 && height > 0 -> IntSize(width, height) + width > 0 -> IntSize(width, width) + height > 0 -> IntSize(height, height) + else -> null + } + } + val painter = + rememberAsyncImagePainter( + model = + remember(ref, thumbnailRef, targetSize) { + ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null, size = targetSize) } + }, + onError = { failed = true }, + contentScale = ContentScale.Fit, + ) val ratio = painter.intrinsicSize.ratioOrNull() ?: thumbnailRatio @@ -191,18 +207,27 @@ private fun BoxScope.CoverArtContent( private fun Size.ratioOrNull(): Float? = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else null -private fun imageRequest( +internal fun imageRequest( context: Context, ref: CoverArtRef, crossfade: Boolean, -): ImageRequest = - ImageRequest - .Builder(context) - .data(ref.url) - .memoryCacheKey(ref.cacheKey) - .diskCacheKey(ref.cacheKey) - .crossfade(crossfade) - .build() + size: IntSize? = null, +): ImageRequest { + val builder = + ImageRequest + .Builder(context) + .data(ref.url) + .diskCacheKey(ref.cacheKey) + .crossfade(crossfade) + if (size == null) { + builder.memoryCacheKey(ref.cacheKey) + } else { + builder + .size(size.width, size.height) + .memoryCacheKey("${ref.cacheKey}:${size.width}x${size.height}") + } + return builder.build() +} @Composable fun LoadingState(modifier: Modifier = Modifier) { diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt index e9d4d399..cd1d0823 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.SingletonImageLoader import coil3.compose.LocalPlatformContext +import coil3.request.CachePolicy import coil3.request.ImageRequest import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.QueueKind @@ -138,8 +139,8 @@ internal fun prefetchImageRequest( return ImageRequest .Builder(context) .data(ref.url) - .memoryCacheKey(ref.cacheKey) .diskCacheKey(ref.cacheKey) + .memoryCachePolicy(CachePolicy.DISABLED) .size(maxDimension) .build() } diff --git a/app/src/test/kotlin/com/subtracks/ui/components/CoverArtRequestTest.kt b/app/src/test/kotlin/com/subtracks/ui/components/CoverArtRequestTest.kt new file mode 100644 index 00000000..749f83c2 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/ui/components/CoverArtRequestTest.kt @@ -0,0 +1,35 @@ +package com.subtracks.ui.components + +import android.content.Context +import androidx.compose.ui.unit.IntSize +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.subtracks.data.model.CoverArtRef +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CoverArtRequestTest { + @Test + fun sizedRequestBucketsTheMemoryKeyBySizeAndKeepsTheDiskKey() { + val context = ApplicationProvider.getApplicationContext() + val ref = CoverArtRef(url = "http://example.test/art", cacheKey = "1:art:false") + + val request = imageRequest(context, ref, crossfade = false, size = IntSize(600, 800)) + + assertEquals("1:art:false:600x800", request.memoryCacheKey) + assertEquals(ref.cacheKey, request.diskCacheKey) + } + + @Test + fun unsizedRequestKeepsTheIdentityMemoryKey() { + val context = ApplicationProvider.getApplicationContext() + val ref = CoverArtRef(url = "http://example.test/art", cacheKey = "1:art:true") + + val request = imageRequest(context, ref, crossfade = false) + + assertEquals(ref.cacheKey, request.memoryCacheKey) + assertEquals(ref.cacheKey, request.diskCacheKey) + } +} diff --git a/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingArtworkTest.kt b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingArtworkTest.kt index 7c78cffc..9449464d 100644 --- a/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingArtworkTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingArtworkTest.kt @@ -5,6 +5,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import coil3.SingletonImageLoader import coil3.annotation.DelicateCoilApi +import coil3.request.CachePolicy import coil3.size.Dimension import com.subtracks.data.model.CoverArtRef import kotlinx.coroutines.runBlocking @@ -33,7 +34,7 @@ class NowPlayingArtworkTest { val metrics = context.resources.displayMetrics assertEquals(maxOf(metrics.widthPixels, metrics.heightPixels), width) - assertEquals(ref.cacheKey, request.memoryCacheKey) assertEquals(ref.cacheKey, request.diskCacheKey) + assertEquals(CachePolicy.DISABLED, request.memoryCachePolicy) } }