Files
jellytau/src/lib/stores/queue.ts
T
dtourolle 1836615dc0
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 3m49s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 19s
🏗️ Build and Test JellyTau / Build Android APK (pull_request) Successful in 18m24s
feat(library): genre sliders, artist links, and navigation utils
- music landing: diverse per-genre album sliders (online counts /
  offline wide-probe fallback) and home-screen library shortcuts
- add ArtistLinks component and shared navigation/genreDiversity utils
- player/playback-mode refinements across Rust and frontend
2026-06-25 19:18:06 +02:00

198 lines
5.2 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";
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;
console.log("[Queue] 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) {
console.error("[Queue] 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);