feat: M3 weighted shuffle v1 — real /api/radio with scoring #21

Merged
bvandeusen merged 262 commits from dev into main 2026-04-27 11:00:29 -04:00
Showing only changes of commit 68891f1df9 - Show all commits
@@ -27,15 +27,21 @@ private const val GRADIENT_TWEEN_MS = 600
* androidx.palette on a background thread, and animates the result
* via [animateColorAsState] so track changes tween smoothly.
*
* Returns [Color.Transparent] while the bitmap is loading or on
* any failure — callers should layer a fallback (e.g.
* `MaterialTheme.colorScheme.surface`) underneath the gradient
* tint so the screen never sits on a flat black.
* The held color is NOT reset when [coverUrl] changes — it stays on
* the previous track's dominant until the new palette resolves, so the
* gradient tweens old→new directly instead of dipping toward the
* fallback mid-swap. Mirrors `now_playing_screen.dart`'s preload-then-
* swap ("keep the previous dominant"); the cover image swaps smoothly
* via `CoverPrefetcher`, which warms the next track's bytes into Coil.
*
* Starts at [Color.Transparent] (cold mount) and resets to it only
* when a track has no cover — callers layer a base color underneath so
* the screen never sits on flat black.
*/
@Composable
fun rememberDominantColor(coverUrl: String?): Color {
val context = LocalContext.current
var extracted by remember(coverUrl) { mutableStateOf(Color.Transparent) }
var extracted by remember { mutableStateOf(Color.Transparent) }
LaunchedEffect(coverUrl) {
if (coverUrl.isNullOrEmpty()) {
@@ -47,6 +53,7 @@ fun rememberDominantColor(coverUrl: String?): Color {
.allowHardware(false)
.build()
val result = SingletonImageLoader.get(context).execute(request)
// Keep the previous dominant on load failure rather than dropping.
if (result !is SuccessResult) return@LaunchedEffect
val bitmap = result.image.toBitmap()
val palette = withContext(Dispatchers.Default) {