Compare commits
2 Commits
deploy
...
4c50a5faab
| Author | SHA1 | Date | |
|---|---|---|---|
|
4c50a5faab
|
|||
|
b684ab790b
|
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user