refactor(player): E.32 — extract UI state actions as factory (#596)

Ten pure-UI state setters move into `src/store/uiStateActions.ts` as a
`createUiStateActions(set)` factory:

  - `setStarredOverride`, `setUserRatingOverride` — optimistic overrides
  - `openContextMenu`, `closeContextMenu` — context menu modal
  - `openSongInfo`, `closeSongInfo` — song info modal
  - `toggleQueue`, `setQueueVisible` — queue panel toggle with persisted
    localStorage round-trip
  - `toggleFullscreen` — fullscreen player toggle
  - `toggleRepeat` — repeat mode cycle

All ten are pure state mutators (no audio engine / network calls).
Store body uses `...createUiStateActions(set)`. Action-factory pattern
established by E.31 reused here. `persistQueueVisibility` import drops
from playerStore.

playerStore 1504 → 1463 LOC.
This commit is contained in:
Frank Stellmacher
2026-05-12 20:25:21 +02:00
committed by GitHub
parent 2c5659b425
commit 0c2aa993f2
2 changed files with 85 additions and 44 deletions
+3 -44
View File
@@ -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<PlayerState>()(
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<PlayerState>()(
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();
+82
View File
@@ -0,0 +1,82 @@
import {
persistQueueVisibility,
} from './queueVisibilityStorage';
import type { PlayerState } from './playerStoreTypes';
type SetState = (
partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
) => 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] };
}),
};
}