diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt b/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt index 8509852a..6831bc5c 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/AnimatedArtworkColors.kt @@ -9,12 +9,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp const val ARTWORK_THEME_TRANSITION_MS = 600 -fun baseArtworkColors(): ArtworkColors = +private val baseArtworkColors = ArtworkColors( scheme = SubtracksColorScheme, gradientHigh = SubtracksColorScheme.surfaceContainerHigh, @@ -29,15 +28,14 @@ fun rememberAnimatedArtworkColors( target: ArtworkColors?, durationMillis: Int = ARTWORK_THEME_TRANSITION_MS, ): ArtworkColors { - val base = remember { baseArtworkColors() } - var from by remember { mutableStateOf(target ?: base) } - var to by remember { mutableStateOf(target ?: base) } + var from by remember { mutableStateOf(target ?: baseArtworkColors) } + var to by remember { mutableStateOf(target ?: baseArtworkColors) } val progress = remember { Animatable(1f) } - LaunchedEffect(target) { - val next = target ?: base + LaunchedEffect(target, durationMillis) { + val next = target ?: baseArtworkColors if (next !== to) { - from = to + from = lerpArtworkColors(from, to, progress.value) to = next progress.snapTo(0f) progress.animateTo(1f, tween(durationMillis = durationMillis)) diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkSeedCache.kt b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkSeedCache.kt index 34ead2ee..e478a85d 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkSeedCache.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkSeedCache.kt @@ -2,6 +2,8 @@ package com.subtracks.ui.theme import android.content.Context import android.graphics.Bitmap +import androidx.annotation.VisibleForTesting +import androidx.collection.LruCache import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color @@ -21,27 +23,43 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap import kotlin.math.abs private const val SEED_ART_SIZE_PX = 128 private const val PREFETCH_SETTLE_MS = 250L +private const val MAX_CACHED_SEEDS = 256 object ArtworkSeedCache { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val jobs = ConcurrentHashMap?>>() + private val cache = LruCache>(MAX_CACHED_SEEDS) - fun load( + suspend fun seeds( context: Context, ref: CoverArtRef, - ): Deferred?> = - jobs.getOrPut(ref.cacheKey) { - scope.async { - val seeds = extract(context, ref) - if (seeds == null) jobs.remove(ref.cacheKey) - seeds - } - } + ): Pair? { + cache.get(ref.cacheKey)?.let { return it } + val job = jobs.computeIfAbsent(ref.cacheKey) { scope.async { extract(context, ref) } } + val seeds = job.await() + jobs.remove(ref.cacheKey, job) + if (seeds != null) cache.put(ref.cacheKey, seeds) + return seeds + } + + fun prefetch( + context: Context, + ref: CoverArtRef, + ) { + scope.launch { seeds(context, ref) } + } + + @VisibleForTesting + fun clear() { + jobs.clear() + cache.evictAll() + } private suspend fun extract( context: Context, @@ -108,6 +126,6 @@ fun PrefetchArtworkSeeds(ref: CoverArtRef?) { LaunchedEffect(ref?.cacheKey) { if (ref == null) return@LaunchedEffect delay(PREFETCH_SETTLE_MS) - ArtworkSeedCache.load(context, ref) + ArtworkSeedCache.prefetch(context, ref) } } diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt index c1ac1a37..e2d52ebd 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt @@ -141,7 +141,7 @@ private fun Color.withMinLuminance(min: Float): Color { } fun ArtworkColors.gradientColorAt(fraction: Float): Color { - if (fraction.isNaN()) return gradientHigh + if (!fraction.isFinite()) return gradientHigh val f = fraction.mod(1f) return if (f < 0.5f) { lerp(gradientHigh, gradientLow, f * 2f) @@ -173,7 +173,7 @@ fun rememberArtworkColors( private fun rememberArtworkSeed(ref: CoverArtRef?): State?> { val context = LocalPlatformContext.current return produceState?>(initialValue = null, ref?.cacheKey) { - value = ref?.let { ArtworkSeedCache.load(context, it).await() } + value = ref?.let { ArtworkSeedCache.seeds(context, it) } } } diff --git a/app/src/test/kotlin/com/subtracks/ui/theme/ArtworkExtractionTest.kt b/app/src/test/kotlin/com/subtracks/ui/theme/ArtworkExtractionTest.kt index 78f4a527..18786ab8 100644 --- a/app/src/test/kotlin/com/subtracks/ui/theme/ArtworkExtractionTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/theme/ArtworkExtractionTest.kt @@ -37,6 +37,7 @@ class ArtworkExtractionTest { @Before fun installImageLoader() { + ArtworkSeedCache.clear() SingletonImageLoader.reset() SingletonImageLoader.setUnsafe( ImageLoader @@ -48,6 +49,7 @@ class ArtworkExtractionTest { @After fun resetImageLoader() { + ArtworkSeedCache.clear() SingletonImageLoader.reset() } diff --git a/tools/__pycache__/preview_server.cpython-313.pyc b/tools/__pycache__/preview_server.cpython-313.pyc deleted file mode 100644 index d254072c..00000000 Binary files a/tools/__pycache__/preview_server.cpython-313.pyc and /dev/null differ