TRACES: | DR-204 484 ungated `console.*` calls across 63 non-test frontend files shipped to end users with no way to turn them off. Mechanical substitution, no control flow, error handling or message semantics changed: console.log / console.debug -> log.debug console.info -> log.info console.warn -> log.warn console.error -> log.error Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope now carries them; scope names that already existed are preserved verbatim (`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename where a file had none. `src/routes/player/[id]/+page.svelte` keeps its `NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than flattening them into the page scope. `grep -rn 'console\.' src/` now matches nothing outside the tests and the facade itself.
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);
|