feat(web): add play + like to artist detail header
Mirrors the play overlay on ArtistCard from /library/artists: fetches every track for the artist (server already filters per-user lidarr_quarantine), shuffles client-side, kicks off playback. Adds a LikeButton next to it with entityType="artist" so the operator can favourite an artist from this surface without bouncing back to the library grid. Play button is disabled when album_count === 0 or while the fetch is in flight, so a double-tap can't start two queues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Play } from 'lucide-svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { createArtistQuery } from '$lib/api/queries';
|
import { createArtistQuery } from '$lib/api/queries';
|
||||||
import AlbumCard from '$lib/components/AlbumCard.svelte';
|
import AlbumCard from '$lib/components/AlbumCard.svelte';
|
||||||
|
import LikeButton from '$lib/components/LikeButton.svelte';
|
||||||
import LibrarySkeleton from '$lib/components/LibrarySkeleton.svelte';
|
import LibrarySkeleton from '$lib/components/LibrarySkeleton.svelte';
|
||||||
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
|
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
|
||||||
import { useDelayed } from '$lib/utils/useDelayed.svelte';
|
import { useDelayed } from '$lib/utils/useDelayed.svelte';
|
||||||
|
import { api } from '$lib/api/client';
|
||||||
|
import { playQueue } from '$lib/player/store.svelte';
|
||||||
import type { ApiError } from '$lib/api/client';
|
import type { ApiError } from '$lib/api/client';
|
||||||
|
import type { TrackRef } from '$lib/api/types';
|
||||||
|
|
||||||
const id = $derived(page.params.id ?? '');
|
const id = $derived(page.params.id ?? '');
|
||||||
const queryStore = $derived(createArtistQuery(id));
|
const queryStore = $derived(createArtistQuery(id));
|
||||||
@@ -15,6 +20,32 @@
|
|||||||
const notFound = $derived(
|
const notFound = $derived(
|
||||||
query.isError && (query.error as unknown as ApiError | undefined)?.code === 'not_found'
|
query.isError && (query.error as unknown as ApiError | undefined)?.code === 'not_found'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Mirrors ArtistCard's play overlay: fetch every track for the artist
|
||||||
|
// (server already honours per-user lidarr_quarantine), shuffle client-side,
|
||||||
|
// start the queue. Bypassed when the artist has nothing to play.
|
||||||
|
function shuffle<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isStartingPlay = $state(false);
|
||||||
|
|
||||||
|
async function onPlay() {
|
||||||
|
if (isStartingPlay || !id) return;
|
||||||
|
isStartingPlay = true;
|
||||||
|
try {
|
||||||
|
const tracks = await api.get<TrackRef[]>(`/api/artists/${id}/tracks`);
|
||||||
|
if (tracks.length === 0) return;
|
||||||
|
playQueue(shuffle(tracks), 0);
|
||||||
|
} finally {
|
||||||
|
isStartingPlay = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@@ -31,11 +62,23 @@
|
|||||||
<LibrarySkeleton variant="grid" count={12} />
|
<LibrarySkeleton variant="grid" count={12} />
|
||||||
{:else if query.data}
|
{:else if query.data}
|
||||||
{@const detail = query.data}
|
{@const detail = query.data}
|
||||||
<header>
|
<header class="flex items-center gap-4">
|
||||||
<h1 class="text-2xl font-semibold">{detail.name}</h1>
|
<div class="min-w-0 flex-1">
|
||||||
|
<h1 class="font-display text-2xl font-medium text-text-primary">{detail.name}</h1>
|
||||||
<p class="text-sm text-text-secondary">
|
<p class="text-sm text-text-secondary">
|
||||||
{detail.album_count} {detail.album_count === 1 ? 'album' : 'albums'}
|
{detail.album_count} {detail.album_count === 1 ? 'album' : 'albums'}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={`Play ${detail.name}`}
|
||||||
|
onclick={onPlay}
|
||||||
|
disabled={isStartingPlay || detail.album_count === 0}
|
||||||
|
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50 disabled:hover:scale-100"
|
||||||
|
>
|
||||||
|
<Play size={24} strokeWidth={1.5} fill="currentColor" class="ml-0.5" />
|
||||||
|
</button>
|
||||||
|
<LikeButton entityType="artist" entityId={detail.id} />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if detail.albums.length === 0}
|
{#if detail.albums.length === 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user