EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

AlbumArt.kt

Each line, with the release that last changed it.

0.5.01package dev.stave.ui.components
2
3import androidx.compose.foundation.Image
4import androidx.compose.foundation.background
5import androidx.compose.foundation.layout.Box
6import androidx.compose.foundation.layout.fillMaxSize
7import androidx.compose.runtime.Composable
8import androidx.compose.runtime.getValue
9import androidx.compose.runtime.mutableStateOf
10import androidx.compose.runtime.remember
11import androidx.compose.runtime.setValue
12import androidx.compose.ui.Alignment
13import androidx.compose.ui.Modifier
14import androidx.compose.ui.layout.ContentScale
15import androidx.compose.ui.res.painterResource
16import coil3.compose.AsyncImage
17import dev.stave.ui.theme.SurfaceDivider
18
19/**
20 * Album art with Earhorn brand fallback. When [model] (the album art URI) is null,
21 * blank, or **fails to load** (unreachable server, corrupt image, etc.), renders a
22 * SurfaceDivider backplate with the Earhorn brand mark as a centered 50%-size
23 * watermark.
24 *
25 * Caller controls size via [modifier]; backplate and content scale are internal.
26 * Callers that want rounded corners should apply .clip() via modifier before
27 * passing in.
28 */
29@Composable
30fun AlbumArt(
31 model: Any?,
32 modifier: Modifier = Modifier,
33 contentScale: ContentScale = ContentScale.Crop,
34) {
35 val uriBlank = when (model) {
36 null -> true
37 is String -> model.isBlank()
38 else -> false
39 }
40
41 // The last model that successfully rendered. While a NEW model is
42 // loading, AsyncImage draws nothing — without this, the backplate
43 // (and brand badge) flashed through at every track boundary.
44 var lastGood by remember { mutableStateOf<Any?>(null) }
45
46 Box(
47 modifier = modifier.background(SurfaceDivider),
48 contentAlignment = Alignment.Center,
49 ) {
50 if (uriBlank) {
51 Image(
52 painter = painterResource(id = dev.stave.R.drawable.earhorn_badge),
53 contentDescription = null,
54 modifier = Modifier.fillMaxSize(0.5f),
55 )
56 } else {
57 var loadFailed by remember(model) { mutableStateOf(false) }
58 if (loadFailed && lastGood == null) {
59 Image(
60 painter = painterResource(id = dev.stave.R.drawable.earhorn_badge),
61 contentDescription = null,
62 modifier = Modifier.fillMaxSize(0.5f),
63 )
64 }
65 // Previous art underlay — drawn while the current model
66 // differs from the last successful one (a load is in flight
67 // or failed). Coil serves it from memory cache, so it stays
68 // painted with no visible gap: same-art transitions show no
69 // change at all; new-art transitions swap old→new directly
70 // with no interstitial backplate/badge flash.
71 if (lastGood != null && lastGood != model) {
72 AsyncImage(
73 model = lastGood,
74 contentDescription = null,
75 contentScale = contentScale,
76 modifier = Modifier.fillMaxSize(),
77 )
78 }
79 AsyncImage(
80 model = model,
81 contentDescription = null,
82 contentScale = contentScale,
83 modifier = Modifier.fillMaxSize(),
84 onError = { loadFailed = true },
85 onSuccess = {
86 loadFailed = false
87 lastGood = model
88 },
89 )
90 }
91 }
92}