mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +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 { resetLoudnessBackfillStateForTrackId } from './loudnessBackfillState';
|
||||||
import {
|
import {
|
||||||
flushPlayQueuePosition,
|
flushPlayQueuePosition,
|
||||||
flushQueueSyncToServer,
|
|
||||||
syncQueueToServer,
|
syncQueueToServer,
|
||||||
} from './queueSync';
|
} from './queueSync';
|
||||||
import {
|
import {
|
||||||
@@ -70,13 +69,11 @@ import {
|
|||||||
clearSeekTarget,
|
clearSeekTarget,
|
||||||
setSeekTarget,
|
setSeekTarget,
|
||||||
} from './seekTargetState';
|
} from './seekTargetState';
|
||||||
import { tryAcquireTogglePlayLock } from './togglePlayLock';
|
|
||||||
import { reseedLoudnessForTrackId } from './loudnessReseed';
|
import { reseedLoudnessForTrackId } from './loudnessReseed';
|
||||||
import { refreshWaveformForTrack } from './waveformRefresh';
|
import { refreshWaveformForTrack } from './waveformRefresh';
|
||||||
import { refreshLoudnessForTrack } from './loudnessRefresh';
|
import { refreshLoudnessForTrack } from './loudnessRefresh';
|
||||||
import {
|
import {
|
||||||
clearRadioReconnectTimer,
|
clearRadioReconnectTimer,
|
||||||
pauseRadio,
|
|
||||||
playRadioStream,
|
playRadioStream,
|
||||||
resumeRadio,
|
resumeRadio,
|
||||||
setRadioVolume,
|
setRadioVolume,
|
||||||
@@ -141,14 +138,8 @@ export {
|
|||||||
import {
|
import {
|
||||||
_resetQueueUndoStacksForTest,
|
_resetQueueUndoStacksForTest,
|
||||||
consumePendingQueueListScrollTop,
|
consumePendingQueueListScrollTop,
|
||||||
popQueueRedoSnapshot,
|
|
||||||
popQueueUndoSnapshot,
|
|
||||||
pushQueueRedoSnapshot,
|
|
||||||
pushQueueUndoFromGetter,
|
pushQueueUndoFromGetter,
|
||||||
pushQueueUndoSnapshot,
|
|
||||||
queueUndoSnapshotFromState,
|
|
||||||
registerQueueListScrollTopReader,
|
registerQueueListScrollTopReader,
|
||||||
type QueueUndoSnapshot,
|
|
||||||
} from './queueUndo';
|
} from './queueUndo';
|
||||||
|
|
||||||
// Re-export for backward compatibility with the ~30 call sites that still
|
// Re-export for backward compatibility with the ~30 call sites that still
|
||||||
@@ -166,10 +157,11 @@ export {
|
|||||||
|
|
||||||
import type { PlayerState, Track } from './playerStoreTypes';
|
import type { PlayerState, Track } from './playerStoreTypes';
|
||||||
export type { PlayerState, Track };
|
export type { PlayerState, Track };
|
||||||
import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot';
|
|
||||||
import { createLastfmActions } from './lastfmActions';
|
import { createLastfmActions } from './lastfmActions';
|
||||||
import { createQueueMutationActions } from './queueMutationActions';
|
import { createQueueMutationActions } from './queueMutationActions';
|
||||||
|
import { createTransportLightActions } from './transportLightActions';
|
||||||
import { createUiStateActions } from './uiStateActions';
|
import { createUiStateActions } from './uiStateActions';
|
||||||
|
import { createUndoRedoActions } from './undoRedoActions';
|
||||||
|
|
||||||
|
|
||||||
// ─── Module-level playback primitives ─────────────────────────────────────────
|
// ─── Module-level playback primitives ─────────────────────────────────────────
|
||||||
@@ -221,36 +213,8 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
...createUiStateActions(set),
|
...createUiStateActions(set),
|
||||||
...createLastfmActions(set, get),
|
...createLastfmActions(set, get),
|
||||||
...createQueueMutationActions(set, get),
|
...createQueueMutationActions(set, get),
|
||||||
|
...createTransportLightActions(set, get),
|
||||||
// ── stop ────────────────────────────────────────────────────────────────
|
...createUndoRedoActions(set, get),
|
||||||
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,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── playRadio ────────────────────────────────────────────────────────────
|
// ── playRadio ────────────────────────────────────────────────────────────
|
||||||
playRadio: async (station) => {
|
playRadio: async (station) => {
|
||||||
@@ -591,28 +555,7 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
if (!wasPlaying) get().resume();
|
if (!wasPlaying) get().resume();
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── pause / resume / togglePlay ──────────────────────────────────────────
|
// ── resume ───────────────────────────────────────────────────────────────
|
||||||
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();
|
clearAllPlaybackScheduleTimers();
|
||||||
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
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 / previous ──────────────────────────────────────────────────────
|
||||||
next: (manual = true) => {
|
next: (manual = true) => {
|
||||||
const { queue, queueIndex, repeatMode, currentTrack } = get();
|
const { queue, queueIndex, repeatMode, currentTrack } = get();
|
||||||
@@ -1099,23 +1036,6 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
set({ currentTime: t, progress: duration > 0 ? t / duration : 0 });
|
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 ─────────────────────────────────────────────────
|
// ── server queue restore ─────────────────────────────────────────────────
|
||||||
initializeFromServerQueue: async () => {
|
initializeFromServerQueue: async () => {
|
||||||
try {
|
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