mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
e92535a5f5
Move the ~280-LOC `playTrack` action body into `runPlayTrack(set, get, track, queue, manual, _orbitConfirmed, targetQueueIndex)` in `src/store/playTrackAction.ts`, following the helper-with-thin-wrapper pattern from E.37–E.40. Covers all three guard layers (Orbit bulk-gate, Orbit-host single-track protection, ghost-command 500 ms guard), the same-track-hot-promote branch, and the inner `runPlayTrackBody` closure that resolves the URL, updates store + normalization optimistically, invokes the Rust engine, and on success seeks to the visual target if there was a pending one. With playTrack extracted, every action body has been moved out of the playerStore create() body. playerStore.ts: 515 → 180 LOC (−335), down from Phase E's starting 3732 LOC (~95% smaller).
181 lines
7.3 KiB
TypeScript
181 lines
7.3 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
import { resolveReplayGainDb } from '../utils/resolveReplayGainDb';
|
|
import { shuffleArray } from '../utils/shuffleArray';
|
|
import { songToTrack } from '../utils/songToTrack';
|
|
import {
|
|
emitPlaybackProgress,
|
|
getPlaybackProgressSnapshot,
|
|
subscribePlaybackProgress,
|
|
type PlaybackProgressSnapshot,
|
|
} from './playbackProgress';
|
|
import { flushPlayQueuePosition } from './queueSync';
|
|
import { initAudioListeners } from './initAudioListeners';
|
|
import { installQueueUndoHotkey } from './queueUndoHotkey';
|
|
import { readInitialQueueVisibility } from './queueVisibilityStorage';
|
|
|
|
// Re-export so MainApp + the 3 playerStore characterization tests keep
|
|
// their existing `from './playerStore'` imports.
|
|
export { initAudioListeners };
|
|
|
|
// Re-export so bootstrap.ts + bootstrap.test keep their existing
|
|
// `from './playerStore'` imports.
|
|
export { installQueueUndoHotkey };
|
|
|
|
// Re-export so TauriEventBridge + persistence test keep their existing
|
|
// `from './playerStore'` imports.
|
|
export { flushPlayQueuePosition };
|
|
|
|
// Re-export the playback-progress public surface so existing call sites
|
|
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
|
|
// TauriEventBridge, plus the progress characterization test) keep their
|
|
// `from './playerStore'` imports working.
|
|
export {
|
|
getPlaybackProgressSnapshot,
|
|
subscribePlaybackProgress,
|
|
type PlaybackProgressSnapshot,
|
|
};
|
|
import {
|
|
_resetQueueUndoStacksForTest,
|
|
consumePendingQueueListScrollTop,
|
|
pushQueueUndoFromGetter,
|
|
registerQueueListScrollTopReader,
|
|
} from './queueUndo';
|
|
|
|
// Re-export for backward compatibility with the ~30 call sites that still
|
|
// import these helpers from playerStore. Phase E (store splits) will migrate
|
|
// the imports to '../utils/*' directly and drop these re-exports.
|
|
export { resolveReplayGainDb, shuffleArray, songToTrack };
|
|
|
|
// Re-export the queue-undo public API so existing callers (QueuePanel,
|
|
// test/helpers/storeReset) keep their `from './playerStore'` imports.
|
|
export {
|
|
_resetQueueUndoStacksForTest,
|
|
consumePendingQueueListScrollTop,
|
|
registerQueueListScrollTopReader,
|
|
};
|
|
|
|
import type { PlayerState, Track } from './playerStoreTypes';
|
|
export type { PlayerState, Track };
|
|
import { createLastfmActions } from './lastfmActions';
|
|
import { createMiscActions } from './miscActions';
|
|
import { runNext } from './nextAction';
|
|
import { runPlayTrack } from './playTrackAction';
|
|
import { runResume } from './resumeAction';
|
|
import { runSeek } from './seekAction';
|
|
import { runUpdateReplayGainForCurrentTrack } from './updateReplayGainAction';
|
|
import { createQueueMutationActions } from './queueMutationActions';
|
|
import { createScheduleActions } from './scheduleActions';
|
|
import { createTransportLightActions } from './transportLightActions';
|
|
import { createUiStateActions } from './uiStateActions';
|
|
import { createUndoRedoActions } from './undoRedoActions';
|
|
|
|
|
|
// ─── Module-level playback primitives ─────────────────────────────────────────
|
|
|
|
|
|
// ─── Store ────────────────────────────────────────────────────────────────────
|
|
|
|
export const usePlayerStore = create<PlayerState>()(
|
|
persist(
|
|
(set, get) => {
|
|
|
|
return {
|
|
currentTrack: null,
|
|
waveformBins: null,
|
|
normalizationNowDb: null,
|
|
normalizationTargetLufs: null,
|
|
normalizationEngineLive: 'off',
|
|
normalizationDbgSource: null,
|
|
normalizationDbgTrackId: null,
|
|
normalizationDbgCacheGainDb: null,
|
|
normalizationDbgCacheTargetLufs: null,
|
|
normalizationDbgCacheUpdatedAt: null,
|
|
normalizationDbgLastEventAt: null,
|
|
currentRadio: null,
|
|
currentPlaybackSource: null,
|
|
enginePreloadedTrackId: null,
|
|
queue: [],
|
|
queueIndex: 0,
|
|
isPlaying: false,
|
|
progress: 0,
|
|
buffered: 0,
|
|
currentTime: 0,
|
|
volume: 0.8,
|
|
scrobbled: false,
|
|
lastfmLoved: false,
|
|
lastfmLovedCache: {},
|
|
starredOverrides: {},
|
|
userRatingOverrides: {},
|
|
isQueueVisible: readInitialQueueVisibility(),
|
|
isFullscreenOpen: false,
|
|
scheduledPauseAtMs: null,
|
|
scheduledPauseStartMs: null,
|
|
scheduledResumeAtMs: null,
|
|
scheduledResumeStartMs: null,
|
|
repeatMode: 'off',
|
|
contextMenu: { isOpen: false, x: 0, y: 0, item: null, type: null },
|
|
songInfoModal: { isOpen: false, songId: null },
|
|
|
|
...createUiStateActions(set),
|
|
...createLastfmActions(set, get),
|
|
...createQueueMutationActions(set, get),
|
|
...createTransportLightActions(set, get),
|
|
...createUndoRedoActions(set, get),
|
|
...createMiscActions(set, get),
|
|
...createScheduleActions(set, get),
|
|
|
|
// ── playTrack ────────────────────────────────────────────────────────────
|
|
playTrack: (track, queue, manual = true, _orbitConfirmed = false, targetQueueIndex) =>
|
|
runPlayTrack(set, get, track, queue, manual, _orbitConfirmed, targetQueueIndex),
|
|
|
|
// ── resume ───────────────────────────────────────────────────────────────
|
|
resume: () => runResume(set, get),
|
|
|
|
// ── next ────────────────────────────────────────────────────────────────
|
|
next: (manual = true) => runNext(set, get, manual),
|
|
|
|
|
|
// ── seek ─────────────────────────────────────────────────────────────────
|
|
seek: (progress) => runSeek(set, get, progress),
|
|
|
|
updateReplayGainForCurrentTrack: () => runUpdateReplayGainForCurrentTrack(set, get),
|
|
};
|
|
},
|
|
{
|
|
name: 'psysonic-player',
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({
|
|
volume: state.volume,
|
|
repeatMode: state.repeatMode,
|
|
currentTrack: state.currentTrack,
|
|
queue: state.queue,
|
|
queueIndex: state.queueIndex,
|
|
isQueueVisible: state.isQueueVisible,
|
|
// currentTime is intentionally NOT persisted here.
|
|
// handleAudioProgress fires every 100ms and each setState with a
|
|
// persisted field triggers a full JSON serialisation of the queue to
|
|
// localStorage. After ~10 minutes of Artist Radio the queue grows to
|
|
// 50+ tracks; 6 000+ synchronous SQLite writes cause WKWebView's
|
|
// storage process to crash on macOS → black screen + audio stop.
|
|
// Resume position is recovered from Subsonic savePlayQueue (5s debounce).
|
|
lastfmLovedCache: state.lastfmLovedCache,
|
|
}),
|
|
}
|
|
)
|
|
);
|
|
|
|
usePlayerStore.subscribe((state, prev) => {
|
|
if (
|
|
state.currentTime === prev.currentTime &&
|
|
state.progress === prev.progress &&
|
|
state.buffered === prev.buffered
|
|
) return;
|
|
emitPlaybackProgress({
|
|
currentTime: state.currentTime,
|
|
progress: state.progress,
|
|
buffered: state.buffered,
|
|
});
|
|
});
|
|
|