mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
bound and harden the artwork seed cache
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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<String, Deferred<Pair<Int, Int?>?>>()
|
||||
private val cache = LruCache<String, Pair<Int, Int?>>(MAX_CACHED_SEEDS)
|
||||
|
||||
fun load(
|
||||
suspend fun seeds(
|
||||
context: Context,
|
||||
ref: CoverArtRef,
|
||||
): Deferred<Pair<Int, Int?>?> =
|
||||
jobs.getOrPut(ref.cacheKey) {
|
||||
scope.async {
|
||||
val seeds = extract(context, ref)
|
||||
if (seeds == null) jobs.remove(ref.cacheKey)
|
||||
seeds
|
||||
}
|
||||
}
|
||||
): Pair<Int, Int?>? {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Pair<Int, Int?>?> {
|
||||
val context = LocalPlatformContext.current
|
||||
return produceState<Pair<Int, Int?>?>(initialValue = null, ref?.cacheKey) {
|
||||
value = ref?.let { ArtworkSeedCache.load(context, it).await() }
|
||||
value = ref?.let { ArtworkSeedCache.seeds(context, it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user