import { create } from 'zustand'; import type { Track } from '../types'; export type RepeatMode = 'none' | 'all' | 'one'; interface PlaybackState { currentTrack: Track | null; queue: Track[]; isPlaying: boolean; position: number; duration: number; volume: number; shuffle: boolean; repeat: RepeatMode; /** Ids already played this shuffle "lap" (repeat-all), to avoid bouncing between the same few tracks. */ shufflePlayed: Set; setQueue: (queue: Track[]) => void; playTrack: (track: Track) => void; play: () => void; pause: () => void; next: () => void; prev: () => void; setPosition: (position: number) => void; setDuration: (duration: number) => void; setVolume: (volume: number) => void; setCurrentTrack: (track: Track | null) => void; toggleShuffle: () => void; cycleRepeat: () => void; } export const usePlaybackStore = create((set, get) => ({ currentTrack: null, queue: [], isPlaying: false, position: 0, duration: 0, volume: 1, shuffle: false, repeat: 'none', shufflePlayed: new Set(), setQueue: (queue) => set({ queue, shufflePlayed: new Set() }), playTrack: (track) => set({ currentTrack: track, isPlaying: true, position: 0, duration: track.duration ?? 0, shufflePlayed: new Set(), }), play: () => set({ isPlaying: true }), pause: () => set({ isPlaying: false }), next: () => { const { queue, currentTrack, shuffle, repeat, shufflePlayed } = get(); if (queue.length === 0) return; const idx = currentTrack ? queue.findIndex((t) => t.id === currentTrack.id) : -1; // Repeat one: replay current track if (repeat === 'one' && currentTrack) { set({ position: 0, isPlaying: true }); return; } if (shuffle) { // Shuffle: pick a random track not yet played this lap (excludes current), // so repeat-all doesn't bounce between the same few tracks. const played = new Set(shufflePlayed); if (currentTrack) played.add(currentTrack.id); let remaining = queue.filter((t) => !played.has(t.id)); if (remaining.length === 0) { if (repeat !== 'all') return; // Lap complete — start a fresh one. played.clear(); if (currentTrack) played.add(currentTrack.id); remaining = queue.filter((t) => t.id !== currentTrack?.id); if (remaining.length === 0) return; } const pick = remaining[Math.floor(Math.random() * remaining.length)]; played.add(pick.id); set({ currentTrack: pick, shufflePlayed: played, position: 0, duration: pick.duration ?? 0, isPlaying: true }); return; } // Sequential const nextTrack = idx >= 0 ? queue[idx + 1] : null; if (nextTrack) { // Trim played tracks from queue to prevent unbounded growth (Vibe prefetch leak) const trimmed = queue.slice(idx + 1); set({ queue: trimmed, currentTrack: nextTrack, position: 0, duration: nextTrack.duration ?? 0, isPlaying: true }); } else if (repeat === 'all') { const first = queue[0]; if (first) { set({ currentTrack: first, position: 0, duration: first.duration ?? 0, isPlaying: true }); } } }, prev: () => { const { queue, currentTrack } = get(); if (queue.length === 0) return; const idx = currentTrack ? queue.findIndex((t) => t.id === currentTrack.id) : -1; const prevTrack = idx > 0 ? queue[idx - 1] : null; if (prevTrack) { set({ currentTrack: prevTrack, position: 0, duration: prevTrack.duration ?? 0, isPlaying: true }); } }, setPosition: (position) => set({ position }), setDuration: (duration) => set({ duration }), setVolume: (volume) => set({ volume }), setCurrentTrack: (currentTrack) => set({ currentTrack }), toggleShuffle: () => set((state) => ({ shuffle: !state.shuffle })), cycleRepeat: () => set((state) => { const modes: RepeatMode[] = ['none', 'all', 'one']; const next = modes[(modes.indexOf(state.repeat) + 1) % modes.length]; return { repeat: next }; }), }));