mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
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:
committed by
GitHub
parent
2c5659b425
commit
0c2aa993f2
@@ -119,10 +119,7 @@ import {
|
|||||||
import { prefetchLoudnessForEnqueuedTracks } from './loudnessPrefetch';
|
import { prefetchLoudnessForEnqueuedTracks } from './loudnessPrefetch';
|
||||||
import { initAudioListeners } from './initAudioListeners';
|
import { initAudioListeners } from './initAudioListeners';
|
||||||
import { installQueueUndoHotkey } from './queueUndoHotkey';
|
import { installQueueUndoHotkey } from './queueUndoHotkey';
|
||||||
import {
|
import { readInitialQueueVisibility } from './queueVisibilityStorage';
|
||||||
persistQueueVisibility,
|
|
||||||
readInitialQueueVisibility,
|
|
||||||
} from './queueVisibilityStorage';
|
|
||||||
|
|
||||||
// Re-export so MainApp + the 3 playerStore characterization tests keep
|
// Re-export so MainApp + the 3 playerStore characterization tests keep
|
||||||
// their existing `from './playerStore'` imports.
|
// their existing `from './playerStore'` imports.
|
||||||
@@ -175,6 +172,7 @@ import type { PlayerState, Track } from './playerStoreTypes';
|
|||||||
export type { PlayerState, Track };
|
export type { PlayerState, Track };
|
||||||
import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot';
|
import { applyQueueHistorySnapshot } from './applyQueueHistorySnapshot';
|
||||||
import { createLastfmActions } from './lastfmActions';
|
import { createLastfmActions } from './lastfmActions';
|
||||||
|
import { createUiStateActions } from './uiStateActions';
|
||||||
|
|
||||||
|
|
||||||
// ─── Module-level playback primitives ─────────────────────────────────────────
|
// ─── Module-level playback primitives ─────────────────────────────────────────
|
||||||
@@ -212,20 +210,7 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
lastfmLoved: false,
|
lastfmLoved: false,
|
||||||
lastfmLovedCache: {},
|
lastfmLovedCache: {},
|
||||||
starredOverrides: {},
|
starredOverrides: {},
|
||||||
setStarredOverride: (id, starred) => set(s => ({ starredOverrides: { ...s.starredOverrides, [id]: starred } })),
|
|
||||||
userRatingOverrides: {},
|
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(),
|
isQueueVisible: readInitialQueueVisibility(),
|
||||||
isFullscreenOpen: false,
|
isFullscreenOpen: false,
|
||||||
scheduledPauseAtMs: null,
|
scheduledPauseAtMs: null,
|
||||||
@@ -234,37 +219,11 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
scheduledResumeStartMs: null,
|
scheduledResumeStartMs: null,
|
||||||
repeatMode: 'off',
|
repeatMode: 'off',
|
||||||
contextMenu: { isOpen: false, x: 0, y: 0, item: null, type: null },
|
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 },
|
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),
|
...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 ────────────────────────────────────────────────────────────────
|
||||||
stop: () => {
|
stop: () => {
|
||||||
clearAllPlaybackScheduleTimers();
|
clearAllPlaybackScheduleTimers();
|
||||||
|
|||||||
@@ -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] };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user