mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
refactor butterchurn state to prevent rerender flicker on cycle
This commit is contained in:
@@ -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<number | undefined>(undefined);
|
||||
const pauseTimerRef = useRef<NodeJS.Timeout | undefined>(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 }}
|
||||
>
|
||||
<canvas className={styles.canvas} ref={canvasRef} />
|
||||
{isVisualizerReady && butterchurnSettings.currentPreset && (
|
||||
<Text className={styles['preset-overlay']} isNoSelect size="sm">
|
||||
{butterchurnSettings.currentPreset}
|
||||
</Text>
|
||||
)}
|
||||
{isVisualizerReady && <CurrentPresetDisplay />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CurrentPresetDisplay = () => {
|
||||
const currentPreset = useSettingsStore.getState().visualizer.butterchurn.currentPreset;
|
||||
return (
|
||||
<Text className={styles['preset-overlay']} isNoSelect size="sm">
|
||||
{currentPreset}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
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<any>(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];
|
||||
|
||||
@@ -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<SettingsSlice>()(
|
||||
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<ArtistReleaseTypeItem>[]) => {
|
||||
set((state) => {
|
||||
state.general.artistReleaseTypeItems = items;
|
||||
});
|
||||
},
|
||||
setGenreBehavior: (target: GenreTarget) => {
|
||||
set((state) => {
|
||||
state.general.genreTarget = target;
|
||||
});
|
||||
},
|
||||
setHomeItems: (items: SortableItem<HomeItem>[]) => {
|
||||
set((state) => {
|
||||
state.general.homeItems = items;
|
||||
});
|
||||
},
|
||||
setList: (type: ItemListKey, data: DeepPartial<ItemListSettings>) => {
|
||||
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<ArtistReleaseTypeItem>[],
|
||||
) => {
|
||||
set((state) => {
|
||||
state.general.artistReleaseTypeItems = items;
|
||||
});
|
||||
},
|
||||
setGenreBehavior: (target: GenreTarget) => {
|
||||
set((state) => {
|
||||
state.general.genreTarget = target;
|
||||
});
|
||||
},
|
||||
setHomeItems: (items: SortableItem<HomeItem>[]) => {
|
||||
set((state) => {
|
||||
state.general.homeItems = items;
|
||||
});
|
||||
},
|
||||
setList: (type: ItemListKey, data: DeepPartial<ItemListSettings>) => {
|
||||
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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user