fix(clients): never leave cover tiles blank — shared web <Cover> + Android Coil placeholder/error
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 <img> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { FALLBACK_COVER } from '$lib/media/covers';
|
||||
import { Disc3 } from 'lucide-svelte';
|
||||
|
||||
// Shared cover-artwork renderer. The web counterpart of Android's
|
||||
// CoverTile/ServerImage: one place that owns the loading placeholder and
|
||||
// the error fallback so no card sits blank while art loads or 404s (the
|
||||
// "You might like" row surfaces unplayed items whose art is often not yet
|
||||
// backfilled). Replaces the three hand-rolled <img> tags that previously
|
||||
// disagreed on fallback handling.
|
||||
let {
|
||||
src,
|
||||
alt = '',
|
||||
shape = 'square',
|
||||
fallback = 'image',
|
||||
class: klass = ''
|
||||
}: {
|
||||
src: string | undefined;
|
||||
alt?: string;
|
||||
// 'round' clips to a circle (artist avatars); 'square' leaves clipping
|
||||
// to the parent so existing rounded-md / rounded corners still apply.
|
||||
shape?: 'square' | 'round';
|
||||
// 'artist' degrades to a Disc3 icon; 'image' to the static placeholder
|
||||
// cover. A null/empty src is treated the same as a failed load.
|
||||
fallback?: 'image' | 'artist';
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
// Per-image lifecycle. Reset when src changes so a reused card (virtualized
|
||||
// list, client-side nav) never keeps a stale 'loaded'/'failed'.
|
||||
let loaded = $state(false);
|
||||
let failed = $state(false);
|
||||
$effect(() => {
|
||||
void src;
|
||||
loaded = false;
|
||||
failed = false;
|
||||
});
|
||||
|
||||
const useIcon = $derived(fallback === 'artist' && (failed || !src));
|
||||
// On failure for the image fallback, swap to the static placeholder asset.
|
||||
// src doesn't change on the second error, so there's no reload loop.
|
||||
const imgSrc = $derived(failed ? FALLBACK_COVER : src);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative h-full w-full overflow-hidden bg-surface-hover {shape === 'round'
|
||||
? 'rounded-full'
|
||||
: ''}"
|
||||
>
|
||||
{#if useIcon}
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<Disc3 strokeWidth={1} class="h-4/5 w-4/5 text-text-muted" />
|
||||
</div>
|
||||
{:else if imgSrc}
|
||||
<img
|
||||
src={imgSrc}
|
||||
{alt}
|
||||
class="h-full w-full object-cover transition-opacity duration-200 {loaded
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'} {klass}"
|
||||
loading="lazy"
|
||||
onload={() => (loaded = true)}
|
||||
onerror={() => (failed = true)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user