Files
feishin/src/renderer/features/player/components/full-screen-visualizer.tsx
T
jaxi e402bd5424 fix: f11 fullscreen was completely broken on linux/windows; feat: butterchurn preset expansion + randomized preset arrows (#2280)
* added a ton of visualizers

* added f11 toggle functionality, f11 makes visualizer cover entire screen when pressed while visualizer is maximized

Bundling the additional butterchurn preset packs (extra, md1) pushed
renderer build memory past the default ~2GB Node heap on the macOS CI
runner, causing OOM crashes during the build.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 20:49:24 -07:00

187 lines
5.8 KiB
TypeScript

import { motion, Variants } from 'motion/react';
import { lazy, memo, ReactNode, Suspense, useEffect, useLayoutEffect, useRef } from 'react';
import { useLocation } from 'react-router';
import styles from './full-screen-visualizer.module.css';
import { FullScreenVisualizerSongInfo } from '/@/renderer/features/player/components/full-screen-visualizer-song-info';
import { VISUALIZER_FULLSCREEN_TARGET_ID } from '/@/renderer/hooks/use-fullscreen-toggle';
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store';
import {
usePlaybackSettings,
useSettingsStore,
useWindowSettings,
} from '/@/renderer/store/settings.store';
import { Platform } from '/@/shared/types/types';
const AudioMotionAnalyzerVisualizer = lazy(() =>
import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({
default: module.Visualizer,
})),
);
const ButterchurnVisualizer = lazy(() =>
import('../../visualizer/components/butternchurn/visualizer').then((module) => ({
default: module.Visualizer,
})),
);
const containerVariants: Variants = {
closed: (custom) => {
const { isMobile, windowBarStyle } = custom;
const height =
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
? 'calc(100vh - 120px)'
: 'calc(100vh - 90px)';
if (isMobile) {
return {
height,
position: 'absolute',
top: '100vh',
transition: {
duration: 0.5,
ease: 'easeInOut',
},
width: '100vw',
y: 0,
};
}
return {
height,
position: 'absolute',
top: '100vh',
transition: {
duration: 0.5,
ease: 'easeInOut',
},
width: '100vw',
y: 0,
};
},
open: (custom) => {
const { isMobile, windowBarStyle } = custom;
const height =
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
? 'calc(100vh - 120px)'
: 'calc(100vh - 90px)';
const topOffset =
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS
? '30px'
: '0px';
if (isMobile) {
return {
height,
left: 0,
position: 'absolute',
top: topOffset,
transition: {
delay: 0.1,
duration: 0.5,
ease: 'easeInOut',
},
width: '100vw',
y: 0,
};
}
return {
height,
left: 0,
position: 'absolute',
top: 0,
transition: {
delay: 0.1,
duration: 0.5,
ease: 'easeInOut',
},
width: '100vw',
y: 0,
};
},
};
interface VisualizerContainerProps {
children: ReactNode;
isMobile?: boolean;
windowBarStyle: Platform;
}
const VisualizerContainer = memo(
({ children, isMobile, windowBarStyle }: VisualizerContainerProps) => {
return (
<motion.div
animate="open"
className={styles.container}
custom={{ isMobile, windowBarStyle }}
exit="closed"
initial="closed"
transition={{ duration: 2 }}
variants={containerVariants}
>
{children}
</motion.div>
);
},
);
VisualizerContainer.displayName = 'VisualizerContainer';
export const FullScreenVisualizer = () => {
const { setStore } = useFullScreenPlayerStoreActions();
const { windowBarStyle } = useWindowSettings();
const { webAudio } = usePlaybackSettings();
const visualizerType = useSettingsStore((store) => store.visualizer.type);
const isMobile = useIsMobile();
const location = useLocation();
const isOpenedRef = useRef<boolean | null>(null);
const handleCloseVisualizer = () => {
// While fullscreen, Escape is the browser's own "leave fullscreen" gesture.
// Let it drop back to the expanded-but-windowed visualizer instead of closing.
if (document.fullscreenElement) return;
setStore({ visualizerExpanded: false });
};
useHotkeys([['Escape', handleCloseVisualizer]]);
// Never leave the window stuck in fullscreen if the visualizer goes away while
// fullscreened (route change, close button, etc.).
useEffect(() => {
return () => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
}
};
}, []);
useLayoutEffect(() => {
if (isOpenedRef.current !== null) {
setStore({ visualizerExpanded: false });
}
isOpenedRef.current = true;
}, [location, setStore]);
return (
<VisualizerContainer isMobile={isMobile} windowBarStyle={windowBarStyle}>
<div className={styles.visualizerContainer} id={VISUALIZER_FULLSCREEN_TARGET_ID}>
{webAudio ? (
<Suspense fallback={<></>}>
{visualizerType === 'butterchurn' ? (
<ButterchurnVisualizer />
) : (
<AudioMotionAnalyzerVisualizer />
)}
</Suspense>
) : null}
<FullScreenVisualizerSongInfo />
</div>
</VisualizerContainer>
);
};