import { AnimatePresence, motion, Variants } from 'motion/react'; import { CSSProperties, memo, ReactNode, useEffect, useLayoutEffect, useRef, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router'; import styles from './full-screen-player.module.css'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns'; import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image'; import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue'; import { useIsRadioActive, useRadioPlayer, } from '/@/renderer/features/radio/hooks/use-radio-player'; import { ListConfigMenu, SONG_DISPLAY_TYPES, } from '/@/renderer/features/shared/components/list-config-menu'; import { useFastAverageColor } from '/@/renderer/hooks'; import { useHotkeys } from '/@/renderer/hooks/use-hotkeys'; import { useFullScreenPlayerStore, useFullScreenPlayerStoreActions, useLyricsDisplaySettings, useLyricsSettings, usePlayerData, usePlayerSong, useSettingsStore, useSettingsStoreActions, useWindowSettings, } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; import { NumberInput } from '/@/shared/components/number-input/number-input'; import { Option } from '/@/shared/components/option/option'; import { Popover } from '/@/shared/components/popover/popover'; import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; import { Slider } from '/@/shared/components/slider/slider'; import { Switch } from '/@/shared/components/switch/switch'; import { LibraryItem } from '/@/shared/types/domain-types'; import { ItemListKey, ListDisplayType, Platform } from '/@/shared/types/types'; const mainBackground = 'var(--theme-colors-background)'; const backgroundImageVariants: Variants = { closed: { opacity: 0, transition: { duration: 0.8, ease: 'linear', }, }, initial: { opacity: 0, }, open: (custom) => { const { isOpen } = custom; return { opacity: isOpen ? 1 : 0, transition: { duration: 0.4, ease: 'linear', }, }; }, }; interface BackgroundImageProps { dynamicBackground: boolean | undefined; dynamicIsImage: boolean | undefined; } const BackgroundImage = memo(({ dynamicBackground, dynamicIsImage }: BackgroundImageProps) => { const currentSong = usePlayerSong(); const { nextSong } = usePlayerData(); const currentImageUrl = useItemImageUrl({ id: currentSong?.imageId || undefined, itemType: LibraryItem.SONG, type: 'itemCard', }); const nextImageUrl = useItemImageUrl({ id: nextSong?.imageId || undefined, itemType: LibraryItem.SONG, type: 'itemCard', }); const [imageState, setImageState] = useState({ bottomImage: nextImageUrl, current: 0, topImage: currentImageUrl, }); const previousSongRef = useRef(currentSong?._uniqueId); const imageStateRef = useRef(imageState); // Keep ref in sync useEffect(() => { imageStateRef.current = imageState; }, [imageState]); // Update images when song changes useEffect(() => { if (currentSong?._uniqueId === previousSongRef.current) { return; } const isTop = imageStateRef.current.current === 0; setImageState({ bottomImage: isTop ? currentImageUrl : nextImageUrl, current: isTop ? 1 : 0, topImage: isTop ? nextImageUrl : currentImageUrl, }); previousSongRef.current = currentSong?._uniqueId; }, [currentSong?._uniqueId, currentImageUrl, nextSong?._uniqueId, nextImageUrl]); if (!dynamicBackground || !dynamicIsImage) { return null; } const getBackgroundImageUrl = ( imageUrl: string | undefined, songId: string | undefined, albumId: string | undefined, ) => { if (!imageUrl || !songId || !albumId) { return imageUrl; } return imageUrl.replace(songId, albumId); }; // Determine which song IDs to use for keys and image URLs const topSongId = imageState.current === 0 ? currentSong?._uniqueId : nextSong?._uniqueId; const bottomSongId = imageState.current === 0 ? nextSong?._uniqueId : currentSong?._uniqueId; const topSong = imageState.current === 0 ? currentSong : nextSong; const bottomSong = imageState.current === 0 ? nextSong : currentSong; return ( {imageState.current === 0 && imageState.topImage && ( )} {imageState.current === 1 && imageState.bottomImage && ( )} ); }); BackgroundImage.displayName = 'BackgroundImage'; interface BackgroundImageOverlayProps { dynamicBackground: boolean | undefined; dynamicImageBlur: number | undefined; } const BackgroundImageOverlay = memo( ({ dynamicBackground, dynamicImageBlur }: BackgroundImageOverlayProps) => { if (!dynamicBackground) { return null; } return (
); }, ); BackgroundImageOverlay.displayName = 'BackgroundImageOverlay'; const Controls = () => { const { t } = useTranslation(); const { dynamicBackground, dynamicImageBlur, dynamicIsImage, expanded, opacity, useImageAspectRatio, } = useFullScreenPlayerStore(); const { setStore } = useFullScreenPlayerStoreActions(); const { setSettings } = useSettingsStoreActions(); const lyricsSettings = useLyricsSettings(); const displaySettings = useLyricsDisplaySettings('default'); const lyricConfig = { ...lyricsSettings, ...displaySettings }; const handleToggleFullScreenPlayer = () => { setStore({ expanded: !expanded, visualizerExpanded: false }); }; const handleLyricsSettings = (property: string, value: any) => { const displayProperties = ['fontSize', 'fontSizeUnsync', 'gap', 'gapUnsync']; if (displayProperties.includes(property)) { const currentDisplay = useSettingsStore.getState().lyricsDisplay; setSettings({ lyricsDisplay: { ...currentDisplay, default: { ...currentDisplay.default, [property]: value, }, }, }); } else { setSettings({ lyrics: { ...useSettingsStore.getState().lyrics, [property]: value, }, }); } }; useHotkeys([['Escape', handleToggleFullScreenPlayer]]); return ( {dynamicBackground && ( )} {dynamicBackground && dynamicIsImage && ( )} {dynamicBackground && ( )} ); }; const containerVariants: Variants = { closed: (custom) => { const { windowBarStyle } = custom; return { height: windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 'calc(100vh - 120px)' : 'calc(100vh - 90px)', position: 'absolute', top: '100vh', transition: { duration: 0.5, ease: 'easeInOut', }, width: '100vw', y: 0, }; }, open: (custom) => { const { background, dynamicBackground, windowBarStyle } = custom; return { backgroundColor: dynamicBackground ? background : mainBackground, height: windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 'calc(100vh - 120px)' : 'calc(100vh - 90px)', left: 0, position: 'absolute', top: 0, transition: { delay: 0.1, duration: 0.5, ease: 'easeInOut', }, width: '100vw', y: 0, }; }, }; interface PlayerContainerProps { children: ReactNode; dynamicBackground: boolean | undefined; dynamicIsImage: boolean | undefined; windowBarStyle: Platform; } const PlayerContainer = memo( ({ children, dynamicBackground, dynamicIsImage, windowBarStyle }: PlayerContainerProps) => { const currentSong = usePlayerSong(); const imageUrl = useItemImageUrl({ id: currentSong?.imageId || undefined, imageUrl: currentSong?.imageUrl, itemType: LibraryItem.SONG, type: 'itemCard', }); const { background } = useFastAverageColor({ algorithm: 'dominant', src: imageUrl, srcLoaded: true, }); return ( {children} ); }, ); PlayerContainer.displayName = 'PlayerContainer'; export const FullScreenPlayer = () => { const { dynamicBackground, dynamicImageBlur, dynamicIsImage } = useFullScreenPlayerStore(); const { setStore } = useFullScreenPlayerStoreActions(); const { windowBarStyle } = useWindowSettings(); const isRadioActive = useIsRadioActive(); const { isPlaying: isRadioPlaying } = useRadioPlayer(); const isPlayingRadio = isRadioActive && isRadioPlaying; const effectiveDynamicBackground = dynamicBackground && !isPlayingRadio; const location = useLocation(); const isOpenedRef = useRef(null); useLayoutEffect(() => { if (isOpenedRef.current !== null) { setStore({ expanded: false }); } isOpenedRef.current = true; }, [location, setStore]); return (
); };