// 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(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 { 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);