mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
8dfe05fe7d
The ~150-LOC `applyQueueHistorySnapshot` helper that lived inside the
playerStore `create((set, get) => { … })` closure moves out into
`src/store/applyQueueHistorySnapshot.ts`. The function now takes the
zustand `set` / `get` references as explicit parameters; the two
caller sites (`undoLastQueueEdit`, `redoLastQueueEdit`) pass them
through unchanged.
Side-cleanup: four imports that only fed the moved helper drop from
playerStore (getPlaybackSourceKind, queueUndoRestoreAudioEngine,
setPendingQueueListScrollTop, shallowCloneQueueTracks).
playerStore 1706 → 1550 LOC (−156).
The undo/redo flow keeps behaving 1:1 — the function is exported and
deterministic; the closure capture only existed for the set/get pair
which is trivial to pass through.
206 lines
6.9 KiB
TypeScript
206 lines
6.9 KiB
TypeScript
import { invoke } from '@tauri-apps/api/core';
|
|
import { reportNowPlaying } from '../api/subsonic';
|
|
import { getPlaybackSourceKind } from '../utils/resolvePlaybackUrl';
|
|
import { useAuthStore } from './authStore';
|
|
import {
|
|
bumpPlayGeneration,
|
|
getPlayGeneration,
|
|
setIsAudioPaused,
|
|
} from './engineState';
|
|
import { clearPreloadingIds } from './gaplessPreloadState';
|
|
import { deriveNormalizationSnapshot } from './normalizationSnapshot';
|
|
import type { PlayerState } from './playerStoreTypes';
|
|
import { sameQueueTrackId, shallowCloneQueueTracks } from '../utils/queueIdentity';
|
|
import { queueUndoRestoreAudioEngine } from './queueUndoAudioRestore';
|
|
import {
|
|
setPendingQueueListScrollTop,
|
|
type QueueUndoSnapshot,
|
|
} from './queueUndo';
|
|
import { refreshLoudnessForTrack } from './loudnessRefresh';
|
|
import { refreshWaveformForTrack } from './waveformRefresh';
|
|
import { stopRadio } from './radioPlayer';
|
|
import { clearAllPlaybackScheduleTimers } from './scheduleTimers';
|
|
import { syncQueueToServer } from './queueSync';
|
|
|
|
type SetState = (
|
|
partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
|
|
) => void;
|
|
type GetState = () => PlayerState;
|
|
|
|
/**
|
|
* Apply a queue-undo snapshot to the player store and resync the Rust
|
|
* audio engine where needed. Used by both `undoLastQueueEdit` and
|
|
* `redoLastQueueEdit` actions inside the store.
|
|
*
|
|
* Behaviour matrix:
|
|
* - **Snapshot has no current track but playback is live** — keep the
|
|
* playing track and prepend it to the restored queue (or rebind by
|
|
* id when it's already there).
|
|
* - **Snapshot's current track matches the live one** — keep playback
|
|
* going, restore queue + position only.
|
|
* - **Snapshot's current track differs** — issue a full audio_play via
|
|
* `queueUndoRestoreAudioEngine` to put the engine on the snapshot
|
|
* track at the captured position.
|
|
*
|
|
* Returns false only when nothing changed (caller shows no toast).
|
|
*/
|
|
export function applyQueueHistorySnapshot(
|
|
snap: QueueUndoSnapshot,
|
|
prior: PlayerState,
|
|
set: SetState,
|
|
get: GetState,
|
|
): boolean {
|
|
if (prior.currentRadio) {
|
|
stopRadio();
|
|
}
|
|
let nextQueue = shallowCloneQueueTracks(snap.queue);
|
|
let nextIndex = snap.queueIndex;
|
|
let nextTrack = snap.currentTrack ? { ...snap.currentTrack } : null;
|
|
|
|
if (snap.currentTrack == null && prior.currentTrack) {
|
|
const playing = prior.currentTrack;
|
|
const pos = nextQueue.findIndex(t => sameQueueTrackId(t.id, playing.id));
|
|
if (pos === -1) {
|
|
nextQueue = [{ ...playing }, ...nextQueue];
|
|
nextIndex = 0;
|
|
nextTrack = { ...playing };
|
|
} else {
|
|
nextTrack = { ...playing };
|
|
nextIndex = pos;
|
|
}
|
|
}
|
|
|
|
nextIndex = Math.max(0, Math.min(nextIndex, Math.max(0, nextQueue.length - 1)));
|
|
|
|
const keepPlaybackFromPrior =
|
|
prior.currentTrack != null
|
|
&& nextTrack != null
|
|
&& sameQueueTrackId(prior.currentTrack.id, nextTrack.id)
|
|
&& nextQueue.some(t => sameQueueTrackId(t.id, prior.currentTrack!.id))
|
|
&& (
|
|
(snap.currentTrack != null && sameQueueTrackId(prior.currentTrack.id, snap.currentTrack.id))
|
|
|| snap.currentTrack == null
|
|
);
|
|
|
|
if (keepPlaybackFromPrior) {
|
|
const playingKeep = prior.currentTrack;
|
|
if (playingKeep) {
|
|
const idxPrior = nextQueue.findIndex(t => sameQueueTrackId(t.id, playingKeep.id));
|
|
if (idxPrior >= 0) {
|
|
nextIndex = idxPrior;
|
|
nextTrack = { ...playingKeep };
|
|
}
|
|
}
|
|
}
|
|
|
|
let tRestoreRaw = typeof snap.currentTime === 'number' && Number.isFinite(snap.currentTime)
|
|
? snap.currentTime
|
|
: 0;
|
|
let playingRestore = snap.isPlaying !== false;
|
|
if (keepPlaybackFromPrior && prior.currentTrack) {
|
|
tRestoreRaw = prior.currentTime;
|
|
playingRestore = prior.isPlaying;
|
|
}
|
|
const durForProgress = nextTrack?.duration && nextTrack.duration > 0 ? nextTrack.duration : null;
|
|
let pRestore = typeof snap.progress === 'number' && Number.isFinite(snap.progress)
|
|
? snap.progress
|
|
: (durForProgress != null && durForProgress > 0
|
|
? Math.max(0, Math.min(1, tRestoreRaw / durForProgress))
|
|
: 0);
|
|
if (keepPlaybackFromPrior) {
|
|
pRestore = prior.progress;
|
|
}
|
|
const tRestore = durForProgress != null
|
|
? Math.max(0, Math.min(tRestoreRaw, durForProgress))
|
|
: Math.max(0, tRestoreRaw);
|
|
|
|
const keepWaveform =
|
|
prior.currentTrack?.id != null &&
|
|
nextTrack?.id != null &&
|
|
sameQueueTrackId(prior.currentTrack.id, nextTrack.id);
|
|
const norm =
|
|
nextTrack != null
|
|
? deriveNormalizationSnapshot(nextTrack, nextQueue, nextIndex)
|
|
: ({
|
|
normalizationNowDb: null,
|
|
normalizationTargetLufs: null,
|
|
normalizationEngineLive: 'off',
|
|
} as Pick<
|
|
PlayerState,
|
|
'normalizationNowDb' | 'normalizationTargetLufs' | 'normalizationEngineLive'
|
|
>);
|
|
const authSnap = useAuthStore.getState();
|
|
const playbackSourceUndo = nextTrack
|
|
? getPlaybackSourceKind(nextTrack.id, authSnap.activeServerId ?? '', null)
|
|
: null;
|
|
const playbackSourceFinal = keepPlaybackFromPrior && prior.currentPlaybackSource != null
|
|
? prior.currentPlaybackSource
|
|
: playbackSourceUndo;
|
|
|
|
clearAllPlaybackScheduleTimers();
|
|
set({
|
|
scheduledPauseAtMs: null,
|
|
scheduledPauseStartMs: null,
|
|
scheduledResumeAtMs: null,
|
|
scheduledResumeStartMs: null,
|
|
});
|
|
|
|
clearPreloadingIds();
|
|
|
|
let gen = getPlayGeneration();
|
|
const resyncEngine = Boolean(nextTrack) && !keepPlaybackFromPrior;
|
|
if (resyncEngine || !nextTrack) {
|
|
gen = bumpPlayGeneration();
|
|
if (resyncEngine) {
|
|
setIsAudioPaused(false);
|
|
}
|
|
}
|
|
|
|
set({
|
|
queue: nextQueue,
|
|
queueIndex: nextIndex,
|
|
currentTrack: nextTrack,
|
|
currentRadio: null,
|
|
currentTime: tRestore,
|
|
progress: pRestore,
|
|
isPlaying: playingRestore,
|
|
waveformBins: keepWaveform ? prior.waveformBins : null,
|
|
enginePreloadedTrackId: keepPlaybackFromPrior ? prior.enginePreloadedTrackId : null,
|
|
currentPlaybackSource: playbackSourceFinal,
|
|
...norm,
|
|
});
|
|
|
|
if (!nextTrack) {
|
|
invoke('audio_stop').catch(console.error);
|
|
setIsAudioPaused(false);
|
|
syncQueueToServer(nextQueue, null, 0);
|
|
if (typeof snap.queueListScrollTop === 'number' && Number.isFinite(snap.queueListScrollTop)) {
|
|
setPendingQueueListScrollTop(Math.max(0, snap.queueListScrollTop));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void refreshWaveformForTrack(nextTrack.id);
|
|
void refreshLoudnessForTrack(nextTrack.id);
|
|
get().updateReplayGainForCurrentTrack();
|
|
|
|
if (!keepPlaybackFromPrior) {
|
|
const { nowPlayingEnabled: npUndo } = useAuthStore.getState();
|
|
if (npUndo) reportNowPlaying(nextTrack.id);
|
|
|
|
queueUndoRestoreAudioEngine({
|
|
generation: gen,
|
|
track: nextTrack,
|
|
queue: nextQueue,
|
|
queueIndex: nextIndex,
|
|
atSeconds: tRestore,
|
|
wantPlaying: playingRestore,
|
|
});
|
|
}
|
|
if (typeof snap.queueListScrollTop === 'number' && Number.isFinite(snap.queueListScrollTop)) {
|
|
setPendingQueueListScrollTop(Math.max(0, snap.queueListScrollTop));
|
|
}
|
|
syncQueueToServer(nextQueue, nextTrack, tRestore);
|
|
return true;
|
|
}
|