Files
minstrel/web/src/lib/components/CompactTrackCard.svelte
T
bvandeusenandClaude Opus 4.8 096a3c0b15
test-web / test (push) Successful in 48s
android / Build + lint + test (push) Successful in 4m10s
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>
2026-06-20 12:29:05 -04:00

80 lines
2.7 KiB
Svelte

<script lang="ts">
import { Plus } from 'lucide-svelte';
import type { TrackRef } from '$lib/api/types';
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
import { coverUrl } from '$lib/media/covers';
import TrackMenu from './TrackMenu.svelte';
import LikeButton from './LikeButton.svelte';
import Cover from './Cover.svelte';
// Horizontal compact track row — cover thumb on the left, title +
// artist on the right. Mirrors Android's CompactTrackTile so the
// Most Played section reads the same way across platforms (three
// narrow rows that scroll together).
let {
track,
sectionTracks,
index
}: {
track: TrackRef;
sectionTracks: TrackRef[];
index: number;
} = $props();
const cover = $derived(coverUrl(track.album_id));
function onClick() {
playQueue(sectionTracks, index);
}
function onAddToQueue(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
enqueueTrack(track);
}
</script>
<div class="card group relative w-72">
<button
type="button"
aria-label={`Play ${track.title}`}
onclick={onClick}
class="flex w-full items-center gap-2 rounded-md p-1 pr-24 text-left
hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-accent"
>
<div class="h-12 w-12 flex-shrink-0 overflow-hidden rounded bg-surface-hover">
<Cover src={cover} />
</div>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium text-text-primary">{track.title}</div>
<div class="truncate text-xs text-text-secondary">{track.artist_name}</div>
</div>
</button>
<!-- Single inline cluster, vertically centred on the right. The shared
CardActionCluster splits Like+Add (top) and the menu (bottom) into
opposite corners — correct for the tall square Album/Artist cards but
it makes the two groups collide on this short one-line row. A compact
row keeps all three controls in one horizontal group instead. -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute right-2 top-1/2 z-10 flex -translate-y-1/2 items-center gap-0.5
opacity-0 transition-opacity duration-150
group-hover:opacity-100 focus-within:opacity-100"
onclick={(e) => e.stopPropagation()}
>
<LikeButton entityType="track" entityId={track.id} size="sm" />
<button
type="button"
aria-label={`Add ${track.title} to queue`}
onclick={onAddToQueue}
class="rounded-full bg-surface p-1 text-text-secondary hover:text-text-primary
focus-visible:ring-2 focus-visible:ring-accent"
>
<Plus size={16} strokeWidth={1} />
</button>
<TrackMenu {track} />
</div>
</div>