diff --git a/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx b/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx index 5d0be31a6..a3fa242db 100644 --- a/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx +++ b/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx @@ -5,7 +5,12 @@ import styles from './visualizer.module.css'; import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio'; import { openVisualizerSettingsModal } from '/@/renderer/features/player/utils/open-visualizer-settings-modal'; import { ComponentErrorBoundary } from '/@/renderer/features/shared/components/component-error-boundary'; -import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store'; +import { + subscribeButterchurnPreset, + useButterchurnSettings, + useSettingsStore, + useSettingsStoreActions, +} from '/@/renderer/store'; import { useFullScreenPlayerStore, useFullScreenPlayerStoreActions, @@ -39,7 +44,7 @@ const VisualizerInner = () => { const cycleStartTimeRef = useRef(undefined); const pauseTimerRef = useRef(undefined); const initialPresetLoadedRef = useRef(false); - const butterchurnSettings = useSettingsStore((store) => store.visualizer.butterchurn); + const butterchurnSettings = useButterchurnSettings(); const opacity = useSettingsStore((store) => store.visualizer.butterchurn.opacity); const { setSettings } = useSettingsStoreActions(); const playerStatus = usePlayerStatus(); @@ -249,10 +254,9 @@ const VisualizerInner = () => { const presetNames = Object.keys(presets); if (presetNames.length > 0) { + const currentPreset = useSettingsStore.getState().visualizer.butterchurn.currentPreset; const presetName = - butterchurnSettings.currentPreset && presets[butterchurnSettings.currentPreset] - ? butterchurnSettings.currentPreset - : presetNames[0]; + currentPreset && presets[currentPreset] ? currentPreset : presetNames[0]; const preset = presets[presetName]; if (preset) { @@ -261,53 +265,14 @@ const VisualizerInner = () => { initialPresetLoadedRef.current = true; } } - }, [ - isVisualizerReady, - butterchurnSettings.currentPreset, - butterchurnSettings.blendTime, - librariesLoaded, - ]); + }, [isVisualizerReady, butterchurnSettings.blendTime, librariesLoaded]); - // Update preset when currentPreset or blendTime changes (but not when cycling) const isCyclingRef = useRef(false); - useEffect(() => { - const visualizer = visualizerRef.current; - if ( - !visualizer || - !butterchurnSettings.currentPreset || - !initialPresetLoadedRef.current || - !librariesLoaded - ) - return; - - // Skip if we're currently cycling (to avoid reloading preset) - if (isCyclingRef.current) { - isCyclingRef.current = false; - return; - } - - const presets = butterchurnPresetsRef.current; - if (!presets) return; - const preset = presets[butterchurnSettings.currentPreset]; - - if (preset) { - visualizer.loadPreset(preset, butterchurnSettings.blendTime || 0.0); - // Reset cycle timer when preset changes manually - cycleStartTimeRef.current = Date.now(); - } - }, [butterchurnSettings.currentPreset, butterchurnSettings.blendTime, librariesLoaded]); - // Handle preset cycling useEffect(() => { const visualizer = visualizerRef.current; - if ( - !visualizer || - !butterchurnSettings.cyclePresets || - !initialPresetLoadedRef.current || - !librariesLoaded - ) { - // Clear cycle timer if cycling is disabled or visualizer not ready + if (!visualizer || !butterchurnSettings.cyclePresets || !librariesLoaded) { if (cycleTimerRef.current) { clearInterval(cycleTimerRef.current); cycleTimerRef.current = undefined; @@ -316,6 +281,7 @@ const VisualizerInner = () => { } const presets = butterchurnPresetsRef.current; + if (!presets) return; const allPresetNames = Object.keys(presets); @@ -342,7 +308,8 @@ const VisualizerInner = () => { const currentVisualizer = visualizerRef.current; if (!currentVisualizer) return; - const currentPresetName = butterchurnSettings.currentPreset; + const currentPresetName = + useSettingsStore.getState().visualizer.butterchurn.currentPreset; let nextPresetName: string; if (butterchurnSettings.randomizeNextPreset) { @@ -365,16 +332,12 @@ const VisualizerInner = () => { const nextPreset = presets[nextPresetName]; if (nextPreset) { - // Get current settings to ensure we use the latest blendTime const currentSettings = useSettingsStore.getState().visualizer.butterchurn; - // Mark that we're cycling to prevent the preset change effect from running isCyclingRef.current = true; - // Load the preset with blending currentVisualizer.loadPreset(nextPreset, currentSettings.blendTime || 0.0); - // Update currentPreset in settings setSettings({ visualizer: { butterchurn: { @@ -387,7 +350,6 @@ const VisualizerInner = () => { } }; - // Check every second if it's time to cycle cycleTimerRef.current = setInterval(() => { if (cycleStartTimeRef.current === undefined) { cycleStartTimeRef.current = Date.now(); @@ -413,7 +375,6 @@ const VisualizerInner = () => { butterchurnSettings.selectedPresets, butterchurnSettings.ignoredPresets, butterchurnSettings.randomizeNextPreset, - butterchurnSettings.currentPreset, setSettings, librariesLoaded, ]); @@ -453,8 +414,40 @@ const VisualizerInner = () => { }; }, [isVisualizerReady, butterchurnSettings.maxFPS]); - // Render container when playing (for initialization) or when visualizer exists - // Canvas must always be rendered when container is rendered so refs are available + // Handle preset changes via subscriber + useEffect(() => { + const unsubscribe = subscribeButterchurnPreset((presetName) => { + const visualizer = visualizerRef.current; + if ( + !visualizer || + !isVisualizerReady || + !librariesLoaded || + !presetName || + !initialPresetLoadedRef.current + ) { + return; + } + + if (isCyclingRef.current) { + isCyclingRef.current = false; + return; + } + + const presets = butterchurnPresetsRef.current; + if (!presets) return; + + const preset = presets[presetName]; + if (preset && typeof preset === 'object') { + visualizer.loadPreset(preset, butterchurnSettings.blendTime || 0.0); + cycleStartTimeRef.current = Date.now(); + } + }); + + return () => { + unsubscribe(); + }; + }, [isVisualizerReady, librariesLoaded, butterchurnSettings.blendTime]); + const shouldRenderContainer = isPlaying || isVisualizerReady; if (!shouldRenderContainer) { @@ -468,20 +461,25 @@ const VisualizerInner = () => { style={{ opacity: isVisualizerReady ? opacity : 0 }} > - {isVisualizerReady && butterchurnSettings.currentPreset && ( - - {butterchurnSettings.currentPreset} - - )} + {isVisualizerReady && } ); }; +const CurrentPresetDisplay = () => { + const currentPreset = useSettingsStore.getState().visualizer.butterchurn.currentPreset; + return ( + + {currentPreset} + + ); +}; + export const Visualizer = () => { const { visualizerExpanded } = useFullScreenPlayerStore(); const { setStore } = useFullScreenPlayerStoreActions(); const { setSettings } = useSettingsStoreActions(); - const butterchurnSettings = useSettingsStore((store) => store.visualizer.butterchurn); + const butterchurnSettings = useButterchurnSettings(); const [presetsLoaded, setPresetsLoaded] = useState(false); const butterchurnPresetsRef = useRef(null); @@ -542,7 +540,7 @@ export const Visualizer = () => { const presetList = getPresetList(); if (presetList.length === 0) return; - const currentPresetName = butterchurnSettings.currentPreset; + const currentPresetName = useSettingsStore.getState().visualizer.butterchurn.currentPreset; const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1; const nextIndex = currentIndex >= 0 && currentIndex < presetList.length - 1 ? currentIndex + 1 : 0; @@ -563,7 +561,7 @@ export const Visualizer = () => { const presetList = getPresetList(); if (presetList.length === 0) return; - const currentPresetName = butterchurnSettings.currentPreset; + const currentPresetName = useSettingsStore.getState().visualizer.butterchurn.currentPreset; const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1; const prevIndex = currentIndex > 0 ? currentIndex - 1 : presetList.length - 1; const prevPresetName = presetList[prevIndex]; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index e7463c0e0..5e4f07800 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -3,7 +3,7 @@ import mergeWith from 'lodash/mergeWith'; import { nanoid } from 'nanoid'; import { generatePath } from 'react-router'; import { z } from 'zod'; -import { devtools, persist } from 'zustand/middleware'; +import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { shallow } from 'zustand/shallow'; import { createWithEqualityFn } from 'zustand/traditional'; @@ -1631,98 +1631,102 @@ const initialState: SettingsState = { export const useSettingsStore = createWithEqualityFn()( persist( devtools( - immer((set) => ({ - actions: { - reset: () => { - localStorage.removeItem('store_settings'); - window.location.reload(); - }, - resetSampleRate: () => { - set((state) => { - state.playback.mpvProperties.audioSampleRateHz = 0; - }); - }, - setArtistItems: (items) => { - set((state) => { - state.general.artistItems = items; - }); - }, - setArtistReleaseTypeItems: (items: SortableItem[]) => { - set((state) => { - state.general.artistReleaseTypeItems = items; - }); - }, - setGenreBehavior: (target: GenreTarget) => { - set((state) => { - state.general.genreTarget = target; - }); - }, - setHomeItems: (items: SortableItem[]) => { - set((state) => { - state.general.homeItems = items; - }); - }, - setList: (type: ItemListKey, data: DeepPartial) => { - set((state) => { - const listState = state.lists[type]; + subscribeWithSelector( + immer((set) => ({ + actions: { + reset: () => { + localStorage.removeItem('store_settings'); + window.location.reload(); + }, + resetSampleRate: () => { + set((state) => { + state.playback.mpvProperties.audioSampleRateHz = 0; + }); + }, + setArtistItems: (items) => { + set((state) => { + state.general.artistItems = items; + }); + }, + setArtistReleaseTypeItems: ( + items: SortableItem[], + ) => { + set((state) => { + state.general.artistReleaseTypeItems = items; + }); + }, + setGenreBehavior: (target: GenreTarget) => { + set((state) => { + state.general.genreTarget = target; + }); + }, + setHomeItems: (items: SortableItem[]) => { + set((state) => { + state.general.homeItems = items; + }); + }, + setList: (type: ItemListKey, data: DeepPartial) => { + set((state) => { + const listState = state.lists[type]; - if (listState && data.table) { - Object.assign(listState.table, data.table); - delete data.table; - } + if (listState && data.table) { + Object.assign(listState.table, data.table); + delete data.table; + } - if (listState && data.grid) { - Object.assign(listState.grid, data.grid); - delete data.grid; - } + if (listState && data.grid) { + Object.assign(listState.grid, data.grid); + delete data.grid; + } - if (listState) { - Object.assign(listState, data); - } - }); + if (listState) { + Object.assign(listState, data); + } + }); + }, + setPlaybackFilters: (filters: PlayerFilter[]) => { + set((state) => { + state.playback.filters = filters; + }); + }, + setSettings: (data) => { + set((state) => { + deepMergeIntoState(state, data); + }); + }, + setSidebarItems: (items: SidebarItemType[]) => { + set((state) => { + state.general.sidebarItems = items; + }); + }, + setTable: (type: ItemListKey, data: DataTableProps) => { + set((state) => { + const listState = state.lists[type]; + if (listState) { + listState.table = data; + } + }); + }, + setTranscodingConfig: (config) => { + set((state) => { + state.playback.transcode = config; + }); + }, + toggleMediaSession: () => { + set((state) => { + state.playback.mediaSession = !state.playback.mediaSession; + }); + }, + toggleSidebarCollapseShare: () => { + set((state) => { + state.general.sidebarCollapseShared = + !state.general.sidebarCollapseShared; + }); + }, }, - setPlaybackFilters: (filters: PlayerFilter[]) => { - set((state) => { - state.playback.filters = filters; - }); - }, - setSettings: (data) => { - set((state) => { - deepMergeIntoState(state, data); - }); - }, - setSidebarItems: (items: SidebarItemType[]) => { - set((state) => { - state.general.sidebarItems = items; - }); - }, - setTable: (type: ItemListKey, data: DataTableProps) => { - set((state) => { - const listState = state.lists[type]; - if (listState) { - listState.table = data; - } - }); - }, - setTranscodingConfig: (config) => { - set((state) => { - state.playback.transcode = config; - }); - }, - toggleMediaSession: () => { - set((state) => { - state.playback.mediaSession = !state.playback.mediaSession; - }); - }, - toggleSidebarCollapseShare: () => { - set((state) => { - state.general.sidebarCollapseShared = - !state.general.sidebarCollapseShared; - }); - }, - }, - ...initialState, - })), + ...initialState, + })), + ), { name: 'store_settings' }, ), { @@ -2175,3 +2179,30 @@ export const useShowVisualizerInSidebar = () => export const useAutoDJSettings = () => useSettingsStore((store) => store.autoDJ, shallow); export const useVisualizerSettings = () => useSettingsStore((store) => store.visualizer, shallow); + +export const subscribeButterchurnPreset = ( + onChange: (preset: string | undefined, prevPreset: string | undefined) => void, +) => { + return useSettingsStore.subscribe( + (state) => state.visualizer.butterchurn.currentPreset, + (preset, prevPreset) => { + onChange(preset, prevPreset); + }, + ); +}; + +export const useButterchurnSettings = () => { + return useSettingsStore((store) => { + return { + blendTime: store.visualizer.butterchurn.blendTime, + cyclePresets: store.visualizer.butterchurn.cyclePresets, + cycleTime: store.visualizer.butterchurn.cycleTime, + ignoredPresets: store.visualizer.butterchurn.ignoredPresets, + includeAllPresets: store.visualizer.butterchurn.includeAllPresets, + maxFPS: store.visualizer.butterchurn.maxFPS, + opacity: store.visualizer.butterchurn.opacity, + randomizeNextPreset: store.visualizer.butterchurn.randomizeNextPreset, + selectedPresets: store.visualizer.butterchurn.selectedPresets, + }; + }, shallow); +};