feat(web/m7-377): album detail header gets LikeButton + Play/Shuffle row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Play, Shuffle } from 'lucide-svelte';
|
||||
import { page } from '$app/state';
|
||||
import { createAlbumQuery } from '$lib/api/queries';
|
||||
import { pageTitle } from '$lib/branding';
|
||||
import TrackRow from '$lib/components/TrackRow.svelte';
|
||||
import LibrarySkeleton from '$lib/components/LibrarySkeleton.svelte';
|
||||
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
|
||||
import LikeButton from '$lib/components/LikeButton.svelte';
|
||||
import { useDelayed } from '$lib/utils/useDelayed.svelte';
|
||||
import { formatDuration } from '$lib/media/duration';
|
||||
import { FALLBACK_COVER } from '$lib/media/covers';
|
||||
import type { ApiError } from '$lib/api/client';
|
||||
import { playQueue } from '$lib/player/store.svelte';
|
||||
|
||||
const id = $derived(page.params.id ?? '');
|
||||
const queryStore = $derived(createAlbumQuery(id));
|
||||
@@ -22,6 +25,39 @@
|
||||
function onCoverError(e: Event) {
|
||||
(e.currentTarget as HTMLImageElement).src = FALLBACK_COVER;
|
||||
}
|
||||
|
||||
let isStartingPlay = $state(false);
|
||||
|
||||
function shuffleArr<T>(items: T[]): T[] {
|
||||
const arr = items.slice();
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function onPlay() {
|
||||
const data = query.data;
|
||||
if (isStartingPlay || !data || data.tracks.length === 0) return;
|
||||
isStartingPlay = true;
|
||||
try {
|
||||
playQueue(data.tracks, 0);
|
||||
} finally {
|
||||
isStartingPlay = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onShuffle() {
|
||||
const data = query.data;
|
||||
if (isStartingPlay || !data || data.tracks.length === 0) return;
|
||||
isStartingPlay = true;
|
||||
try {
|
||||
playQueue(shuffleArr(data.tracks), 0);
|
||||
} finally {
|
||||
isStartingPlay = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -55,8 +91,11 @@
|
||||
loading="lazy"
|
||||
onerror={onCoverError}
|
||||
/>
|
||||
<div class="flex-1 space-y-1">
|
||||
<h1 class="text-3xl font-semibold">{album.title}</h1>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="font-display text-2xl font-medium text-text-primary">{album.title}</h1>
|
||||
<LikeButton entityType="album" entityId={album.id} size="lg" />
|
||||
</div>
|
||||
<p>
|
||||
<a href={`/artists/${album.artist_id}`} class="hover:underline">{album.artist_name}</a>
|
||||
</p>
|
||||
@@ -67,6 +106,28 @@
|
||||
{album.track_count} {album.track_count === 1 ? 'track' : 'tracks'}
|
||||
· {formatDuration(album.duration_sec)}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Play ${album.title}`}
|
||||
onclick={onPlay}
|
||||
disabled={isStartingPlay || album.tracks.length === 0}
|
||||
class="flex items-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-medium text-text-primary hover:opacity-90 focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50"
|
||||
>
|
||||
<Play size={16} strokeWidth={1.5} fill="currentColor" />
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Shuffle ${album.title}`}
|
||||
onclick={onShuffle}
|
||||
disabled={isStartingPlay || album.tracks.length === 0}
|
||||
class="flex items-center gap-2 rounded-md border border-border bg-surface px-4 py-2 text-sm font-medium text-text-primary hover:border-accent focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50"
|
||||
>
|
||||
<Shuffle size={16} strokeWidth={1.5} />
|
||||
Shuffle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user