211 lines
5.4 KiB
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}
|