diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index 596df221..3bc30c74 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -119,10 +119,7 @@ import { import { prefetchLoudnessForEnqueuedTracks } from './loudnessPrefetch'; import { initAudioListeners } from './initAudioListeners'; import { installQueueUndoHotkey } from './queueUndoHotkey'; -import { - persistQueueVisibility, - readInitialQueueVisibility, -} from './queueVisibilityStorage'; +import { readInitialQueueVisibility } from './queueVisibilityStorage'; // Re-export so MainApp + the 3 playerStore characterization tests keep // their existing `from './playerStore'` imports. @@ -175,6 +172,7 @@ import type { PlayerState, Track } from './playerStoreTypes'; export type { PlayerState, Track }; import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot'; import { createLastfmActions } from './lastfmActions'; +import { createUiStateActions } from './uiStateActions'; // ─── Module-level playback primitives ───────────────────────────────────────── @@ -212,20 +210,7 @@ export const usePlayerStore = create()( lastfmLoved: false, lastfmLovedCache: {}, starredOverrides: {}, - setStarredOverride: (id, starred) => set(s => ({ starredOverrides: { ...s.starredOverrides, [id]: starred } })), userRatingOverrides: {}, - setUserRatingOverride: (id, rating) => - set(s => { - const nextOverrides = { ...s.userRatingOverrides }; - if (rating === 0) delete nextOverrides[id]; - else nextOverrides[id] = rating; - return { - userRatingOverrides: nextOverrides, - queue: s.queue.map(t => (t.id === id ? { ...t, userRating: rating } : t)), - currentTrack: - s.currentTrack?.id === id ? { ...s.currentTrack, userRating: rating } : s.currentTrack, - }; - }), isQueueVisible: readInitialQueueVisibility(), isFullscreenOpen: false, scheduledPauseAtMs: null, @@ -234,37 +219,11 @@ export const usePlayerStore = create()( scheduledResumeStartMs: null, repeatMode: 'off', contextMenu: { isOpen: false, x: 0, y: 0, item: null, type: null }, - - openContextMenu: (x, y, item, type, queueIndex, playlistId, playlistSongIndex, shareKindOverride) => set({ - contextMenu: { isOpen: true, x, y, item, type, queueIndex, playlistId, playlistSongIndex, shareKindOverride }, - }), - closeContextMenu: () => set(state => ({ - contextMenu: { ...state.contextMenu, isOpen: false }, - })), - songInfoModal: { isOpen: false, songId: null }, - openSongInfo: (songId) => set({ songInfoModal: { isOpen: true, songId } }), - closeSongInfo: () => set({ songInfoModal: { isOpen: false, songId: null } }), - - toggleQueue: () => - set(state => { - const next = !state.isQueueVisible; - persistQueueVisibility(next); - return { isQueueVisible: next }; - }), - setQueueVisible: (v: boolean) => { - persistQueueVisibility(v); - set({ isQueueVisible: v }); - }, - toggleFullscreen: () => set(state => ({ isFullscreenOpen: !state.isFullscreenOpen })), + ...createUiStateActions(set), ...createLastfmActions(set, get), - toggleRepeat: () => set(state => { - const modes = ['off', 'all', 'one'] as const; - return { repeatMode: modes[(modes.indexOf(state.repeatMode) + 1) % modes.length] }; - }), - // ── stop ──────────────────────────────────────────────────────────────── stop: () => { clearAllPlaybackScheduleTimers(); diff --git a/src/store/uiStateActions.ts b/src/store/uiStateActions.ts new file mode 100644 index 00000000..5f85f8a7 --- /dev/null +++ b/src/store/uiStateActions.ts @@ -0,0 +1,82 @@ +import { + persistQueueVisibility, +} from './queueVisibilityStorage'; +import type { PlayerState } from './playerStoreTypes'; + +type SetState = ( + partial: Partial | ((state: PlayerState) => Partial), +) => void; + +/** + * Pure-UI state setters that mutate playerStore fields with no audio + * engine / network side effects: starred + rating optimistic overrides, + * context menu modal, song info modal, queue panel visibility (with + * localStorage round-trip), fullscreen toggle, repeat mode cycle. + * + * Factored out of the playerStore `create()` body so the action set + * stays a flat object literal instead of a giant inline block. + */ +export function createUiStateActions(set: SetState): Pick< + PlayerState, + | 'setStarredOverride' + | 'setUserRatingOverride' + | 'openContextMenu' + | 'closeContextMenu' + | 'openSongInfo' + | 'closeSongInfo' + | 'toggleQueue' + | 'setQueueVisible' + | 'toggleFullscreen' + | 'toggleRepeat' +> { + return { + setStarredOverride: (id, starred) => + set(s => ({ starredOverrides: { ...s.starredOverrides, [id]: starred } })), + + setUserRatingOverride: (id, rating) => + set(s => { + const nextOverrides = { ...s.userRatingOverrides }; + if (rating === 0) delete nextOverrides[id]; + else nextOverrides[id] = rating; + return { + userRatingOverrides: nextOverrides, + queue: s.queue.map(t => (t.id === id ? { ...t, userRating: rating } : t)), + currentTrack: + s.currentTrack?.id === id ? { ...s.currentTrack, userRating: rating } : s.currentTrack, + }; + }), + + openContextMenu: (x, y, item, type, queueIndex, playlistId, playlistSongIndex, shareKindOverride) => + set({ + contextMenu: { isOpen: true, x, y, item, type, queueIndex, playlistId, playlistSongIndex, shareKindOverride }, + }), + + closeContextMenu: () => + set(state => ({ + contextMenu: { ...state.contextMenu, isOpen: false }, + })), + + openSongInfo: (songId) => set({ songInfoModal: { isOpen: true, songId } }), + closeSongInfo: () => set({ songInfoModal: { isOpen: false, songId: null } }), + + toggleQueue: () => + set(state => { + const next = !state.isQueueVisible; + persistQueueVisibility(next); + return { isQueueVisible: next }; + }), + + setQueueVisible: (v: boolean) => { + persistQueueVisibility(v); + set({ isQueueVisible: v }); + }, + + toggleFullscreen: () => set(state => ({ isFullscreenOpen: !state.isFullscreenOpen })), + + toggleRepeat: () => + set(state => { + const modes = ['off', 'all', 'one'] as const; + return { repeatMode: modes[(modes.indexOf(state.repeatMode) + 1) % modes.length] }; + }), + }; +}