2 Commits

2 changed files with 151 additions and 32 deletions

View File

@@ -1,9 +1,18 @@
<script lang="ts"> <script lang="ts">
import { GripVertical, LocateFixed, Play, X } from "@lucide/svelte"; import {
Download,
GripVertical,
LocateFixed,
Play,
Upload,
X,
} from "@lucide/svelte";
import { tick } from "svelte"; import { tick } from "svelte";
import { z } from "zod";
import * as AlertDialog from "$lib/components/ui/alert-dialog"; import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import VirtualList from "$lib/components/ui/VirtualList.svelte"; import VirtualList from "$lib/components/ui/VirtualList.svelte";
import { trackSchema } from "$lib/player/persist";
import { player } from "$lib/player/store.svelte"; import { player } from "$lib/player/store.svelte";
import type { Track } from "$lib/player/types"; import type { Track } from "$lib/player/types";
import { songTypeNumberLabel } from "$lib/utils/amq"; import { songTypeNumberLabel } from "$lib/utils/amq";
@@ -59,6 +68,54 @@
player.move(fromIndex, toIndex); player.move(fromIndex, toIndex);
} }
} }
function exportQueue() {
const dataStr = JSON.stringify(player.queue, null, 2);
const dataUri =
"data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
const exportFileDefaultName = "amq-queue.json";
const linkElement = document.createElement("a");
linkElement.setAttribute("href", dataUri);
linkElement.setAttribute("download", exportFileDefaultName);
linkElement.click();
}
let fileInput: HTMLInputElement;
function importQueue(e: Event) {
const input = e.target as HTMLInputElement;
if (!input.files?.length) return;
const file = input.files[0];
const reader = new FileReader();
reader.onload = (event) => {
try {
const content = event.target?.result as string;
const parsed = JSON.parse(content);
const result = z.array(trackSchema).safeParse(parsed);
if (result.success) {
player.clearQueue();
player.isShuffled = false;
player.repeatMode = "off";
player.addAll(result.data);
} else {
console.error("Invalid queue format", result.error);
alert("Failed to import: Invalid queue format.");
}
} catch (err) {
console.error("Error reading file", err);
alert("Failed to read file.");
}
// Reset input so the same file can be selected again
input.value = "";
};
reader.readAsText(file);
}
</script> </script>
<div <div
@@ -84,29 +141,65 @@
</Button> </Button>
{/if} {/if}
</div> </div>
<AlertDialog.Root> <div class="flex items-center gap-1 text-muted-foreground pl-2">
<AlertDialog.Trigger> <input
{#snippet child({ props })} type="file"
<Button variant="ghost" size="sm" class="h-6 w-6 p-0" {...props}> accept=".json"
<X class="h-3 w-3" aria-label="Clear" /> bind:this={fileInput}
</Button> onchange={importQueue}
{/snippet} class="hidden"
</AlertDialog.Trigger> />
<AlertDialog.Content> <Button
<AlertDialog.Header> variant="ghost"
<AlertDialog.Title>Clear queue?</AlertDialog.Title> size="sm"
<AlertDialog.Description> class="h-6 w-6 p-0"
This will remove all {player.displayQueue.length} songs from the queue. aria-label="Import Queue"
</AlertDialog.Description> onclick={() => fileInput?.click()}
</AlertDialog.Header> title="Import Queue"
<AlertDialog.Footer> >
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel> <Upload class="h-3 w-3" />
<AlertDialog.Action onclick={() => player.clearQueue()} </Button>
>Clear</AlertDialog.Action <Button
> variant="ghost"
</AlertDialog.Footer> size="sm"
</AlertDialog.Content> class="h-6 w-6 p-0"
</AlertDialog.Root> aria-label="Export Queue"
onclick={exportQueue}
disabled={player.queue.length === 0}
title="Export Queue"
>
<Download class="h-3 w-3" />
</Button>
<AlertDialog.Root>
<AlertDialog.Trigger>
{#snippet child({ props })}
<Button
variant="ghost"
size="sm"
class="h-6 w-6 p-0"
{...props}
title="Clear Queue"
>
<X class="h-3 w-3" aria-label="Clear" />
</Button>
{/snippet}
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Clear queue?</AlertDialog.Title>
<AlertDialog.Description>
This will remove all {player.displayQueue.length} songs from the queue.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
<AlertDialog.Action onclick={() => player.clearQueue()}
>Clear</AlertDialog.Action
>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
</div>
</div> </div>
<VirtualList <VirtualList

View File

@@ -1,22 +1,48 @@
import { z } from "zod";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import type { Track } from "./types"; import type { Track } from "./types";
const STORAGE_KEY = "amqtrain:player:v2"; const STORAGE_KEY = "amqtrain:player:v2";
export type PersistedState = { export const trackSchema: z.ZodType<Track> = z.object({
queue: Track[]; id: z.number(),
currentId: number | null; src: z.string(),
volume: number; title: z.string(),
isMuted: boolean; artist: z.string(),
minimized: boolean; album: z.string(),
}; animeName: z.string().optional(),
type: z.number(),
number: z.number(),
fileName: z.string().nullable().optional(),
dub: z.boolean(),
rebroadcast: z.boolean(),
globalPercent: z.number(),
});
export const persistedStateSchema = z.object({
queue: z.array(trackSchema),
currentId: z.number().nullable(),
volume: z.number(),
isMuted: z.boolean(),
minimized: z.boolean(),
});
export type PersistedState = z.infer<typeof persistedStateSchema>;
export function loadState(): PersistedState | null { export function loadState(): PersistedState | null {
if (!browser) return null; if (!browser) return null;
try { try {
const raw = localStorage.getItem(STORAGE_KEY); const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null; if (!raw) return null;
return JSON.parse(raw);
const parsed = JSON.parse(raw);
const result = persistedStateSchema.safeParse(parsed);
if (result.success) {
return result.data;
}
console.error("Failed to parse player state", result.error);
return null;
} catch (e) { } catch (e) {
console.error("Failed to load player state", e); console.error("Failed to load player state", e);
return null; return null;