diff --git a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/ServerImage.kt b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/ServerImage.kt index f30b9732..0560b76e 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/ServerImage.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/ServerImage.kt @@ -1,15 +1,25 @@ package com.fabledsword.minstrel.shared.widgets +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 coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter import com.fabledsword.minstrel.shared.resolveServerUrl /** * Renders a server-hosted image, resolving relative URLs centrally so * every cover surface loads consistently. Shows [fallback] when the URL - * is blank or unresolvable. + * is blank/unresolvable, while the image is still loading, and when the + * load fails — so a tile is never left blank (e.g. art not yet backfilled, + * which the "You might like" row hits often). */ @Composable fun ServerImage( @@ -22,12 +32,26 @@ fun ServerImage( val resolved = resolveServerUrl(url) if (resolved == null) { fallback() - } else { + return + } + // Track Coil's load state so the fallback doubles as a placeholder + // (loading) and an error state (404 / unreachable) — not just a + // null-URL guard, which left present-but-failing URLs blank. + var state by remember(resolved) { + mutableStateOf(AsyncImagePainter.State.Empty) + } + Box(modifier = modifier, contentAlignment = Alignment.Center) { AsyncImage( model = resolved, contentDescription = contentDescription, - modifier = modifier, + modifier = Modifier.fillMaxSize(), contentScale = contentScale, + onState = { state = it }, ) + if (state is AsyncImagePainter.State.Loading || + state is AsyncImagePainter.State.Error + ) { + fallback() + } } } diff --git a/web/src/lib/components/AlbumCard.svelte b/web/src/lib/components/AlbumCard.svelte index 69712910..20b6426f 100644 --- a/web/src/lib/components/AlbumCard.svelte +++ b/web/src/lib/components/AlbumCard.svelte @@ -1,18 +1,14 @@ + +
+ {#if useIcon} +
+ +
+ {:else if imgSrc} + (loaded = true)} + onerror={() => (failed = true)} + /> + {/if} +