add fullscreen visualizer (#1546)

This commit is contained in:
jeffvli
2026-01-18 02:17:55 -08:00
parent 27a5153b8a
commit 0e388dabf5
14 changed files with 555 additions and 34 deletions
@@ -14,19 +14,21 @@
margin: auto;
}
&:hover {
.settings-icon {
opacity: 1;
}
}
}
.container .settings-icon {
.icon-group {
z-index: 100;
}
.icon-group > * {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.container:hover .icon-group > * {
opacity: 1;
}
.visualizer {
width: 100%;
max-width: 100%;
@@ -6,7 +6,12 @@ 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 { useAccent, useSettingsStore } from '/@/renderer/store';
import {
useFullScreenPlayerStore,
useFullScreenPlayerStoreActions,
} from '/@/renderer/store/full-screen-player.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
const VisualizerInner = () => {
const { webAudio } = useWebAudio();
@@ -289,18 +294,35 @@ const VisualizerInner = () => {
};
export const Visualizer = () => {
const { visualizerExpanded } = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
const handleToggleFullscreen = () => {
setStore({ expanded: false, visualizerExpanded: !visualizerExpanded });
};
return (
<div className={styles.container}>
<ActionIcon
className={styles.settingsIcon}
icon="settings2"
iconProps={{ size: 'lg' }}
onClick={openVisualizerSettingsModal}
<Group
className={styles.iconGroup}
gap="xs"
pos="absolute"
right="var(--theme-spacing-sm)"
top="var(--theme-spacing-sm)"
variant="subtle"
/>
>
<ActionIcon
icon="expand"
iconProps={{ size: 'lg' }}
onClick={handleToggleFullscreen}
variant="subtle"
/>
<ActionIcon
icon="settings2"
iconProps={{ size: 'lg' }}
onClick={openVisualizerSettingsModal}
variant="subtle"
/>
</Group>
<ComponentErrorBoundary>
<VisualizerInner />
</ComponentErrorBoundary>
@@ -6,19 +6,21 @@
max-height: 100%;
margin: auto;
overflow: hidden;
&:hover {
.settings-icon {
opacity: 1;
}
}
background: #000;
}
.container .settings-icon {
.icon-group {
z-index: 100;
}
.icon-group > * {
opacity: 0;
}
.container:hover .icon-group > * {
opacity: 1;
}
.canvas {
display: block;
width: 100%;
@@ -34,7 +36,7 @@
z-index: 10;
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
font-weight: 500;
color: var(--theme-colors-foreground);
color: #ddd;
pointer-events: none;
background-color: rgb(0 0 0 / 50%);
border-radius: 0 var(--theme-radius-md) 0 0;
@@ -1,4 +1,4 @@
import { createRef, useEffect, useRef, useState } from 'react';
import { createRef, useCallback, useEffect, useRef, useState } from 'react';
import styles from './visualizer.module.css';
@@ -6,8 +6,13 @@ 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 {
useFullScreenPlayerStore,
useFullScreenPlayerStoreActions,
} from '/@/renderer/store/full-screen-player.store';
import { usePlayerStatus } from '/@/renderer/store/player.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { Text } from '/@/shared/components/text/text';
import { PlayerStatus } from '/@/shared/types/types';
@@ -473,18 +478,147 @@ const VisualizerInner = () => {
};
export const Visualizer = () => {
const { visualizerExpanded } = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
const { setSettings } = useSettingsStoreActions();
const butterchurnSettings = useSettingsStore((store) => store.visualizer.butterchurn);
const [presetsLoaded, setPresetsLoaded] = useState(false);
const butterchurnPresetsRef = useRef<any>(null);
useEffect(() => {
let isMounted = true;
const loadPresets = async () => {
try {
const presetsModule = await import('butterchurn-presets');
if (isMounted) {
butterchurnPresetsRef.current = presetsModule.default;
setPresetsLoaded(true);
}
} catch (error) {
console.error('Failed to load butterchurn presets:', error);
}
};
loadPresets();
return () => {
isMounted = false;
};
}, []);
const getPresetList = useCallback(() => {
const presets = butterchurnPresetsRef.current;
if (!presets) return [];
const allPresetNames = Object.keys(presets);
let presetList = butterchurnSettings.includeAllPresets
? allPresetNames
: butterchurnSettings.selectedPresets.length > 0
? butterchurnSettings.selectedPresets.filter((name) => presets[name])
: allPresetNames;
if (butterchurnSettings.ignoredPresets && butterchurnSettings.ignoredPresets.length > 0) {
presetList = presetList.filter(
(name) => !butterchurnSettings.ignoredPresets.includes(name),
);
}
return presetList;
}, [
butterchurnSettings.includeAllPresets,
butterchurnSettings.selectedPresets,
butterchurnSettings.ignoredPresets,
]);
const handleToggleFullscreen = () => {
setStore({ expanded: false, visualizerExpanded: !visualizerExpanded });
};
const handleNextPreset = () => {
if (!presetsLoaded) return;
const presetList = getPresetList();
if (presetList.length === 0) return;
const currentPresetName = butterchurnSettings.currentPreset;
const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1;
const nextIndex =
currentIndex >= 0 && currentIndex < presetList.length - 1 ? currentIndex + 1 : 0;
const nextPresetName = presetList[nextIndex];
setSettings({
visualizer: {
butterchurn: {
currentPreset: nextPresetName,
},
},
});
};
const handlePreviousPreset = () => {
if (!presetsLoaded) return;
const presetList = getPresetList();
if (presetList.length === 0) return;
const currentPresetName = butterchurnSettings.currentPreset;
const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1;
const prevIndex = currentIndex > 0 ? currentIndex - 1 : presetList.length - 1;
const prevPresetName = presetList[prevIndex];
setSettings({
visualizer: {
butterchurn: {
currentPreset: prevPresetName,
},
},
});
};
return (
<div className={styles.container}>
<ActionIcon
className={styles.settingsIcon}
icon="settings2"
iconProps={{ size: 'lg' }}
onClick={openVisualizerSettingsModal}
<Group
className={styles.iconGroup}
gap="xs"
pos="absolute"
right="var(--theme-spacing-sm)"
top="var(--theme-spacing-sm)"
variant="subtle"
/>
>
<ActionIcon
icon="expand"
iconProps={{ size: 'lg' }}
onClick={handleToggleFullscreen}
variant="subtle"
/>
<ActionIcon
icon="settings2"
iconProps={{ size: 'lg' }}
onClick={openVisualizerSettingsModal}
variant="subtle"
/>
</Group>
<Group
className={styles.iconGroup}
gap="xs"
pos="absolute"
right="var(--theme-spacing-sm)"
style={{ bottom: 'var(--theme-spacing-sm)' }}
>
<ActionIcon
icon="arrowLeftS"
iconProps={{ size: 'lg' }}
onClick={handlePreviousPreset}
variant="subtle"
/>
<ActionIcon
icon="arrowRightS"
iconProps={{ size: 'lg' }}
onClick={handleNextPreset}
variant="subtle"
/>
</Group>
<ComponentErrorBoundary>
<VisualizerInner />
</ComponentErrorBoundary>