diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index 965ae076..55ae2769 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -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()( ...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()( 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()( }); }, - 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()( 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 { diff --git a/src/store/transportLightActions.ts b/src/store/transportLightActions.ts new file mode 100644 index 00000000..506a67a0 --- /dev/null +++ b/src/store/transportLightActions.ts @@ -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 | ((state: PlayerState) => Partial), +) => 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(); + }, + }; +} diff --git a/src/store/undoRedoActions.ts b/src/store/undoRedoActions.ts new file mode 100644 index 00000000..881a81e1 --- /dev/null +++ b/src/store/undoRedoActions.ts @@ -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 | ((state: PlayerState) => Partial), +) => 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); + }, + }; +}