From 096a3c0b15e35a208f11ed02133e11eb3e742b8f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 20 Jun 2026 12:29:05 -0400 Subject: [PATCH] =?UTF-8?q?fix(clients):=20never=20leave=20cover=20tiles?= =?UTF-8?q?=20blank=20=E2=80=94=20shared=20web=20=20+=20Android=20C?= =?UTF-8?q?oil=20placeholder/error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover tiles (worst in the "You might like" home row, which surfaces unplayed items whose art is often not yet backfilled) sat empty while loading and stayed blank on a 404. The server returns a fast 404; the gap was missing client-side loading/fallback states. Web: new shared Cover.svelte owns the loading placeholder + onerror fallback (static cover, or Disc3 for artists). AlbumCard, ArtistCard and CompactTrackCard now reuse it instead of three hand-rolled tags that disagreed on fallback handling — notably ArtistCard had no onerror. Android: ServerImage tracks Coil's load state so the per-caller fallback doubles as a placeholder (loading) and an error state (404 / unreachable), instead of only guarding the null-URL case. All five call sites pass an explicit size modifier, so the new Box wrapper is layout-safe. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../minstrel/shared/widgets/ServerImage.kt | 30 ++++++++- web/src/lib/components/AlbumCard.svelte | 13 +--- web/src/lib/components/ArtistCard.svelte | 21 +++--- .../lib/components/CompactTrackCard.svelte | 15 +---- web/src/lib/components/Cover.svelte | 66 +++++++++++++++++++ 5 files changed, 107 insertions(+), 38 deletions(-) create mode 100644 web/src/lib/components/Cover.svelte 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} +