mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
decode covers at their display size and bucket the memory cache by size
This commit is contained in:
@@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
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.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.compose.LocalPlatformContext
|
import coil3.compose.LocalPlatformContext
|
||||||
@@ -73,13 +74,13 @@ fun CoverArt(
|
|||||||
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(thumbnailRef?.cacheKey?.let(ArtworkRatioCache::get)) }
|
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 =
|
val frameModifier =
|
||||||
Modifier
|
Modifier
|
||||||
.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) {
|
if (square) {
|
||||||
|
val model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null) } }
|
||||||
Box(modifier.then(frameModifier)) {
|
Box(modifier.then(frameModifier)) {
|
||||||
CoverArtContent(
|
CoverArtContent(
|
||||||
ref = ref,
|
ref = ref,
|
||||||
@@ -109,13 +110,28 @@ fun CoverArt(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
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 =
|
val painter =
|
||||||
rememberAsyncImagePainter(
|
rememberAsyncImagePainter(
|
||||||
model = model,
|
model =
|
||||||
|
remember(ref, thumbnailRef, targetSize) {
|
||||||
|
ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null, size = targetSize) }
|
||||||
|
},
|
||||||
onError = { failed = true },
|
onError = { failed = true },
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
BoxWithConstraints(modifier, contentAlignment = Alignment.Center) {
|
|
||||||
val ratio =
|
val ratio =
|
||||||
painter.intrinsicSize.ratioOrNull()
|
painter.intrinsicSize.ratioOrNull()
|
||||||
?: thumbnailRatio
|
?: 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 Size.ratioOrNull(): Float? = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else null
|
||||||
|
|
||||||
private fun imageRequest(
|
internal fun imageRequest(
|
||||||
context: Context,
|
context: Context,
|
||||||
ref: CoverArtRef,
|
ref: CoverArtRef,
|
||||||
crossfade: Boolean,
|
crossfade: Boolean,
|
||||||
): ImageRequest =
|
size: IntSize? = null,
|
||||||
|
): ImageRequest {
|
||||||
|
val builder =
|
||||||
ImageRequest
|
ImageRequest
|
||||||
.Builder(context)
|
.Builder(context)
|
||||||
.data(ref.url)
|
.data(ref.url)
|
||||||
.memoryCacheKey(ref.cacheKey)
|
|
||||||
.diskCacheKey(ref.cacheKey)
|
.diskCacheKey(ref.cacheKey)
|
||||||
.crossfade(crossfade)
|
.crossfade(crossfade)
|
||||||
.build()
|
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
|
@Composable
|
||||||
fun LoadingState(modifier: Modifier = Modifier) {
|
fun LoadingState(modifier: Modifier = Modifier) {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ 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
|
||||||
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.data.model.QueueKind
|
import com.subtracks.data.model.QueueKind
|
||||||
@@ -138,8 +139,8 @@ internal fun prefetchImageRequest(
|
|||||||
return ImageRequest
|
return ImageRequest
|
||||||
.Builder(context)
|
.Builder(context)
|
||||||
.data(ref.url)
|
.data(ref.url)
|
||||||
.memoryCacheKey(ref.cacheKey)
|
|
||||||
.diskCacheKey(ref.cacheKey)
|
.diskCacheKey(ref.cacheKey)
|
||||||
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.size(maxDimension)
|
.size(maxDimension)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Context>()
|
||||||
|
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<Context>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import androidx.test.core.app.ApplicationProvider
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.annotation.DelicateCoilApi
|
import coil3.annotation.DelicateCoilApi
|
||||||
|
import coil3.request.CachePolicy
|
||||||
import coil3.size.Dimension
|
import coil3.size.Dimension
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -33,7 +34,7 @@ class NowPlayingArtworkTest {
|
|||||||
|
|
||||||
val metrics = context.resources.displayMetrics
|
val metrics = context.resources.displayMetrics
|
||||||
assertEquals(maxOf(metrics.widthPixels, metrics.heightPixels), width)
|
assertEquals(maxOf(metrics.widthPixels, metrics.heightPixels), width)
|
||||||
assertEquals(ref.cacheKey, request.memoryCacheKey)
|
|
||||||
assertEquals(ref.cacheKey, request.diskCacheKey)
|
assertEquals(ref.cacheKey, request.diskCacheKey)
|
||||||
|
assertEquals(CachePolicy.DISABLED, request.memoryCachePolicy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user