mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(player): E.34 — extract transport-light + undo/redo factories (#598)
Two small action-factory extractions in one PR, both following the pattern from E.31–E.33. - `createTransportLightActions` — `stop`, `pause`, `resetAudioPause`, `togglePlay`. Everything in the pause/togglePlay cluster except `resume` (~165 LOC, deferred to its own PR). - `createUndoRedoActions` — `undoLastQueueEdit`, `redoLastQueueEdit`. Trivial wrappers around `applyQueueHistorySnapshot` + the queue-undo stack helpers. Pure code-move. playerStore.ts: 1247 → 1167 LOC (−80).
This commit is contained in:
committed by
GitHub
parent
168d5905c2
commit
76fc3bb9c9
@@ -58,7 +58,6 @@ import { applySkipStarOnManualNext } from './skipStarRating';
|
||||
import { resetLoudnessBackfillStateForTrackId } from './loudnessBackfillState';
|
||||
import {
|
||||
flushPlayQueuePosition,
|
||||
flushQueueSyncToServer,
|
||||
syncQueueToServer,
|
||||
} from './queueSync';
|
||||
import {
|
||||
@@ -70,13 +69,11 @@ import {
|
||||
clearSeekTarget,
|
||||
setSeekTarget,
|
||||
} from './seekTargetState';
|
||||
import { tryAcquireTogglePlayLock } from './togglePlayLock';
|
||||
import { reseedLoudnessForTrackId } from './loudnessReseed';
|
||||
import { refreshWaveformForTrack } from './waveformRefresh';
|
||||
import { refreshLoudnessForTrack } from './loudnessRefresh';
|
||||
import {
|
||||
clearRadioReconnectTimer,
|
||||
pauseRadio,
|
||||
playRadioStream,
|
||||
resumeRadio,
|
||||
setRadioVolume,
|
||||
@@ -141,14 +138,8 @@ export {
|
||||
import {
|
||||
_resetQueueUndoStacksForTest,
|
||||
consumePendingQueueListScrollTop,
|
||||
popQueueRedoSnapshot,
|
||||
popQueueUndoSnapshot,
|
||||
pushQueueRedoSnapshot,
|
||||
pushQueueUndoFromGetter,
|
||||
pushQueueUndoSnapshot,
|
||||
queueUndoSnapshotFromState,
|
||||
registerQueueListScrollTopReader,
|
||||
type QueueUndoSnapshot,
|
||||
} from './queueUndo';
|
||||
|
||||
// Re-export for backward compatibility with the ~30 call sites that still
|
||||
@@ -166,10 +157,11 @@ export {
|
||||
|
||||
import type { PlayerState, Track } from './playerStoreTypes';
|
||||
export type { PlayerState, Track };
|
||||
import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot';
|
||||
import { createLastfmActions } from './lastfmActions';
|
||||
import { createQueueMutationActions } from './queueMutationActions';
|
||||
import { createTransportLightActions } from './transportLightActions';
|
||||
import { createUiStateActions } from './uiStateActions';
|
||||
import { createUndoRedoActions } from './undoRedoActions';
|
||||
|
||||
|
||||
// ─── Module-level playback primitives ─────────────────────────────────────────
|
||||
@@ -221,36 +213,8 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
...createUiStateActions(set),
|
||||
...createLastfmActions(set, get),
|
||||
...createQueueMutationActions(set, get),
|
||||
|
||||
// ── stop ────────────────────────────────────────────────────────────────
|
||||
stop: () => {
|
||||
clearAllPlaybackScheduleTimers();
|
||||
if (get().currentRadio) {
|
||||
stopRadio();
|
||||
} else {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
}
|
||||
setIsAudioPaused(false);
|
||||
clearSeekFallbackRetry();
|
||||
clearSeekDebounce(); clearSeekTarget();
|
||||
set({
|
||||
isPlaying: false,
|
||||
progress: 0,
|
||||
buffered: 0,
|
||||
currentTime: 0,
|
||||
currentRadio: null,
|
||||
waveformBins: null,
|
||||
normalizationNowDb: null,
|
||||
normalizationTargetLufs: null,
|
||||
normalizationEngineLive: 'off',
|
||||
currentPlaybackSource: null,
|
||||
enginePreloadedTrackId: null,
|
||||
scheduledPauseAtMs: null,
|
||||
scheduledPauseStartMs: null,
|
||||
scheduledResumeAtMs: null,
|
||||
scheduledResumeStartMs: null,
|
||||
});
|
||||
},
|
||||
...createTransportLightActions(set, get),
|
||||
...createUndoRedoActions(set, get),
|
||||
|
||||
// ── playRadio ────────────────────────────────────────────────────────────
|
||||
playRadio: async (station) => {
|
||||
@@ -591,28 +555,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
if (!wasPlaying) get().resume();
|
||||
},
|
||||
|
||||
// ── pause / resume / togglePlay ──────────────────────────────────────────
|
||||
pause: () => {
|
||||
clearAllPlaybackScheduleTimers();
|
||||
if (get().currentRadio) {
|
||||
pauseRadio();
|
||||
} else {
|
||||
invoke('audio_pause').catch(console.error);
|
||||
setIsAudioPaused(true);
|
||||
// Flush position so a quick close after pause still leaves the
|
||||
// server with the right resume point for other devices.
|
||||
const s = get();
|
||||
if (s.currentTrack) {
|
||||
void flushQueueSyncToServer(s.queue, s.currentTrack, s.currentTime);
|
||||
}
|
||||
}
|
||||
set({ isPlaying: false, scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
},
|
||||
|
||||
resetAudioPause: () => {
|
||||
setIsAudioPaused(false);
|
||||
},
|
||||
|
||||
// ── resume ───────────────────────────────────────────────────────────────
|
||||
resume: () => {
|
||||
clearAllPlaybackScheduleTimers();
|
||||
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
@@ -815,12 +758,6 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
});
|
||||
},
|
||||
|
||||
togglePlay: () => {
|
||||
if (!tryAcquireTogglePlayLock()) return;
|
||||
const { isPlaying } = get();
|
||||
isPlaying ? get().pause() : get().resume();
|
||||
},
|
||||
|
||||
// ── next / previous ──────────────────────────────────────────────────────
|
||||
next: (manual = true) => {
|
||||
const { queue, queueIndex, repeatMode, currentTrack } = get();
|
||||
@@ -1099,23 +1036,6 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
set({ currentTime: t, progress: duration > 0 ? t / duration : 0 });
|
||||
},
|
||||
|
||||
// ── queue management ─────────────────────────────────────────────────────
|
||||
undoLastQueueEdit: () => {
|
||||
const prior = get();
|
||||
const snap = popQueueUndoSnapshot();
|
||||
if (!snap) return false;
|
||||
pushQueueRedoSnapshot(queueUndoSnapshotFromState(prior));
|
||||
return applyQueueHistorySnapshot(snap, prior, set, get);
|
||||
},
|
||||
|
||||
redoLastQueueEdit: () => {
|
||||
const prior = get();
|
||||
const snap = popQueueRedoSnapshot();
|
||||
if (!snap) return false;
|
||||
pushQueueUndoSnapshot(queueUndoSnapshotFromState(prior));
|
||||
return applyQueueHistorySnapshot(snap, prior, set, get);
|
||||
},
|
||||
|
||||
// ── server queue restore ─────────────────────────────────────────────────
|
||||
initializeFromServerQueue: async () => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { setIsAudioPaused } from './engineState';
|
||||
import type { PlayerState } from './playerStoreTypes';
|
||||
import { flushQueueSyncToServer } from './queueSync';
|
||||
import { pauseRadio, stopRadio } from './radioPlayer';
|
||||
import { clearAllPlaybackScheduleTimers } from './scheduleTimers';
|
||||
import { clearSeekDebounce } from './seekDebounce';
|
||||
import { clearSeekFallbackRetry } from './seekFallbackState';
|
||||
import { clearSeekTarget } from './seekTargetState';
|
||||
import { tryAcquireTogglePlayLock } from './togglePlayLock';
|
||||
|
||||
type SetState = (
|
||||
partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
|
||||
) => void;
|
||||
type GetState = () => PlayerState;
|
||||
|
||||
/**
|
||||
* Light transport actions factored out of the playerStore `create()`
|
||||
* body — everything except `resume` (~165 LOC, separate PR) and the
|
||||
* scheduled pause/resume timer setters.
|
||||
*
|
||||
* - `stop` — full reset: stops audio/radio, clears timers + seek + visual
|
||||
* state, blanks playback metadata.
|
||||
* - `pause` — pauses audio (or radio), flushes queue position so other
|
||||
* devices can pick up the resume point.
|
||||
* - `resetAudioPause` — flips the engine-paused flag without touching
|
||||
* the UI `isPlaying` state. Used by `audio:ended` paths.
|
||||
* - `togglePlay` — guarded toggle so a double media-key tap can't race
|
||||
* pause + resume into a stuck state.
|
||||
*/
|
||||
export function createTransportLightActions(set: SetState, get: GetState): Pick<
|
||||
PlayerState,
|
||||
'stop' | 'pause' | 'resetAudioPause' | 'togglePlay'
|
||||
> {
|
||||
return {
|
||||
stop: () => {
|
||||
clearAllPlaybackScheduleTimers();
|
||||
if (get().currentRadio) {
|
||||
stopRadio();
|
||||
} else {
|
||||
invoke('audio_stop').catch(console.error);
|
||||
}
|
||||
setIsAudioPaused(false);
|
||||
clearSeekFallbackRetry();
|
||||
clearSeekDebounce(); clearSeekTarget();
|
||||
set({
|
||||
isPlaying: false,
|
||||
progress: 0,
|
||||
buffered: 0,
|
||||
currentTime: 0,
|
||||
currentRadio: null,
|
||||
waveformBins: null,
|
||||
normalizationNowDb: null,
|
||||
normalizationTargetLufs: null,
|
||||
normalizationEngineLive: 'off',
|
||||
currentPlaybackSource: null,
|
||||
enginePreloadedTrackId: null,
|
||||
scheduledPauseAtMs: null,
|
||||
scheduledPauseStartMs: null,
|
||||
scheduledResumeAtMs: null,
|
||||
scheduledResumeStartMs: null,
|
||||
});
|
||||
},
|
||||
|
||||
pause: () => {
|
||||
clearAllPlaybackScheduleTimers();
|
||||
if (get().currentRadio) {
|
||||
pauseRadio();
|
||||
} else {
|
||||
invoke('audio_pause').catch(console.error);
|
||||
setIsAudioPaused(true);
|
||||
// Flush position so a quick close after pause still leaves the
|
||||
// server with the right resume point for other devices.
|
||||
const s = get();
|
||||
if (s.currentTrack) {
|
||||
void flushQueueSyncToServer(s.queue, s.currentTrack, s.currentTime);
|
||||
}
|
||||
}
|
||||
set({ isPlaying: false, scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
},
|
||||
|
||||
resetAudioPause: () => {
|
||||
setIsAudioPaused(false);
|
||||
},
|
||||
|
||||
togglePlay: () => {
|
||||
if (!tryAcquireTogglePlayLock()) return;
|
||||
const { isPlaying } = get();
|
||||
isPlaying ? get().pause() : get().resume();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot';
|
||||
import type { PlayerState } from './playerStoreTypes';
|
||||
import {
|
||||
popQueueRedoSnapshot,
|
||||
popQueueUndoSnapshot,
|
||||
pushQueueRedoSnapshot,
|
||||
pushQueueUndoSnapshot,
|
||||
queueUndoSnapshotFromState,
|
||||
} from './queueUndo';
|
||||
|
||||
type SetState = (
|
||||
partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
|
||||
) => void;
|
||||
type GetState = () => PlayerState;
|
||||
|
||||
/**
|
||||
* Undo / redo wrappers for queue edits. Both pop the matching history
|
||||
* snapshot, push the *prior* state onto the opposite stack so the next
|
||||
* call reverses direction, and delegate the actual state reconciliation
|
||||
* to `applyQueueHistorySnapshot`.
|
||||
*/
|
||||
export function createUndoRedoActions(set: SetState, get: GetState): Pick<
|
||||
PlayerState,
|
||||
'undoLastQueueEdit' | 'redoLastQueueEdit'
|
||||
> {
|
||||
return {
|
||||
undoLastQueueEdit: () => {
|
||||
const prior = get();
|
||||
const snap = popQueueUndoSnapshot();
|
||||
if (!snap) return false;
|
||||
pushQueueRedoSnapshot(queueUndoSnapshotFromState(prior));
|
||||
return applyQueueHistorySnapshot(snap, prior, set, get);
|
||||
},
|
||||
|
||||
redoLastQueueEdit: () => {
|
||||
const prior = get();
|
||||
const snap = popQueueRedoSnapshot();
|
||||
if (!snap) return false;
|
||||
pushQueueUndoSnapshot(queueUndoSnapshotFromState(prior));
|
||||
return applyQueueHistorySnapshot(snap, prior, set, get);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user