First working POC
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
// 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.
|
||||
|
||||
import { writable, derived, get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
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
|
||||
unlisten = await listen<QueueChangedEvent>("player-event", (event) => {
|
||||
if ((event.payload as any).type === "queue_changed") {
|
||||
const queueEvent = event.payload as any;
|
||||
set({
|
||||
items: queueEvent.items,
|
||||
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 invoke<QueueChangedEvent>("player_get_queue");
|
||||
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
|
||||
|
||||
async function next() {
|
||||
await invoke("player_next");
|
||||
}
|
||||
|
||||
async function previous() {
|
||||
await invoke("player_previous");
|
||||
}
|
||||
|
||||
async function skipTo(index: number) {
|
||||
await invoke("player_skip_to", { index });
|
||||
}
|
||||
|
||||
async function toggleShuffle() {
|
||||
await invoke("player_toggle_shuffle");
|
||||
}
|
||||
|
||||
async function cycleRepeat() {
|
||||
await invoke("player_cycle_repeat");
|
||||
}
|
||||
|
||||
async function removeFromQueue(index: number) {
|
||||
await invoke("player_remove_from_queue", { index });
|
||||
}
|
||||
|
||||
async function moveInQueue(fromIndex: number, toIndex: number) {
|
||||
await invoke("player_move_in_queue", { fromIndex, toIndex });
|
||||
}
|
||||
|
||||
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 || !authState.repository) {
|
||||
throw new Error("User not authenticated");
|
||||
}
|
||||
const repositoryHandle = authState.repository.getHandle();
|
||||
|
||||
// Use new Rust commands that accept IDs only
|
||||
if (trackIds.length === 1) {
|
||||
await invoke("player_add_track_by_id", {
|
||||
repositoryHandle,
|
||||
request: {
|
||||
trackId: trackIds[0],
|
||||
position,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await invoke("player_add_tracks_by_ids", {
|
||||
repositoryHandle,
|
||||
request: {
|
||||
trackIds,
|
||||
position,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
next,
|
||||
previous,
|
||||
skipTo,
|
||||
toggleShuffle,
|
||||
cycleRepeat,
|
||||
addToQueue,
|
||||
removeFromQueue,
|
||||
moveInQueue,
|
||||
syncFromRust,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user