Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
201 lines
5.3 KiB
TypeScript
201 lines
5.3 KiB
TypeScript
// Queue state store - event-driven view of Rust player queue
|
|
//
|
|
// This store listens for queue_changed events from the Rust backend
|
|
// and provides reactive state for the frontend. All business logic
|
|
// (shuffle order, next/previous calculations, etc.) is handled by Rust.
|
|
//
|
|
// TRACES: UR-005, UR-015 | DR-005, DR-020
|
|
|
|
import { writable, derived, get } from "svelte/store";
|
|
import { commands, events } from "$lib/api/bindings";
|
|
import type { MediaItem } from "$lib/api/types";
|
|
import { auth } from "$lib/stores/auth";
|
|
import { createLogger } from "$lib/utils/logger";
|
|
|
|
const log = createLogger("Queue");
|
|
|
|
export type RepeatMode = "off" | "all" | "one";
|
|
|
|
interface QueueState {
|
|
items: MediaItem[];
|
|
currentIndex: number | null;
|
|
shuffle: boolean;
|
|
repeat: RepeatMode;
|
|
hasNext: boolean;
|
|
hasPrevious: boolean;
|
|
}
|
|
|
|
interface QueueChangedEvent {
|
|
items: MediaItem[];
|
|
currentIndex: number | null;
|
|
shuffle: boolean;
|
|
repeat: RepeatMode;
|
|
hasNext: boolean;
|
|
hasPrevious: boolean;
|
|
}
|
|
|
|
function createQueueStore() {
|
|
const initialState: QueueState = {
|
|
items: [],
|
|
currentIndex: null,
|
|
shuffle: false,
|
|
repeat: "off",
|
|
hasNext: false,
|
|
hasPrevious: false,
|
|
};
|
|
|
|
const { subscribe, set } = writable<QueueState>(initialState);
|
|
|
|
// Listen for queue changed events from Rust backend
|
|
let unlisten: (() => void) | null = null;
|
|
|
|
async function init() {
|
|
// Initial sync from backend
|
|
await syncFromRust();
|
|
|
|
// Listen for queue changed events (generated tauri-specta event)
|
|
unlisten = await events.playerStatusEvent.listen((event) => {
|
|
if (event.payload.type === "queue_changed") {
|
|
const queueEvent = event.payload;
|
|
set({
|
|
items: queueEvent.items as unknown as MediaItem[],
|
|
currentIndex: queueEvent.current_index,
|
|
shuffle: queueEvent.shuffle,
|
|
repeat: queueEvent.repeat,
|
|
hasNext: queueEvent.has_next,
|
|
hasPrevious: queueEvent.has_previous,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sync queue state from Rust backend (for initial load)
|
|
*/
|
|
async function syncFromRust(): Promise<void> {
|
|
try {
|
|
const rustQueue = (await commands.playerGetQueue()) as unknown as QueueChangedEvent;
|
|
log.debug("Synced from Rust - items:", rustQueue.items.length);
|
|
set({
|
|
items: rustQueue.items,
|
|
currentIndex: rustQueue.currentIndex,
|
|
shuffle: rustQueue.shuffle,
|
|
repeat: rustQueue.repeat,
|
|
hasNext: rustQueue.hasNext,
|
|
hasPrevious: rustQueue.hasPrevious,
|
|
});
|
|
} catch (error) {
|
|
log.error("Failed to sync from Rust:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up event listener
|
|
*/
|
|
function cleanup() {
|
|
if (unlisten) {
|
|
unlisten();
|
|
unlisten = null;
|
|
}
|
|
}
|
|
|
|
// Initialize on creation
|
|
init();
|
|
|
|
// All queue operations now invoke backend commands
|
|
// Backend handles all business logic and emits events
|
|
|
|
// TRACES: UR-005, UR-015 | DR-005
|
|
async function next() {
|
|
await commands.playerNext();
|
|
}
|
|
|
|
// TRACES: UR-005, UR-015 | DR-005
|
|
async function previous() {
|
|
await commands.playerPrevious();
|
|
}
|
|
|
|
// TRACES: UR-005, UR-015 | DR-005, DR-020
|
|
async function skipTo(index: number) {
|
|
await commands.playerSkipTo(index);
|
|
}
|
|
|
|
// TRACES: UR-005, UR-015 | DR-005
|
|
async function toggleShuffle() {
|
|
await commands.playerToggleShuffle();
|
|
}
|
|
|
|
// TRACES: UR-005, UR-015 | DR-005
|
|
async function cycleRepeat() {
|
|
await commands.playerCycleRepeat();
|
|
}
|
|
|
|
// TRACES: UR-015 | DR-020
|
|
async function removeFromQueue(index: number) {
|
|
await commands.playerRemoveFromQueue(index);
|
|
}
|
|
|
|
// TRACES: UR-015 | DR-020
|
|
async function moveInQueue(fromIndex: number, toIndex: number) {
|
|
await commands.playerMoveInQueue(fromIndex, toIndex);
|
|
}
|
|
|
|
// TRACES: UR-015 | DR-020
|
|
async function addToQueue(items: MediaItem | MediaItem[], position: "next" | "end" = "end") {
|
|
const toAdd = Array.isArray(items) ? items : [items];
|
|
const trackIds = toAdd.map((item) => item.id);
|
|
|
|
// Get repository handle from auth store
|
|
const authState = get(auth);
|
|
if (!authState.isAuthenticated) {
|
|
throw new Error("User not authenticated");
|
|
}
|
|
const repositoryHandle = auth.getRepository().getHandle();
|
|
|
|
// Use new Rust commands that accept IDs only
|
|
if (trackIds.length === 1) {
|
|
await commands.playerAddTrackById(repositoryHandle, {
|
|
trackId: trackIds[0],
|
|
position,
|
|
});
|
|
} else {
|
|
await commands.playerAddTracksByIds(repositoryHandle, {
|
|
trackIds,
|
|
position,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function clear() {
|
|
set(initialState);
|
|
}
|
|
|
|
return {
|
|
subscribe,
|
|
next,
|
|
previous,
|
|
skipTo,
|
|
toggleShuffle,
|
|
cycleRepeat,
|
|
addToQueue,
|
|
removeFromQueue,
|
|
moveInQueue,
|
|
syncFromRust,
|
|
cleanup,
|
|
clear,
|
|
};
|
|
}
|
|
|
|
export const queue = createQueueStore();
|
|
|
|
// Derived stores for convenience
|
|
export const queueItems = derived(queue, ($q) => $q.items);
|
|
export const currentQueueIndex = derived(queue, ($q) => $q.currentIndex);
|
|
export const currentQueueItem = derived(queue, ($q) =>
|
|
$q.currentIndex !== null ? $q.items[$q.currentIndex] : null,
|
|
);
|
|
export const isShuffle = derived(queue, ($q) => $q.shuffle);
|
|
export const repeatMode = derived(queue, ($q) => $q.repeat);
|
|
export const hasNext = derived(queue, ($q) => $q.hasNext);
|
|
export const hasPrevious = derived(queue, ($q) => $q.hasPrevious);
|