mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(player): E.33 — extract queue mutation actions as factory (#597)
Move all eleven queue-mutation actions (enqueue, enqueueAt, playNext, enqueueRadio, setRadioArtistId, pruneUpcomingToCurrent, clearQueue, reorderQueue, shuffleQueue, shuffleUpcomingQueue, removeTrack) from the playerStore create() body into a new `createQueueMutationActions` factory, following the action-factory pattern established in E.31 (Last.fm) + E.32 (UI state). Pure code-move: existing characterization tests in playerStore.queue.test.ts continue to exercise the actions through the full store flow. playerStore.ts: 1463 → 1247 LOC (−216).
This commit is contained in:
committed by
GitHub
parent
0c2aa993f2
commit
168d5905c2
+2
-218
@@ -104,19 +104,15 @@ import {
|
|||||||
} from './engineState';
|
} from './engineState';
|
||||||
import {
|
import {
|
||||||
addRadioSessionSeen,
|
addRadioSessionSeen,
|
||||||
clearRadioSessionSeenIds,
|
|
||||||
deleteRadioSessionSeen,
|
|
||||||
getCurrentRadioArtistId,
|
getCurrentRadioArtistId,
|
||||||
hasRadioSessionSeen,
|
hasRadioSessionSeen,
|
||||||
isRadioFetching,
|
isRadioFetching,
|
||||||
setCurrentRadioArtistId,
|
|
||||||
setRadioFetching,
|
setRadioFetching,
|
||||||
} from './radioSessionState';
|
} from './radioSessionState';
|
||||||
import {
|
import {
|
||||||
isInfiniteQueueFetching,
|
isInfiniteQueueFetching,
|
||||||
setInfiniteQueueFetching,
|
setInfiniteQueueFetching,
|
||||||
} from './infiniteQueueState';
|
} from './infiniteQueueState';
|
||||||
import { prefetchLoudnessForEnqueuedTracks } from './loudnessPrefetch';
|
|
||||||
import { initAudioListeners } from './initAudioListeners';
|
import { initAudioListeners } from './initAudioListeners';
|
||||||
import { installQueueUndoHotkey } from './queueUndoHotkey';
|
import { installQueueUndoHotkey } from './queueUndoHotkey';
|
||||||
import { readInitialQueueVisibility } from './queueVisibilityStorage';
|
import { readInitialQueueVisibility } from './queueVisibilityStorage';
|
||||||
@@ -172,6 +168,7 @@ import type { PlayerState, Track } from './playerStoreTypes';
|
|||||||
export type { PlayerState, Track };
|
export type { PlayerState, Track };
|
||||||
import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot';
|
import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot';
|
||||||
import { createLastfmActions } from './lastfmActions';
|
import { createLastfmActions } from './lastfmActions';
|
||||||
|
import { createQueueMutationActions } from './queueMutationActions';
|
||||||
import { createUiStateActions } from './uiStateActions';
|
import { createUiStateActions } from './uiStateActions';
|
||||||
|
|
||||||
|
|
||||||
@@ -223,6 +220,7 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
|
|
||||||
...createUiStateActions(set),
|
...createUiStateActions(set),
|
||||||
...createLastfmActions(set, get),
|
...createLastfmActions(set, get),
|
||||||
|
...createQueueMutationActions(set, get),
|
||||||
|
|
||||||
// ── stop ────────────────────────────────────────────────────────────────
|
// ── stop ────────────────────────────────────────────────────────────────
|
||||||
stop: () => {
|
stop: () => {
|
||||||
@@ -593,27 +591,6 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
if (!wasPlaying) get().resume();
|
if (!wasPlaying) get().resume();
|
||||||
},
|
},
|
||||||
|
|
||||||
pruneUpcomingToCurrent: () => {
|
|
||||||
const s = get();
|
|
||||||
if (s.currentRadio) return;
|
|
||||||
if (!s.currentTrack) {
|
|
||||||
if (s.queue.length === 0) return;
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
set({ queue: [], queueIndex: 0 });
|
|
||||||
syncQueueToServer([], null, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
const at = s.queue.findIndex(t => t.id === s.currentTrack!.id);
|
|
||||||
const newQueue: Track[] =
|
|
||||||
at >= 0
|
|
||||||
? s.queue.slice(0, at + 1)
|
|
||||||
: [s.currentTrack!];
|
|
||||||
const newIndex = at >= 0 ? at : 0;
|
|
||||||
set({ queue: newQueue, queueIndex: newIndex });
|
|
||||||
syncQueueToServer(newQueue, s.currentTrack, s.currentTime);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── pause / resume / togglePlay ──────────────────────────────────────────
|
// ── pause / resume / togglePlay ──────────────────────────────────────────
|
||||||
pause: () => {
|
pause: () => {
|
||||||
clearAllPlaybackScheduleTimers();
|
clearAllPlaybackScheduleTimers();
|
||||||
@@ -1123,190 +1100,6 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── queue management ─────────────────────────────────────────────────────
|
// ── queue management ─────────────────────────────────────────────────────
|
||||||
enqueue: (tracks, _orbitConfirmed = false) => {
|
|
||||||
if (!_orbitConfirmed && tracks.length > 1) {
|
|
||||||
void orbitBulkGuard(tracks.length).then(ok => {
|
|
||||||
if (ok) get().enqueue(tracks, true);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
set(state => {
|
|
||||||
// Insert before the first upcoming auto-added track so the
|
|
||||||
// "Added automatically" separator always stays at the boundary.
|
|
||||||
const firstAutoIdx = state.queue.findIndex(
|
|
||||||
(t, i) => t.autoAdded && i > state.queueIndex
|
|
||||||
);
|
|
||||||
const newQueue = firstAutoIdx === -1
|
|
||||||
? [...state.queue, ...tracks]
|
|
||||||
: [
|
|
||||||
...state.queue.slice(0, firstAutoIdx),
|
|
||||||
...tracks,
|
|
||||||
...state.queue.slice(firstAutoIdx),
|
|
||||||
];
|
|
||||||
syncQueueToServer(newQueue, state.currentTrack, state.currentTime);
|
|
||||||
prefetchLoudnessForEnqueuedTracks(newQueue, state.queueIndex);
|
|
||||||
return { queue: newQueue };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setRadioArtistId: (artistId) => {
|
|
||||||
if (artistId !== getCurrentRadioArtistId()) {
|
|
||||||
clearRadioSessionSeenIds();
|
|
||||||
}
|
|
||||||
setCurrentRadioArtistId(artistId);
|
|
||||||
},
|
|
||||||
|
|
||||||
enqueueRadio: (tracks, artistId) => {
|
|
||||||
if (artistId !== undefined) {
|
|
||||||
if (artistId !== getCurrentRadioArtistId()) {
|
|
||||||
clearRadioSessionSeenIds();
|
|
||||||
}
|
|
||||||
setCurrentRadioArtistId(artistId);
|
|
||||||
}
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
set(state => {
|
|
||||||
// Drop all upcoming (not yet played) radio tracks — clicking "Start Radio"
|
|
||||||
// again replaces the pending radio batch instead of stacking on top.
|
|
||||||
const beforeAndCurrent = state.queue.slice(0, state.queueIndex + 1);
|
|
||||||
const upcoming = state.queue.slice(state.queueIndex + 1).filter(t => !t.radioAdded);
|
|
||||||
// Tracks about to leave the queue here. Callers like ContextMenu.startRadio
|
|
||||||
// pass the previous pending radio back in `tracks` to merge with new
|
|
||||||
// similars — the seen-set must not block those re-introductions.
|
|
||||||
const droppedRadioIds = state.queue
|
|
||||||
.slice(state.queueIndex + 1)
|
|
||||||
.filter(t => t.radioAdded)
|
|
||||||
.map(t => t.id);
|
|
||||||
for (const id of droppedRadioIds) deleteRadioSessionSeen(id);
|
|
||||||
// Capture surviving queue ids in the seen-set so the next radio top-up
|
|
||||||
// can dedupe against the seed track + already-queued non-radio items.
|
|
||||||
for (const t of beforeAndCurrent) addRadioSessionSeen(t.id);
|
|
||||||
for (const t of upcoming) addRadioSessionSeen(t.id);
|
|
||||||
// Drop incoming tracks already seen earlier this session AND
|
|
||||||
// intra-batch duplicates (top + similar Last.fm responses commonly
|
|
||||||
// overlap). The seen-set is mutated inside the loop so a repeated
|
|
||||||
// id later in `tracks` is rejected by the same pass that admitted
|
|
||||||
// the first occurrence (issue #500).
|
|
||||||
const dedupedTracks: Track[] = [];
|
|
||||||
for (const t of tracks) {
|
|
||||||
if (hasRadioSessionSeen(t.id)) continue;
|
|
||||||
addRadioSessionSeen(t.id);
|
|
||||||
dedupedTracks.push(t);
|
|
||||||
}
|
|
||||||
// Insert new radio tracks before any autoAdded tracks in the upcoming section.
|
|
||||||
const firstAutoIdx = upcoming.findIndex(t => t.autoAdded);
|
|
||||||
const merged = firstAutoIdx === -1
|
|
||||||
? [...upcoming, ...dedupedTracks]
|
|
||||||
: [
|
|
||||||
...upcoming.slice(0, firstAutoIdx),
|
|
||||||
...dedupedTracks,
|
|
||||||
...upcoming.slice(firstAutoIdx),
|
|
||||||
];
|
|
||||||
const newQueue = [...beforeAndCurrent, ...merged];
|
|
||||||
syncQueueToServer(newQueue, state.currentTrack, state.currentTime);
|
|
||||||
return { queue: newQueue };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
enqueueAt: (tracks, insertIndex, _orbitConfirmed = false) => {
|
|
||||||
if (!_orbitConfirmed && tracks.length > 1) {
|
|
||||||
void orbitBulkGuard(tracks.length).then(ok => {
|
|
||||||
if (ok) get().enqueueAt(tracks, insertIndex, true);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
set(state => {
|
|
||||||
const idx = Math.max(0, Math.min(insertIndex, state.queue.length));
|
|
||||||
const newQueue = [
|
|
||||||
...state.queue.slice(0, idx),
|
|
||||||
...tracks,
|
|
||||||
...state.queue.slice(idx),
|
|
||||||
];
|
|
||||||
const newQueueIndex = idx <= state.queueIndex
|
|
||||||
? state.queueIndex + tracks.length
|
|
||||||
: state.queueIndex;
|
|
||||||
syncQueueToServer(newQueue, state.currentTrack, state.currentTime);
|
|
||||||
prefetchLoudnessForEnqueuedTracks(newQueue, newQueueIndex);
|
|
||||||
return { queue: newQueue, queueIndex: newQueueIndex };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
playNext: (tracks) => {
|
|
||||||
if (tracks.length === 0) return;
|
|
||||||
const state = get();
|
|
||||||
const tagged = tracks.map(t => ({ ...t, playNextAdded: true as const }));
|
|
||||||
if (!state.currentTrack) {
|
|
||||||
state.playTrack(tagged[0], tagged);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const baseIdx = state.queueIndex + 1;
|
|
||||||
let insertIdx = baseIdx;
|
|
||||||
if (useAuthStore.getState().preservePlayNextOrder) {
|
|
||||||
const q = state.queue;
|
|
||||||
while (insertIdx < q.length && q[insertIdx].playNextAdded) insertIdx++;
|
|
||||||
}
|
|
||||||
get().enqueueAt(tagged, insertIdx);
|
|
||||||
},
|
|
||||||
|
|
||||||
clearQueue: () => {
|
|
||||||
invoke('audio_stop').catch(console.error);
|
|
||||||
setIsAudioPaused(false);
|
|
||||||
clearSeekFallbackRetry();
|
|
||||||
clearSeekDebounce(); clearSeekTarget();
|
|
||||||
clearRadioSessionSeenIds();
|
|
||||||
setCurrentRadioArtistId(null);
|
|
||||||
set({ queue: [], queueIndex: 0, currentTrack: null, isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
|
||||||
syncQueueToServer([], null, 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
reorderQueue: (startIndex, endIndex) => {
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
const { queue, queueIndex, currentTrack } = get();
|
|
||||||
const result = Array.from(queue);
|
|
||||||
const [removed] = result.splice(startIndex, 1);
|
|
||||||
result.splice(endIndex, 0, removed);
|
|
||||||
let newIndex = queueIndex;
|
|
||||||
if (currentTrack) newIndex = result.findIndex(t => t.id === currentTrack.id);
|
|
||||||
set({ queue: result, queueIndex: Math.max(0, newIndex) });
|
|
||||||
syncQueueToServer(result, currentTrack, get().currentTime);
|
|
||||||
},
|
|
||||||
|
|
||||||
shuffleQueue: () => {
|
|
||||||
const { queue, currentTrack } = get();
|
|
||||||
if (queue.length < 2) return;
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
const currentIdx = currentTrack ? queue.findIndex(t => t.id === currentTrack.id) : -1;
|
|
||||||
const others = queue.filter((_, i) => i !== currentIdx);
|
|
||||||
for (let i = others.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[others[i], others[j]] = [others[j], others[i]];
|
|
||||||
}
|
|
||||||
const result = currentIdx >= 0
|
|
||||||
? [queue[currentIdx], ...others]
|
|
||||||
: others;
|
|
||||||
const newIndex = currentIdx >= 0 ? 0 : -1;
|
|
||||||
set({ queue: result, queueIndex: Math.max(0, newIndex) });
|
|
||||||
syncQueueToServer(result, currentTrack, get().currentTime);
|
|
||||||
},
|
|
||||||
|
|
||||||
shuffleUpcomingQueue: () => {
|
|
||||||
const { queue, queueIndex, currentTrack } = get();
|
|
||||||
const upcomingStart = queueIndex + 1;
|
|
||||||
const upcomingCount = queue.length - upcomingStart;
|
|
||||||
if (upcomingCount < 2) return;
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
const head = queue.slice(0, upcomingStart);
|
|
||||||
const upcoming = queue.slice(upcomingStart);
|
|
||||||
for (let i = upcoming.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[upcoming[i], upcoming[j]] = [upcoming[j], upcoming[i]];
|
|
||||||
}
|
|
||||||
const result = [...head, ...upcoming];
|
|
||||||
set({ queue: result });
|
|
||||||
syncQueueToServer(result, currentTrack, get().currentTime);
|
|
||||||
},
|
|
||||||
|
|
||||||
undoLastQueueEdit: () => {
|
undoLastQueueEdit: () => {
|
||||||
const prior = get();
|
const prior = get();
|
||||||
const snap = popQueueUndoSnapshot();
|
const snap = popQueueUndoSnapshot();
|
||||||
@@ -1323,15 +1116,6 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
return applyQueueHistorySnapshot(snap, prior, set, get);
|
return applyQueueHistorySnapshot(snap, prior, set, get);
|
||||||
},
|
},
|
||||||
|
|
||||||
removeTrack: (index) => {
|
|
||||||
pushQueueUndoFromGetter(get);
|
|
||||||
const { queue, queueIndex } = get();
|
|
||||||
const newQueue = [...queue];
|
|
||||||
newQueue.splice(index, 1);
|
|
||||||
set({ queue: newQueue, queueIndex: Math.min(queueIndex, newQueue.length - 1) });
|
|
||||||
syncQueueToServer(newQueue, get().currentTrack, get().currentTime);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── server queue restore ─────────────────────────────────────────────────
|
// ── server queue restore ─────────────────────────────────────────────────
|
||||||
initializeFromServerQueue: async () => {
|
initializeFromServerQueue: async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { orbitBulkGuard } from '../utils/orbitBulkGuard';
|
||||||
|
import { useAuthStore } from './authStore';
|
||||||
|
import { setIsAudioPaused } from './engineState';
|
||||||
|
import { prefetchLoudnessForEnqueuedTracks } from './loudnessPrefetch';
|
||||||
|
import type { PlayerState, Track } from './playerStoreTypes';
|
||||||
|
import { pushQueueUndoFromGetter } from './queueUndo';
|
||||||
|
import { syncQueueToServer } from './queueSync';
|
||||||
|
import {
|
||||||
|
addRadioSessionSeen,
|
||||||
|
clearRadioSessionSeenIds,
|
||||||
|
deleteRadioSessionSeen,
|
||||||
|
getCurrentRadioArtistId,
|
||||||
|
hasRadioSessionSeen,
|
||||||
|
setCurrentRadioArtistId,
|
||||||
|
} from './radioSessionState';
|
||||||
|
import { clearSeekDebounce } from './seekDebounce';
|
||||||
|
import { clearSeekFallbackRetry } from './seekFallbackState';
|
||||||
|
import { clearSeekTarget } from './seekTargetState';
|
||||||
|
|
||||||
|
type SetState = (
|
||||||
|
partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
|
||||||
|
) => void;
|
||||||
|
type GetState = () => PlayerState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue-mutation actions factored out of the playerStore `create()` body.
|
||||||
|
* All eleven members of the cluster: insertion (`enqueue`, `enqueueAt`,
|
||||||
|
* `playNext`, `enqueueRadio`, `setRadioArtistId`), pruning (`clearQueue`,
|
||||||
|
* `pruneUpcomingToCurrent`, `removeTrack`), and rearrangement
|
||||||
|
* (`reorderQueue`, `shuffleQueue`, `shuffleUpcomingQueue`). All but
|
||||||
|
* `setRadioArtistId` push a queue-undo snapshot and call
|
||||||
|
* `syncQueueToServer` so the Navidrome `savePlayQueue` stays in sync.
|
||||||
|
*/
|
||||||
|
export function createQueueMutationActions(set: SetState, get: GetState): Pick<
|
||||||
|
PlayerState,
|
||||||
|
| 'enqueue'
|
||||||
|
| 'enqueueAt'
|
||||||
|
| 'playNext'
|
||||||
|
| 'enqueueRadio'
|
||||||
|
| 'setRadioArtistId'
|
||||||
|
| 'pruneUpcomingToCurrent'
|
||||||
|
| 'clearQueue'
|
||||||
|
| 'reorderQueue'
|
||||||
|
| 'shuffleQueue'
|
||||||
|
| 'shuffleUpcomingQueue'
|
||||||
|
| 'removeTrack'
|
||||||
|
> {
|
||||||
|
return {
|
||||||
|
enqueue: (tracks, _orbitConfirmed = false) => {
|
||||||
|
if (!_orbitConfirmed && tracks.length > 1) {
|
||||||
|
void orbitBulkGuard(tracks.length).then(ok => {
|
||||||
|
if (ok) get().enqueue(tracks, true);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
set(state => {
|
||||||
|
// Insert before the first upcoming auto-added track so the
|
||||||
|
// "Added automatically" separator always stays at the boundary.
|
||||||
|
const firstAutoIdx = state.queue.findIndex(
|
||||||
|
(t, i) => t.autoAdded && i > state.queueIndex
|
||||||
|
);
|
||||||
|
const newQueue = firstAutoIdx === -1
|
||||||
|
? [...state.queue, ...tracks]
|
||||||
|
: [
|
||||||
|
...state.queue.slice(0, firstAutoIdx),
|
||||||
|
...tracks,
|
||||||
|
...state.queue.slice(firstAutoIdx),
|
||||||
|
];
|
||||||
|
syncQueueToServer(newQueue, state.currentTrack, state.currentTime);
|
||||||
|
prefetchLoudnessForEnqueuedTracks(newQueue, state.queueIndex);
|
||||||
|
return { queue: newQueue };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setRadioArtistId: (artistId) => {
|
||||||
|
if (artistId !== getCurrentRadioArtistId()) {
|
||||||
|
clearRadioSessionSeenIds();
|
||||||
|
}
|
||||||
|
setCurrentRadioArtistId(artistId);
|
||||||
|
},
|
||||||
|
|
||||||
|
enqueueRadio: (tracks, artistId) => {
|
||||||
|
if (artistId !== undefined) {
|
||||||
|
if (artistId !== getCurrentRadioArtistId()) {
|
||||||
|
clearRadioSessionSeenIds();
|
||||||
|
}
|
||||||
|
setCurrentRadioArtistId(artistId);
|
||||||
|
}
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
set(state => {
|
||||||
|
// Drop all upcoming (not yet played) radio tracks — clicking "Start Radio"
|
||||||
|
// again replaces the pending radio batch instead of stacking on top.
|
||||||
|
const beforeAndCurrent = state.queue.slice(0, state.queueIndex + 1);
|
||||||
|
const upcoming = state.queue.slice(state.queueIndex + 1).filter(t => !t.radioAdded);
|
||||||
|
// Tracks about to leave the queue here. Callers like ContextMenu.startRadio
|
||||||
|
// pass the previous pending radio back in `tracks` to merge with new
|
||||||
|
// similars — the seen-set must not block those re-introductions.
|
||||||
|
const droppedRadioIds = state.queue
|
||||||
|
.slice(state.queueIndex + 1)
|
||||||
|
.filter(t => t.radioAdded)
|
||||||
|
.map(t => t.id);
|
||||||
|
for (const id of droppedRadioIds) deleteRadioSessionSeen(id);
|
||||||
|
// Capture surviving queue ids in the seen-set so the next radio top-up
|
||||||
|
// can dedupe against the seed track + already-queued non-radio items.
|
||||||
|
for (const t of beforeAndCurrent) addRadioSessionSeen(t.id);
|
||||||
|
for (const t of upcoming) addRadioSessionSeen(t.id);
|
||||||
|
// Drop incoming tracks already seen earlier this session AND
|
||||||
|
// intra-batch duplicates (top + similar Last.fm responses commonly
|
||||||
|
// overlap). The seen-set is mutated inside the loop so a repeated
|
||||||
|
// id later in `tracks` is rejected by the same pass that admitted
|
||||||
|
// the first occurrence (issue #500).
|
||||||
|
const dedupedTracks: Track[] = [];
|
||||||
|
for (const t of tracks) {
|
||||||
|
if (hasRadioSessionSeen(t.id)) continue;
|
||||||
|
addRadioSessionSeen(t.id);
|
||||||
|
dedupedTracks.push(t);
|
||||||
|
}
|
||||||
|
// Insert new radio tracks before any autoAdded tracks in the upcoming section.
|
||||||
|
const firstAutoIdx = upcoming.findIndex(t => t.autoAdded);
|
||||||
|
const merged = firstAutoIdx === -1
|
||||||
|
? [...upcoming, ...dedupedTracks]
|
||||||
|
: [
|
||||||
|
...upcoming.slice(0, firstAutoIdx),
|
||||||
|
...dedupedTracks,
|
||||||
|
...upcoming.slice(firstAutoIdx),
|
||||||
|
];
|
||||||
|
const newQueue = [...beforeAndCurrent, ...merged];
|
||||||
|
syncQueueToServer(newQueue, state.currentTrack, state.currentTime);
|
||||||
|
return { queue: newQueue };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
enqueueAt: (tracks, insertIndex, _orbitConfirmed = false) => {
|
||||||
|
if (!_orbitConfirmed && tracks.length > 1) {
|
||||||
|
void orbitBulkGuard(tracks.length).then(ok => {
|
||||||
|
if (ok) get().enqueueAt(tracks, insertIndex, true);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
set(state => {
|
||||||
|
const idx = Math.max(0, Math.min(insertIndex, state.queue.length));
|
||||||
|
const newQueue = [
|
||||||
|
...state.queue.slice(0, idx),
|
||||||
|
...tracks,
|
||||||
|
...state.queue.slice(idx),
|
||||||
|
];
|
||||||
|
const newQueueIndex = idx <= state.queueIndex
|
||||||
|
? state.queueIndex + tracks.length
|
||||||
|
: state.queueIndex;
|
||||||
|
syncQueueToServer(newQueue, state.currentTrack, state.currentTime);
|
||||||
|
prefetchLoudnessForEnqueuedTracks(newQueue, newQueueIndex);
|
||||||
|
return { queue: newQueue, queueIndex: newQueueIndex };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
playNext: (tracks) => {
|
||||||
|
if (tracks.length === 0) return;
|
||||||
|
const state = get();
|
||||||
|
const tagged = tracks.map(t => ({ ...t, playNextAdded: true as const }));
|
||||||
|
if (!state.currentTrack) {
|
||||||
|
state.playTrack(tagged[0], tagged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const baseIdx = state.queueIndex + 1;
|
||||||
|
let insertIdx = baseIdx;
|
||||||
|
if (useAuthStore.getState().preservePlayNextOrder) {
|
||||||
|
const q = state.queue;
|
||||||
|
while (insertIdx < q.length && q[insertIdx].playNextAdded) insertIdx++;
|
||||||
|
}
|
||||||
|
get().enqueueAt(tagged, insertIdx);
|
||||||
|
},
|
||||||
|
|
||||||
|
pruneUpcomingToCurrent: () => {
|
||||||
|
const s = get();
|
||||||
|
if (s.currentRadio) return;
|
||||||
|
if (!s.currentTrack) {
|
||||||
|
if (s.queue.length === 0) return;
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
set({ queue: [], queueIndex: 0 });
|
||||||
|
syncQueueToServer([], null, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
const at = s.queue.findIndex(t => t.id === s.currentTrack!.id);
|
||||||
|
const newQueue: Track[] =
|
||||||
|
at >= 0
|
||||||
|
? s.queue.slice(0, at + 1)
|
||||||
|
: [s.currentTrack!];
|
||||||
|
const newIndex = at >= 0 ? at : 0;
|
||||||
|
set({ queue: newQueue, queueIndex: newIndex });
|
||||||
|
syncQueueToServer(newQueue, s.currentTrack, s.currentTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearQueue: () => {
|
||||||
|
invoke('audio_stop').catch(console.error);
|
||||||
|
setIsAudioPaused(false);
|
||||||
|
clearSeekFallbackRetry();
|
||||||
|
clearSeekDebounce(); clearSeekTarget();
|
||||||
|
clearRadioSessionSeenIds();
|
||||||
|
setCurrentRadioArtistId(null);
|
||||||
|
set({ queue: [], queueIndex: 0, currentTrack: null, isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||||
|
syncQueueToServer([], null, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
reorderQueue: (startIndex, endIndex) => {
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
const { queue, queueIndex, currentTrack } = get();
|
||||||
|
const result = Array.from(queue);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
let newIndex = queueIndex;
|
||||||
|
if (currentTrack) newIndex = result.findIndex(t => t.id === currentTrack.id);
|
||||||
|
set({ queue: result, queueIndex: Math.max(0, newIndex) });
|
||||||
|
syncQueueToServer(result, currentTrack, get().currentTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
shuffleQueue: () => {
|
||||||
|
const { queue, currentTrack } = get();
|
||||||
|
if (queue.length < 2) return;
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
const currentIdx = currentTrack ? queue.findIndex(t => t.id === currentTrack.id) : -1;
|
||||||
|
const others = queue.filter((_, i) => i !== currentIdx);
|
||||||
|
for (let i = others.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[others[i], others[j]] = [others[j], others[i]];
|
||||||
|
}
|
||||||
|
const result = currentIdx >= 0
|
||||||
|
? [queue[currentIdx], ...others]
|
||||||
|
: others;
|
||||||
|
const newIndex = currentIdx >= 0 ? 0 : -1;
|
||||||
|
set({ queue: result, queueIndex: Math.max(0, newIndex) });
|
||||||
|
syncQueueToServer(result, currentTrack, get().currentTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
shuffleUpcomingQueue: () => {
|
||||||
|
const { queue, queueIndex, currentTrack } = get();
|
||||||
|
const upcomingStart = queueIndex + 1;
|
||||||
|
const upcomingCount = queue.length - upcomingStart;
|
||||||
|
if (upcomingCount < 2) return;
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
const head = queue.slice(0, upcomingStart);
|
||||||
|
const upcoming = queue.slice(upcomingStart);
|
||||||
|
for (let i = upcoming.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[upcoming[i], upcoming[j]] = [upcoming[j], upcoming[i]];
|
||||||
|
}
|
||||||
|
const result = [...head, ...upcoming];
|
||||||
|
set({ queue: result });
|
||||||
|
syncQueueToServer(result, currentTrack, get().currentTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTrack: (index) => {
|
||||||
|
pushQueueUndoFromGetter(get);
|
||||||
|
const { queue, queueIndex } = get();
|
||||||
|
const newQueue = [...queue];
|
||||||
|
newQueue.splice(index, 1);
|
||||||
|
set({ queue: newQueue, queueIndex: Math.min(queueIndex, newQueue.length - 1) });
|
||||||
|
syncQueueToServer(newQueue, get().currentTrack, get().currentTime);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user