diff --git a/src/renderer/features/player/components/full-screen-player-image.tsx b/src/renderer/features/player/components/full-screen-player-image.tsx index fba056ff3..b09be9661 100644 --- a/src/renderer/features/player/components/full-screen-player-image.tsx +++ b/src/renderer/features/player/components/full-screen-player-image.tsx @@ -6,14 +6,8 @@ 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 { usePlayerData, usePlayerSong } from '/@/renderer/store'; import { useSettingsStore } from '/@/renderer/store/settings.store'; import { Badge } from '/@/shared/components/badge/badge'; import { Center } from '/@/shared/components/center/center'; @@ -95,13 +89,9 @@ export const FullScreenPlayerImage = () => { 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 currentSong = usePlayerSong(); + const { nextSong } = usePlayerData(); + const [imageState, setImageState] = useSetState({ bottomImage: scaleImageUrl(mainImageDimensions.idealSize, nextSong?.imageUrl), current: 0, @@ -110,12 +100,6 @@ export const FullScreenPlayerImage = () => { 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 || @@ -123,54 +107,56 @@ export const FullScreenPlayerImage = () => { }); setImageState({ - bottomImage: scaleImageUrl(mainImageDimensions.idealSize, next?.imageUrl), + bottomImage: scaleImageUrl(mainImageDimensions.idealSize, nextSong?.imageUrl), current: 0, - topImage: scaleImageUrl(mainImageDimensions.idealSize, current?.imageUrl), + topImage: scaleImageUrl(mainImageDimensions.idealSize, currentSong?.imageUrl), }); } - }, [mainImageDimensions.idealSize, setImageState, albumArtRes]); + }, [ + mainImageDimensions.idealSize, + setImageState, + albumArtRes, + currentSong?.imageUrl, + nextSong?.imageUrl, + ]); useLayoutEffect(() => { updateImageSize(); }, [updateImageSize]); - // Use ref to track current image state to avoid recreating subscription + // Track previous song to detect changes + const previousSongRef = useRef(currentSong?._uniqueId); const imageStateRef = useRef(imageState); + + // Keep ref in sync useEffect(() => { imageStateRef.current = imageState; }, [imageState]); + // Update images when song changes useEffect(() => { - const unsubSongChange = subscribeCurrentTrack(({ index, song }, prev) => { - // Only update if the song actually changed - if (song?._uniqueId === prev.song?._uniqueId) { - return; - } + if (currentSong?._uniqueId === previousSongRef.current) { + 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 isTop = imageStateRef.current.current === 0; + const currentImageUrl = scaleImageUrl(mainImageDimensions.idealSize, currentSong?.imageUrl); + const nextImageUrl = scaleImageUrl(mainImageDimensions.idealSize, nextSong?.imageUrl); - 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, - }); + setImageState({ + bottomImage: isTop ? currentImageUrl : nextImageUrl, + current: isTop ? 1 : 0, + topImage: isTop ? nextImageUrl : currentImageUrl, }); - return () => { - unsubSongChange(); - }; - }, [mainImageDimensions.idealSize, setImageState]); + previousSongRef.current = currentSong?._uniqueId; + }, [ + currentSong?._uniqueId, + currentSong?.imageUrl, + nextSong?.imageUrl, + mainImageDimensions.idealSize, + setImageState, + ]); return ( { draggable={false} exit="closed" initial="closed" - key={imageKey} + key={`top-${currentSong?._uniqueId || 'none'}`} placeholder="var(--theme-colors-foreground-muted)" src={imageState.topImage || ''} variants={imageVariants} @@ -205,7 +191,7 @@ export const FullScreenPlayerImage = () => { draggable={false} exit="closed" initial="closed" - key={imageKey} + key={`bottom-${currentSong?._uniqueId || 'none'}`} placeholder="var(--theme-colors-foreground-muted)" src={imageState.bottomImage || ''} variants={imageVariants} diff --git a/src/renderer/features/player/components/full-screen-player.module.css b/src/renderer/features/player/components/full-screen-player.module.css index 79d888c47..de3f74301 100644 --- a/src/renderer/features/player/components/full-screen-player.module.css +++ b/src/renderer/features/player/components/full-screen-player.module.css @@ -28,6 +28,18 @@ } } +.background-image { + position: absolute; + top: 0; + left: 0; + z-index: -2; + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-position: center; + background-size: cover; +} + .background-image-overlay { position: absolute; top: 0; diff --git a/src/renderer/features/player/components/full-screen-player.tsx b/src/renderer/features/player/components/full-screen-player.tsx index 4d2a3fa92..97ff6c055 100644 --- a/src/renderer/features/player/components/full-screen-player.tsx +++ b/src/renderer/features/player/components/full-screen-player.tsx @@ -1,5 +1,13 @@ -import { motion, Variants } from 'motion/react'; -import { CSSProperties, useLayoutEffect, useRef, useState } from 'react'; +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'; @@ -14,6 +22,7 @@ import { useFullScreenPlayerStore, useFullScreenPlayerStoreActions, useLyricsSettings, + usePlayerData, usePlayerSong, useSettingsStore, useSettingsStoreActions, @@ -33,6 +42,179 @@ 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 [imageState, setImageState] = useState({ + bottomImage: nextSong?.imageUrl + ? nextSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined, + current: 0, + topImage: currentSong?.imageUrl + ? currentSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined, + }); + + 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; + const currentImageUrl = currentSong?.imageUrl + ? currentSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined; + const nextImageUrl = nextSong?.imageUrl + ? nextSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined; + + setImageState({ + bottomImage: isTop ? currentImageUrl : nextImageUrl, + current: isTop ? 1 : 0, + topImage: isTop ? nextImageUrl : currentImageUrl, + }); + + previousSongRef.current = currentSong?._uniqueId; + }, [currentSong?._uniqueId, currentSong?.imageUrl, nextSong?._uniqueId, nextSong?.imageUrl]); + + 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'; + interface ControlsProps { isPageHovered: boolean; } @@ -388,13 +570,9 @@ const containerVariants: Variants = { }; }, open: (custom) => { - const { background, backgroundImage, dynamicBackground, windowBarStyle } = custom; + const { background, dynamicBackground, windowBarStyle } = custom; return { - background: dynamicBackground ? backgroundImage : mainBackground, backgroundColor: dynamicBackground ? background : mainBackground, - backgroundPosition: 'center', - backgroundRepeat: 'no-repeat', - backgroundSize: 'cover', height: windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS ? 'calc(100vh - 120px)' @@ -403,10 +581,6 @@ const containerVariants: Variants = { position: 'absolute', top: 0, transition: { - background: { - duration: 0.5, - ease: 'easeInOut', - }, delay: 0.1, duration: 0.5, ease: 'easeInOut', @@ -417,6 +591,55 @@ const containerVariants: Variants = { }, }; +interface PlayerContainerProps { + children: ReactNode; + dynamicBackground: boolean | undefined; + dynamicIsImage: boolean | undefined; + onMouseEnter: () => void; + onMouseLeave: () => void; + windowBarStyle: Platform; +} + +const PlayerContainer = memo( + ({ + children, + dynamicBackground, + dynamicIsImage, + onMouseEnter, + onMouseLeave, + windowBarStyle, + }: PlayerContainerProps) => { + const currentSong = usePlayerSong(); + const { background } = useFastAverageColor({ + algorithm: 'dominant', + src: currentSong?.imageUrl, + srcLoaded: true, + }); + + return ( + + + {children} + + ); + }, +); + +PlayerContainer.displayName = 'PlayerContainer'; + export const FullScreenPlayer = () => { const { dynamicBackground, dynamicImageBlur, dynamicIsImage } = useFullScreenPlayerStore(); const { setStore } = useFullScreenPlayerStoreActions(); @@ -435,46 +658,23 @@ export const FullScreenPlayer = () => { isOpenedRef.current = true; }, [location, setStore]); - const currentSong = usePlayerSong(); - const { background } = useFastAverageColor({ - algorithm: 'dominant', - src: currentSong?.imageUrl, - srcLoaded: true, - }); - - const imageUrl = currentSong?.imageUrl && currentSong.imageUrl.replace(/size=\d+/g, 'size=500'); - const backgroundImage = - imageUrl && dynamicIsImage - ? `url("${imageUrl.replace(currentSong.id, currentSong.albumId)}"), url("${imageUrl}")` - : mainBackground; - return ( - setIsPageHovered(true)} onMouseLeave={() => setIsPageHovered(false)} - transition={{ duration: 2 }} - variants={containerVariants} + windowBarStyle={windowBarStyle} > - {dynamicBackground && ( -
- )} +
- + ); }; diff --git a/src/renderer/features/player/components/mobile-fullscreen-player-album-art.tsx b/src/renderer/features/player/components/mobile-fullscreen-player-album-art.tsx index 64cd60ca4..952007d51 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player-album-art.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player-album-art.tsx @@ -1,13 +1,37 @@ import clsx from 'clsx'; -import { motion } from 'motion/react'; -import { memo } from 'react'; +import { AnimatePresence, HTMLMotionProps, motion, Variants } from 'motion/react'; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import styles from './mobile-fullscreen-player.module.css'; -import { useFullScreenPlayerStore, useGeneralSettings } from '/@/renderer/store'; -import { Image } from '/@/shared/components/image/image'; +import { useFullScreenPlayerStore, useGeneralSettings, usePlayerData, usePlayerSong } from '/@/renderer/store'; +import { Center } from '/@/shared/components/center/center'; +import { Icon } from '/@/shared/components/icon/icon'; import { PlaybackSelectors } from '/@/shared/constants/playback-selectors'; -import { QueueSong } from '/@/shared/types/domain-types'; +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 @@ -16,44 +40,161 @@ const scaleImageUrl = (imageSize: number, url?: null | string) => { .replace(/&height=\d+/, `&height=${imageSize}`); }; +const MotionImage = motion.img; + +const ImageWithPlaceholder = ({ + className, + useImageAspectRatio, + ...props +}: HTMLMotionProps<'img'> & { placeholder?: string; useImageAspectRatio?: boolean }) => { + if (!props.src) { + return ( +
+ +
+ ); + } + + return ( + + ); +}; + interface MobileFullscreenPlayerAlbumArtProps { currentSong?: QueueSong; } -export const MobileFullscreenPlayerAlbumArt = memo( - ({ currentSong }: MobileFullscreenPlayerAlbumArtProps) => { - const { albumArtRes } = useGeneralSettings(); - const { useImageAspectRatio } = useFullScreenPlayerStore(); - const imageSize = albumArtRes || 1000; - const imageUrl = scaleImageUrl(imageSize, currentSong?.imageUrl); +export const MobileFullscreenPlayerAlbumArt = ({ currentSong: _currentSong }: MobileFullscreenPlayerAlbumArtProps) => { + const mainImageRef = useRef(null); + const [mainImageDimensions, setMainImageDimensions] = useState({ idealSize: 1000 }); - if (!imageUrl) { - return null; + const { albumArtRes } = useGeneralSettings(); + const { useImageAspectRatio } = useFullScreenPlayerStore(); + const currentSong = usePlayerSong(); + const { nextSong } = usePlayerData(); + + 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 idealSize = + albumArtRes || + Math.ceil((mainImageRef.current as HTMLDivElement).offsetHeight / 100) * 100; + + setMainImageDimensions({ idealSize }); + + setImageState({ + bottomImage: scaleImageUrl(idealSize, nextSong?.imageUrl), + current: 0, + topImage: scaleImageUrl(idealSize, currentSong?.imageUrl), + }); + } + }, [ + albumArtRes, + currentSong?.imageUrl, + nextSong?.imageUrl, + setImageState, + ]); + + useLayoutEffect(() => { + updateImageSize(); + }, [updateImageSize]); + + // Track previous song to detect changes + 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; } - return ( -
- - - -
- ); - }, -); + const isTop = imageStateRef.current.current === 0; + const currentImageUrl = scaleImageUrl(mainImageDimensions.idealSize, currentSong?.imageUrl); + const nextImageUrl = scaleImageUrl(mainImageDimensions.idealSize, nextSong?.imageUrl); -MobileFullscreenPlayerAlbumArt.displayName = 'MobileFullscreenPlayerAlbumArt'; + setImageState({ + bottomImage: isTop ? currentImageUrl : nextImageUrl, + current: isTop ? 1 : 0, + topImage: isTop ? nextImageUrl : currentImageUrl, + }); + + previousSongRef.current = currentSong?._uniqueId; + }, [ + currentSong?._uniqueId, + currentSong?.imageUrl, + nextSong?.imageUrl, + mainImageDimensions.idealSize, + setImageState, + ]); + + return ( +
+
+ + {imageState.current === 0 && ( + + )} + + {imageState.current === 1 && ( + + )} + +
+
+ ); +}; diff --git a/src/renderer/features/player/components/mobile-fullscreen-player.module.css b/src/renderer/features/player/components/mobile-fullscreen-player.module.css index 2fdde9dfd..ca53617db 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player.module.css +++ b/src/renderer/features/player/components/mobile-fullscreen-player.module.css @@ -6,11 +6,23 @@ background: var(--theme-colors-background); } +.background-image { + position: absolute; + top: 0; + left: 0; + z-index: -2; + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-position: center; + background-size: cover; +} + .background-image-overlay { position: absolute; top: 0; left: 0; - z-index: 0; + z-index: -1; width: 100%; height: 100%; background: var(--theme-overlay-header); @@ -87,6 +99,9 @@ } .album-image { + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; object-fit: contain; diff --git a/src/renderer/features/player/components/mobile-fullscreen-player.tsx b/src/renderer/features/player/components/mobile-fullscreen-player.tsx index b675ca82e..8335b87f3 100644 --- a/src/renderer/features/player/components/mobile-fullscreen-player.tsx +++ b/src/renderer/features/player/components/mobile-fullscreen-player.tsx @@ -1,5 +1,15 @@ -import { motion } from 'motion/react'; -import { CSSProperties, MouseEvent, useCallback, useState } from 'react'; +import { AnimatePresence, motion } from 'motion/react'; +import { Variants } from 'motion/react'; +import { + CSSProperties, + memo, + MouseEvent, + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; import styles from './mobile-fullscreen-player.module.css'; @@ -32,6 +42,302 @@ import { ItemListKey } 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 [imageState, setImageState] = useState({ + bottomImage: nextSong?.imageUrl + ? nextSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined, + current: 0, + topImage: currentSong?.imageUrl + ? currentSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined, + }); + + const previousSongRef = useRef(currentSong?._uniqueId); + const imageStateRef = useRef(imageState); + + useEffect(() => { + imageStateRef.current = imageState; + }, [imageState]); + + // Update images when song changes + useEffect(() => { + if (currentSong?._uniqueId === previousSongRef.current) { + return; + } + + const isTop = imageStateRef.current.current === 0; + const currentImageUrl = currentSong?.imageUrl + ? currentSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined; + const nextImageUrl = nextSong?.imageUrl + ? nextSong.imageUrl.replace(/size=\d+/g, 'size=500') + : undefined; + + setImageState({ + bottomImage: isTop ? currentImageUrl : nextImageUrl, + current: isTop ? 1 : 0, + topImage: isTop ? nextImageUrl : currentImageUrl, + }); + + previousSongRef.current = currentSong?._uniqueId; + }, [currentSong?._uniqueId, currentSong?.imageUrl, nextSong?._uniqueId, nextSong?.imageUrl]); + + 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'; + +const overlayVariants: Variants = { + closed: { + opacity: 0, + transition: { + duration: 0, + }, + }, + initial: { + opacity: 1, + }, + open: { + opacity: 1, + transition: { + duration: 0, + }, + }, +}; + +interface BackgroundImageOverlayProps { + dynamicBackground: boolean | undefined; + dynamicImageBlur: number | undefined; +} + +const BackgroundImageOverlay = memo( + ({ dynamicBackground, dynamicImageBlur }: BackgroundImageOverlayProps) => { + const currentSong = usePlayerSong(); + const { nextSong } = usePlayerData(); + + const [overlayState, setOverlayState] = useState({ + bottomSongId: nextSong?._uniqueId, + current: 0, + topSongId: currentSong?._uniqueId, + }); + + const previousSongRef = useRef(currentSong?._uniqueId); + const overlayStateRef = useRef(overlayState); + + useEffect(() => { + overlayStateRef.current = overlayState; + }, [overlayState]); + + // Update overlays when song changes + useEffect(() => { + if (currentSong?._uniqueId === previousSongRef.current) { + return; + } + + const isTop = overlayStateRef.current.current === 0; + + setOverlayState({ + bottomSongId: isTop ? currentSong?._uniqueId : nextSong?._uniqueId, + current: isTop ? 1 : 0, + topSongId: isTop ? nextSong?._uniqueId : currentSong?._uniqueId, + }); + + previousSongRef.current = currentSong?._uniqueId; + }, [currentSong?._uniqueId, nextSong?._uniqueId]); + + if (!dynamicBackground) { + return null; + } + + return ( + + {overlayState.current === 0 && ( + + )} + + {overlayState.current === 1 && ( + + )} + + ); + }, +); + +BackgroundImageOverlay.displayName = 'BackgroundImageOverlay'; + +interface MobilePlayerContainerProps { + children: ReactNode; + dynamicBackground: boolean | undefined; + dynamicIsImage: boolean | undefined; +} + +const MobilePlayerContainer = memo( + ({ children, dynamicBackground, dynamicIsImage }: MobilePlayerContainerProps) => { + const currentSong = usePlayerSong(); + const { background } = useFastAverageColor({ + algorithm: 'dominant', + src: currentSong?.imageUrl, + srcLoaded: true, + }); + + let backgroundColor = mainBackground; + if (dynamicBackground) { + if (dynamicIsImage && background) { + const rgbMatch = background.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (rgbMatch) { + backgroundColor = `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, 0.3)`; + } else { + backgroundColor = background; + } + } else { + backgroundColor = background || mainBackground; + } + } + + return ( +
+ + {children} +
+ ); + }, +); + +MobilePlayerContainer.displayName = 'MobilePlayerContainer'; + export const MobileFullscreenPlayer = () => { const { t } = useTranslation(); const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); @@ -47,18 +353,6 @@ export const MobileFullscreenPlayer = () => { const [isPageHovered, setIsPageHovered] = useState(false); - const { background } = useFastAverageColor({ - algorithm: 'dominant', - src: currentSong?.imageUrl, - srcLoaded: true, - }); - - const imageUrl = currentSong?.imageUrl && currentSong.imageUrl.replace(/size=\d+/g, 'size=500'); - const backgroundImage = - imageUrl && dynamicIsImage - ? `url("${imageUrl.replace(currentSong.id, currentSong.albumId)}"), url("${imageUrl}")` - : mainBackground; - const handleToggleFullScreenPlayer = useCallback(() => { setFullScreenPlayerStore({ expanded: false }); }, [setFullScreenPlayerStore]); @@ -140,26 +434,14 @@ export const MobileFullscreenPlayer = () => { (server?.type === ServerType.NAVIDROME || server?.type === ServerType.SUBSONIC); return ( -
- {dynamicBackground && ( -
- )} + {
-
+ ); };