package dev.stave.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import coil3.compose.AsyncImage
import dev.stave.ui.theme.SurfaceDivider
/**
* Album art with Earhorn brand fallback. When [model] (the album art URI) is null,
* blank, or **fails to load** (unreachable server, corrupt image, etc.), renders a
* SurfaceDivider backplate with the Earhorn brand mark as a centered 50%-size
* watermark.
*
* Caller controls size via [modifier]; backplate and content scale are internal.
* Callers that want rounded corners should apply .clip() via modifier before
* passing in.
*/
@Composable
fun AlbumArt(
model: Any?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Crop,
) {
val uriBlank = when (model) {
null -> true
is String -> model.isBlank()
else -> false
}
// The last model that successfully rendered. While a NEW model is
// loading, AsyncImage draws nothing — without this, the backplate
// (and brand badge) flashed through at every track boundary.
var lastGood by remember { mutableStateOf<Any?>(null) }
Box(
modifier = modifier.background(SurfaceDivider),
contentAlignment = Alignment.Center,
) {
if (uriBlank) {
Image(
painter = painterResource(id = dev.stave.R.drawable.earhorn_badge),
contentDescription = null,
modifier = Modifier.fillMaxSize(0.5f),
)
} else {
var loadFailed by remember(model) { mutableStateOf(false) }
if (loadFailed && lastGood == null) {
Image(
painter = painterResource(id = dev.stave.R.drawable.earhorn_badge),
contentDescription = null,
modifier = Modifier.fillMaxSize(0.5f),
)
}
// Previous art underlay — drawn while the current model
// differs from the last successful one (a load is in flight
// or failed). Coil serves it from memory cache, so it stays
// painted with no visible gap: same-art transitions show no
// change at all; new-art transitions swap old→new directly
// with no interstitial backplate/badge flash.
if (lastGood != null && lastGood != model) {
AsyncImage(
model = lastGood,
contentDescription = null,
contentScale = contentScale,
modifier = Modifier.fillMaxSize(),
)
}
AsyncImage(
model = model,
contentDescription = null,
contentScale = contentScale,
modifier = Modifier.fillMaxSize(),
onError = { loadFailed = true },
onSuccess = {
loadFailed = false
lastGood = model
},
)
}
}
}