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>
67 lines
2.2 KiB
Svelte
67 lines
2.2 KiB
Svelte
<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>
|