import clsx from 'clsx'; import { AnimatePresence, HTMLMotionProps, motion, Variants } from 'motion/react'; import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { generatePath } from 'react-router'; import { Link } from 'react-router'; import styles from './full-screen-player-image.module.css'; import { useFastAverageColor } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; import { calculateNextSong, subscribeCurrentTrack, usePlayerData, usePlayerStoreBase, } from '/@/renderer/store'; import { useSettingsStore } from '/@/renderer/store/settings.store'; import { Badge } from '/@/shared/components/badge/badge'; import { Center } from '/@/shared/components/center/center'; import { Flex } from '/@/shared/components/flex/flex'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Stack } from '/@/shared/components/stack/stack'; import { Text } from '/@/shared/components/text/text'; import { useSetState } from '/@/shared/hooks/use-set-state'; const imageVariants: 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', }, }; }, }; const scaleImageUrl = (imageSize: number, url?: null | string) => { return url ?.replace(/&size=\d+/, `&size=${imageSize}`) .replace(/\?width=\d+/, `?width=${imageSize}`) .replace(/&height=\d+/, `&height=${imageSize}`); }; const MotionImage = motion.img; const ImageWithPlaceholder = ({ className, ...props }: HTMLMotionProps<'img'> & { placeholder?: string }) => { const nativeAspectRatio = useSettingsStore((store) => store.general.nativeAspectRatio); if (!props.src) { return (
); } return ( ); }; export const FullScreenPlayerImage = () => { const mainImageRef = useRef(null); const [mainImageDimensions, setMainImageDimensions] = useState({ idealSize: 1 }); const albumArtRes = useSettingsStore((store) => store.general.albumArtRes); const { currentSong, nextSong } = usePlayerData(); const { background } = useFastAverageColor({ algorithm: 'dominant', src: currentSong?.imageUrl, srcLoaded: true, }); const imageKey = `image-${background}`; const [imageState, setImageState] = useSetState({ bottomImage: scaleImageUrl(mainImageDimensions.idealSize, nextSong?.imageUrl), current: 0, topImage: scaleImageUrl(mainImageDimensions.idealSize, currentSong?.imageUrl), }); const updateImageSize = useCallback(() => { if (mainImageRef.current) { const state = usePlayerStoreBase.getState(); const playerData = state.getQueue(); const currentIndex = state.player.index; const current = playerData.items[currentIndex]; const next = calculateNextSong(currentIndex, playerData.items, state.player.repeat); setMainImageDimensions({ idealSize: albumArtRes || Math.ceil((mainImageRef.current as HTMLDivElement).offsetHeight / 100) * 100, }); setImageState({ bottomImage: scaleImageUrl(mainImageDimensions.idealSize, next?.imageUrl), current: 0, topImage: scaleImageUrl(mainImageDimensions.idealSize, current?.imageUrl), }); } }, [mainImageDimensions.idealSize, setImageState, albumArtRes]); useLayoutEffect(() => { updateImageSize(); }, [updateImageSize]); // Use ref to track current image state to avoid recreating subscription const imageStateRef = useRef(imageState); useEffect(() => { imageStateRef.current = imageState; }, [imageState]); useEffect(() => { const unsubSongChange = subscribeCurrentTrack(({ index, song }, prev) => { // Only update if the song actually changed if (song?._uniqueId === prev.song?._uniqueId) { return; } // Use ref to get current state without causing dependency issues const isTop = imageStateRef.current.current === 0; const state = usePlayerStoreBase.getState(); const queue = state.getQueue(); const currentSong = queue.items[index]; const nextSong = calculateNextSong(index, queue.items, state.player.repeat); const currentImageUrl = scaleImageUrl( mainImageDimensions.idealSize, currentSong?.imageUrl, ); const nextImageUrl = scaleImageUrl(mainImageDimensions.idealSize, nextSong?.imageUrl); setImageState({ bottomImage: isTop ? currentImageUrl : nextImageUrl, current: isTop ? 1 : 0, topImage: isTop ? nextImageUrl : currentImageUrl, }); }); return () => { unsubSongChange(); }; }, [mainImageDimensions.idealSize, setImageState]); return (
{imageState.current === 0 && ( )} {imageState.current === 1 && ( )}
{currentSong?.name} {currentSong?.album} {currentSong?.artists?.map((artist, index) => ( {index > 0 && ( )} {artist.name} ))} {currentSong?.container && ( {currentSong?.container} )} {currentSong?.releaseYear && ( {currentSong?.releaseYear} )}
); };