Files
amqtrain/src/routes/songs/+page.svelte

211 lines
5.4 KiB
Svelte

<script lang="ts">
import { useSearchParams } from "runed/kit";
import { onMount } from "svelte";
import { browser } from "$app/environment";
import { invalidate } from "$app/navigation";
import SongEntry from "$lib/components/SongEntry.svelte";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import {
NativeSelect,
NativeSelectOption,
} from "$lib/components/ui/native-select";
import { db as clientDb } from "$lib/db/client-db";
import { addAllToQueue, playAllNext } from "$lib/player/player.svelte";
import { trackFromSongRow } from "$lib/player/types";
import type { PageData } from "./$types";
import { SearchParamsSchemaClient } from "./schema";
const params = useSearchParams(SearchParamsSchemaClient, {
pushHistory: false,
showDefaults: false,
});
let { data }: { data: PageData } = $props();
// If SSR returned no songRows (because client DB wasn't available),
// re-run load on the client once the DB is ready by invalidating.
onMount(() => {
if (data.songRows.length > 0) return;
if (clientDb) {
void invalidate("clientdb:songs");
return;
}
});
function songArtistLabel(r: (typeof data.songRows)[number]) {
// Use animeMainName as the primary anime name for display
return r.artistName ?? r.groupName ?? null;
}
const tracksFromResults = $derived.by(() =>
data.songRows
.map((r) =>
trackFromSongRow({
annSongId: r.annSongId,
animeName: r.animeMainName, // Use animeMainName from the query result
type: r.type,
number: r.number,
songName: r.songName,
artistName: songArtistLabel(r),
fileName: r.fileName,
}),
)
.filter((t) => t !== null),
);
</script>
<h1 class="text-2xl font-semibold">Songs Search</h1>
<p class="mt-2 text-sm text-muted-foreground">
{#if !clientDb}
Loading DB...
{/if}
</p>
<form class="mt-4 flex flex-col gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="flex flex-col gap-2">
<Label for="anime-name">Anime Name</Label>
<Input
id="anime-name"
placeholder="e.g. Bakemonogatari"
bind:value={params.anime}
autocomplete="off"
spellcheck={false}
/>
</div>
<div class="flex flex-col gap-2">
<Label for="song-name">Song Name</Label>
<Input
id="song-name"
placeholder="e.g. Renai Circulation"
bind:value={params.song}
autocomplete="off"
spellcheck={false}
/>
</div>
<div class="flex flex-col gap-2">
<Label for="artist-name">Artist Name</Label>
<Input
id="artist-name"
placeholder="e.g. Kana Hanazawa"
bind:value={params.artist}
autocomplete="off"
spellcheck={false}
/>
</div>
<div class="flex flex-col gap-2">
<Label for="global-percent-min">Global Percent Range</Label>
<div class="flex gap-2">
<Input
id="global-percent-min"
type="number"
min="0"
max="100"
class="w-1/2"
placeholder="Min (0-100)"
bind:value={params.gpm}
/>
<Input
id="global-percent-max"
type="number"
min="0"
max="100"
class="w-1/2"
placeholder="Max (0-100)"
bind:value={params.gpx}
/>
</div>
</div>
<div class="flex flex-col gap-2">
<Label for="song-type">Song Type</Label>
<NativeSelect id="song-type" bind:value={params.songType}>
<NativeSelectOption value="0">All</NativeSelectOption>
<NativeSelectOption value="1">OP</NativeSelectOption>
<NativeSelectOption value="2">ED</NativeSelectOption>
<NativeSelectOption value="3">INS</NativeSelectOption>
</NativeSelect>
</div>
<div class="flex flex-col gap-2">
<Label for="songs-limit">Limit</Label>
<Input
id="songs-limit"
type="number"
min="20"
max="200"
step="20"
class="w-1/2"
bind:value={params.songsLimit}
/>
</div>
</div>
</form>
{#if data.songRows.length > 0}
<div class="mt-6 flex flex-col gap-2">
<div class="flex flex-wrap gap-2">
<Button
variant="outline"
class="cursor-pointer"
onclick={() => addAllToQueue(tracksFromResults)}
disabled={tracksFromResults.length === 0}
>
Add all to queue
</Button>
<Button
variant="outline"
class="cursor-pointer"
onclick={() => playAllNext(tracksFromResults)}
disabled={tracksFromResults.length === 0}
>
Play all next
</Button>
{#if tracksFromResults.length !== data.songRows.length}
<span class="self-center text-sm text-muted-foreground">
({tracksFromResults.length} playable)
</span>
{/if}
</div>
<div class="flex items-center gap-4">
<h2 class="text-lg font-semibold">Songs</h2>
<span>{data.songRows.length}</span>
</div>
<ul class="space-y-2">
{#each data.songRows as r (r.annSongId)}
<li>
<SongEntry
annSongId={r.annSongId}
animeName={r.animeMainName}
type={r.type}
number={r.number}
songName={r.songName}
artistName={songArtistLabel(r)}
fileName={r.fileName}
/>
</li>
{/each}
</ul>
</div>
{:else if Object.values(params).some((val) => (Array.isArray(val) && val.length > 0) || (typeof val === "string" && val.trim() !== "") || typeof val === "number")}
<p class="mt-4 text-sm text-muted-foreground">
No songs found matching your criteria.
</p>
{:else}
<p class="mt-4 text-sm text-muted-foreground">
Use the filters above to search for songs.
</p>
{/if}
{#if !browser}
Loading stuff...
{/if}