feat(vibe): reconcile mutable session previews in playback
This commit is contained in:
@@ -2,6 +2,8 @@ import { create } from 'zustand';
|
||||
import type { Track } from '../types';
|
||||
|
||||
export type RepeatMode = 'none' | 'all' | 'one';
|
||||
export type VibeAdvanceReason = 'skipped' | 'completed' | 'disliked';
|
||||
export type PlaybackOwner = 'ordinary' | 'vibe';
|
||||
|
||||
/**
|
||||
* How many already-played tracks to keep behind the cursor. Bounds queue growth
|
||||
@@ -22,12 +24,22 @@ interface PlaybackState {
|
||||
repeat: RepeatMode;
|
||||
/** Ids already played this shuffle "lap" (repeat-all), to avoid bouncing between the same few tracks. */
|
||||
shufflePlayed: Set<string>;
|
||||
/** Installed only while a durable Vibe session owns the queue. */
|
||||
vibeAdvanceHandler: ((reason: VibeAdvanceReason) => void) | null;
|
||||
/** Vibe must opt in explicitly; ordinary browsing always owns itself. */
|
||||
queueOwner: PlaybackOwner;
|
||||
|
||||
setQueue: (queue: Track[]) => void;
|
||||
/** Vibe-only queue replacement. Do not use for library browsing. */
|
||||
setVibeQueue: (queue: Track[]) => void;
|
||||
playTrack: (track: Track) => void;
|
||||
play: () => void;
|
||||
pause: () => void;
|
||||
next: () => void;
|
||||
nextWithReason: (reason: VibeAdvanceReason) => void;
|
||||
/** Bypass the Vibe controller after it has prepared the next committed track. */
|
||||
advance: () => void;
|
||||
setVibeAdvanceHandler: (handler: ((reason: VibeAdvanceReason) => void) | null) => void;
|
||||
prev: () => void;
|
||||
setPosition: (position: number) => void;
|
||||
setDuration: (duration: number) => void;
|
||||
@@ -71,6 +83,8 @@ export const usePlaybackStore = create<PlaybackState>((set, get) => ({
|
||||
shuffle: false,
|
||||
repeat: 'none',
|
||||
shufflePlayed: new Set<string>(),
|
||||
vibeAdvanceHandler: null,
|
||||
queueOwner: 'ordinary',
|
||||
|
||||
setQueue: (queue) =>
|
||||
set((state) => ({
|
||||
@@ -78,6 +92,18 @@ export const usePlaybackStore = create<PlaybackState>((set, get) => ({
|
||||
// Keep the cursor pointing at whatever is playing, if it is still queued.
|
||||
currentIndex: state.currentTrack ? queue.findIndex((t) => t.id === state.currentTrack!.id) : -1,
|
||||
shufflePlayed: new Set(),
|
||||
// Every ordinary queue operation is an explicit ownership handoff. This
|
||||
// prevents a stale Vibe session from intercepting browser/UI next.
|
||||
queueOwner: 'ordinary',
|
||||
vibeAdvanceHandler: null,
|
||||
})),
|
||||
|
||||
setVibeQueue: (queue) =>
|
||||
set((state) => ({
|
||||
queue,
|
||||
currentIndex: state.currentTrack ? queue.findIndex((t) => t.id === state.currentTrack!.id) : -1,
|
||||
shufflePlayed: new Set(),
|
||||
queueOwner: 'vibe',
|
||||
})),
|
||||
|
||||
playTrack: (track) =>
|
||||
@@ -94,6 +120,19 @@ export const usePlaybackStore = create<PlaybackState>((set, get) => ({
|
||||
pause: () => set({ isPlaying: false }),
|
||||
|
||||
next: () => {
|
||||
get().nextWithReason('skipped');
|
||||
},
|
||||
|
||||
nextWithReason: (reason) => {
|
||||
const { vibeAdvanceHandler: handler, queueOwner } = get();
|
||||
if (queueOwner === 'vibe' && handler) {
|
||||
handler(reason);
|
||||
return;
|
||||
}
|
||||
get().advance();
|
||||
},
|
||||
|
||||
advance: () => {
|
||||
const { queue, currentTrack, shuffle, repeat, shufflePlayed } = get();
|
||||
if (queue.length === 0) {
|
||||
set({ isPlaying: false, position: 0 });
|
||||
@@ -169,6 +208,11 @@ export const usePlaybackStore = create<PlaybackState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
setVibeAdvanceHandler: (vibeAdvanceHandler) => set((state) => ({
|
||||
vibeAdvanceHandler,
|
||||
queueOwner: vibeAdvanceHandler ? 'vibe' : state.queueOwner,
|
||||
})),
|
||||
|
||||
prev: () => {
|
||||
const { queue, currentTrack, currentIndex } = get();
|
||||
if (queue.length === 0) return;
|
||||
|
||||
Reference in New Issue
Block a user