176 lines
5.0 KiB
Svelte
176 lines
5.0 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from "svelte";
|
||
import { invalidate } from "$app/navigation";
|
||
import SongEntry from "$lib/components/SongEntry.svelte";
|
||
import { db as clientDb } from "$lib/db/client-db";
|
||
import { player } from "$lib/player/store.svelte";
|
||
import { trackFromSongRow } from "$lib/player/types";
|
||
import { seasonName } from "$lib/utils/amq";
|
||
import type { PageData } from "./$types";
|
||
|
||
let { data }: { data: PageData } = $props();
|
||
|
||
// If SSR returned null (because client DB wasn't available),
|
||
// re-run load on the client once the DB is ready by invalidating.
|
||
onMount(() => {
|
||
if (data.animeWithSongs) return;
|
||
|
||
// Invalid route param -> nothing to hydrate
|
||
if (!data.annId) return;
|
||
|
||
if (clientDb) {
|
||
void invalidate("clientdb:songs");
|
||
return;
|
||
}
|
||
});
|
||
|
||
function playableTracks() {
|
||
const rows = data.animeWithSongs?.songs ?? [];
|
||
return rows
|
||
.map((s) =>
|
||
trackFromSongRow({
|
||
annSongId: s.annSongId,
|
||
animeName: data.animeWithSongs?.anime.mainName ?? "Unknown Anime",
|
||
type: s.type,
|
||
number: s.number,
|
||
songName: s.songName,
|
||
artistName: s.artistName,
|
||
fileName: s.fileName ?? null,
|
||
}),
|
||
)
|
||
.filter((t) => t !== null);
|
||
}
|
||
|
||
function queueAll() {
|
||
player.addAll(playableTracks());
|
||
}
|
||
|
||
function playAllNextFromAnime() {
|
||
player.playAllNext(playableTracks());
|
||
}
|
||
</script>
|
||
|
||
{#if !clientDb}
|
||
<p class="mt-3 text-sm text-muted-foreground">Loading DB...</p>
|
||
{/if}
|
||
|
||
{#if !data.annId}
|
||
<h1 class="text-2xl font-semibold">Anime not found</h1>
|
||
<p class="mt-2 text-sm text-muted-foreground">
|
||
The requested anime entry doesn’t exist (or the route param wasn’t a
|
||
valid ANN id).
|
||
</p>
|
||
{:else if !data.animeWithSongs}
|
||
<p class="mt-3 text-sm text-muted-foreground">Loading anime…</p>
|
||
{:else}
|
||
<header class="mt-2 space-y-2">
|
||
<h1 class="text-2xl font-semibold">
|
||
{data.animeWithSongs.anime.mainName}
|
||
</h1>
|
||
|
||
<p class="text-sm text-muted-foreground">
|
||
{data.animeWithSongs.anime.year}
|
||
{seasonName(Number(data.animeWithSongs.anime.seasonId))}
|
||
</p>
|
||
|
||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||
<a
|
||
class="underline underline-offset-4 hover:no-underline text-muted-foreground hover:text-foreground"
|
||
href={`https://www.animenewsnetwork.com/encyclopedia/anime.php?id=${data.animeWithSongs.anime.annId}`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>
|
||
ANN
|
||
</a>
|
||
|
||
{#if data.animeWithSongs.anime.malId != null}
|
||
<a
|
||
class="underline underline-offset-4 hover:no-underline text-muted-foreground hover:text-foreground"
|
||
href={`https://myanimelist.net/anime/${data.animeWithSongs.anime.malId}/`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>
|
||
MAL
|
||
</a>
|
||
{:else}
|
||
<span
|
||
class="cursor-not-allowed select-none text-muted-foreground/60 underline underline-offset-4 decoration-dotted"
|
||
title="MAL link unavailable (missing MAL id)"
|
||
aria-disabled="true"
|
||
>
|
||
MAL
|
||
</span>
|
||
{/if}
|
||
|
||
{#if data.animeWithSongs.anime.aniListId != null}
|
||
<a
|
||
class="underline underline-offset-4 hover:no-underline text-muted-foreground hover:text-foreground"
|
||
href={`https://anilist.co/anime/${data.animeWithSongs.anime.aniListId}`}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>
|
||
AniList
|
||
</a>
|
||
{:else}
|
||
<span
|
||
class="cursor-not-allowed select-none text-muted-foreground/60 underline underline-offset-4 decoration-dotted"
|
||
title="AniList link unavailable (missing AniList id)"
|
||
aria-disabled="true"
|
||
>
|
||
AniList
|
||
</span>
|
||
{/if}
|
||
</div>
|
||
</header>
|
||
|
||
<section class="mt-6">
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<h2 class="text-lg font-semibold">Songs</h2>
|
||
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<button
|
||
type="button"
|
||
class="inline-flex h-9 items-center justify-center rounded-md border px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
||
onclick={queueAll}
|
||
disabled={data.animeWithSongs.songs.length === 0}
|
||
title="Add all playable songs from this anime to the end of the queue"
|
||
>
|
||
Queue all
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="inline-flex h-9 items-center justify-center rounded-md border px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
||
onclick={playAllNextFromAnime}
|
||
disabled={data.animeWithSongs.songs.length === 0}
|
||
title="Insert all playable songs from this anime right after the current track"
|
||
>
|
||
Play all next
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{#if data.animeWithSongs.songs.length === 0}
|
||
<p class="mt-2 text-sm text-muted-foreground">
|
||
No linked songs found for this anime.
|
||
</p>
|
||
{:else}
|
||
<ul class="mt-3 space-y-3">
|
||
{#each data.animeWithSongs.songs as s (s.annSongId)}
|
||
<li>
|
||
<SongEntry
|
||
annSongId={s.annSongId}
|
||
animeName={data.animeWithSongs.anime.mainName}
|
||
type={s.type}
|
||
number={s.number}
|
||
songName={s.songName}
|
||
artistName={s.artistName}
|
||
fileName={s.fileName}
|
||
globalPercent={s.globalPercent}
|
||
/>
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
{/if}
|
||
</section>
|
||
{/if}
|