diff --git a/src/renderer/features/player/components/playerbar-waveform.tsx b/src/renderer/features/player/components/playerbar-waveform.tsx index 56eb4d507..dfeef02c6 100644 --- a/src/renderer/features/player/components/playerbar-waveform.tsx +++ b/src/renderer/features/player/components/playerbar-waveform.tsx @@ -1,8 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; import { useWavesurfer } from '@wavesurfer/react'; import formatDuration from 'format-duration'; import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { PlayerbarSeekSlider } from './playerbar-seek-slider'; import { CustomPlayerbarSlider } from './playerbar-slider'; import styles from './playerbar-waveform.module.css'; @@ -29,6 +31,7 @@ export const PlayerbarWaveform = () => { const { mediaSeekToTimestamp } = usePlayer(); const [isLoading, setIsLoading] = useState(true); const [isDragging, setIsDragging] = useState(false); + const [isHovered, setIsHovered] = useState(false); const [tooltipPosition, setTooltipPosition] = useState(null); const [tooltipValue, setTooltipValue] = useState(0); const seekTimeoutRef = useRef(null); @@ -39,6 +42,21 @@ export const PlayerbarWaveform = () => { const streamUrl = useSongUrl(currentSong, true, transcode); + // Fetch blob from stream URL + const { data: streamBlob } = useQuery({ + enabled: !!streamUrl && !!currentSong, + queryFn: async () => { + if (!streamUrl) return undefined; + + const response = await fetch(streamUrl); + if (!response.ok) { + throw new Error('Failed to fetch stream blob'); + } + return await response.blob(); + }, + queryKey: [currentSong?._serverId, streamUrl], + }); + const primaryColor = usePrimaryColor(); const colorScheme = useColorScheme(); @@ -65,22 +83,37 @@ export const PlayerbarWaveform = () => { interact: false, normalize: false, progressColor: primaryColor, - url: streamUrl || undefined, + url: undefined, // URL will be loaded separately via useEffect waveColor, }); - // Reset loading state when stream URL changes and ensure media is muted + // Update wavesurfer with blob when it becomes available useEffect(() => { + if (!wavesurfer || !streamBlob) return; + + wavesurfer.loadBlob(streamBlob); setIsLoading(true); - if (wavesurfer) { - wavesurfer.setVolume(0); - const mediaElement = wavesurfer.getMediaElement(); - if (mediaElement) { - mediaElement.muted = true; - mediaElement.volume = 0; - } + wavesurfer.setVolume(0); + const mediaElement = wavesurfer.getMediaElement(); + if (mediaElement) { + mediaElement.muted = true; + mediaElement.volume = 0; } - }, [streamUrl, wavesurfer]); + }, [streamBlob, wavesurfer]); + + // Reset loading state when song changes + useEffect(() => { + if (!wavesurfer) return; + + setIsLoading(true); + + wavesurfer.setVolume(0); + const mediaElement = wavesurfer.getMediaElement(); + if (mediaElement) { + mediaElement.muted = true; + mediaElement.volume = 0; + } + }, [wavesurfer]); // Handle waveform ready state useEffect(() => { @@ -351,6 +384,8 @@ export const PlayerbarWaveform = () => { onClick={(e) => { e?.stopPropagation(); }} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} style={{ position: 'relative' }} > { transition={{ duration: 0.2 }} /> - {isLoading && ( + {isLoading && !isHovered && ( { )} + {isLoading && isHovered && ( +
+ +
+ )} {tooltipPosition && isDragging && (