mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-13 20:10:07 +02:00
add fullscreen visualizer (#1546)
This commit is contained in:
@@ -237,7 +237,7 @@ const Controls = () => {
|
||||
const lyricConfig = { ...lyricsSettings, ...displaySettings };
|
||||
|
||||
const handleToggleFullScreenPlayer = () => {
|
||||
setStore({ expanded: !expanded });
|
||||
setStore({ expanded: !expanded, visualizerExpanded: false });
|
||||
};
|
||||
|
||||
const handleLyricsSettings = (property: string, value: any) => {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { motion } from 'motion/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './full-screen-visualizer.module.css';
|
||||
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { usePlayerSong } from '/@/renderer/store/player.store';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
export const FullScreenVisualizerSongInfo = () => {
|
||||
const currentSong = usePlayerSong();
|
||||
const [showSongInfo, setShowSongInfo] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
usePlayerEvents(
|
||||
{
|
||||
onCurrentSongChange: () => {
|
||||
setShowSongInfo(true);
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setShowSongInfo(false);
|
||||
}, 3000);
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const overlayVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 1.5,
|
||||
ease: 'easeInOut' as const,
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: 'easeInOut' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!currentSong) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
animate={showSongInfo ? 'visible' : 'hidden'}
|
||||
className={styles.songInfoBackdrop}
|
||||
initial="hidden"
|
||||
variants={overlayVariants}
|
||||
/>
|
||||
<motion.div
|
||||
animate={showSongInfo ? 'visible' : 'hidden'}
|
||||
className={styles.songInfoOverlay}
|
||||
initial="hidden"
|
||||
variants={overlayVariants}
|
||||
>
|
||||
<Stack align="center" gap="lg" justify="center">
|
||||
<TextTitle className={styles.songInfoTitle} fw="800" isNoSelect order={1}>
|
||||
{currentSong.name}
|
||||
</TextTitle>
|
||||
{currentSong.artistName && (
|
||||
<Text className={styles.songInfoArtist} isNoSelect>
|
||||
{currentSong.artistName}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
.container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
z-index: 201;
|
||||
background: rgb(var(--theme-colors-background-transparent), 80%);
|
||||
}
|
||||
|
||||
.visualizer-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.song-info-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
.song-info-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 51;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 60%;
|
||||
padding: var(--theme-spacing-lg);
|
||||
color: var(--theme-colors-foreground);
|
||||
text-align: center;
|
||||
text-shadow: 0 2px 8px rgb(0 0 0 / 50%);
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.song-info-title {
|
||||
font-size: clamp(2rem, 8vw, 6rem);
|
||||
line-height: 1.2;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.song-info-artist {
|
||||
font-size: clamp(1.25rem, 5vw, 3.5rem);
|
||||
line-height: 1.3;
|
||||
color: #ddd;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { motion, Variants } from 'motion/react';
|
||||
import { lazy, memo, ReactNode, Suspense, 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 { 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 { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||
import { Platform, PlayerType } 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 { type, webAudio } = usePlaybackSettings();
|
||||
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const location = useLocation();
|
||||
const isOpenedRef = useRef<boolean | null>(null);
|
||||
|
||||
const handleCloseVisualizer = () => {
|
||||
setStore({ visualizerExpanded: false });
|
||||
};
|
||||
|
||||
useHotkeys([['Escape', handleCloseVisualizer]]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isOpenedRef.current !== null) {
|
||||
setStore({ visualizerExpanded: false });
|
||||
}
|
||||
|
||||
isOpenedRef.current = true;
|
||||
}, [location, setStore]);
|
||||
|
||||
return (
|
||||
<VisualizerContainer isMobile={isMobile} windowBarStyle={windowBarStyle}>
|
||||
<div className={styles.visualizerContainer}>
|
||||
{type === PlayerType.WEB && webAudio ? (
|
||||
<Suspense fallback={<></>}>
|
||||
{visualizerType === 'butterchurn' ? (
|
||||
<ButterchurnVisualizer />
|
||||
) : (
|
||||
<AudioMotionAnalyzerVisualizer />
|
||||
)}
|
||||
</Suspense>
|
||||
) : null}
|
||||
<FullScreenVisualizerSongInfo />
|
||||
</div>
|
||||
</VisualizerContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user