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>
80 lines
2.7 KiB
Svelte
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>
|