From dde2e4e780ef62e8df84219f8c79c7e3a3e53531 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 18 Nov 2025 22:07:27 -0800 Subject: [PATCH] match drag behavior on waveform playerbar to original --- .../components/playerbar-waveform.module.css | 15 ++ .../player/components/playerbar-waveform.tsx | 205 +++++++++++++++++- 2 files changed, 209 insertions(+), 11 deletions(-) diff --git a/src/renderer/features/player/components/playerbar-waveform.module.css b/src/renderer/features/player/components/playerbar-waveform.module.css index 5d5a5ff2c..2a40b214e 100644 --- a/src/renderer/features/player/components/playerbar-waveform.module.css +++ b/src/renderer/features/player/components/playerbar-waveform.module.css @@ -8,3 +8,18 @@ width: 100%; height: 100%; } + +.tooltip { + display: flex; + align-items: center; + justify-content: center; + max-width: 200px; + padding: var(--theme-spacing-sm) var(--theme-spacing-md); + font-size: var(--theme-font-size-md); + font-weight: 550; + color: var(--theme-colors-surface-foreground); + white-space: nowrap; + pointer-events: none; + background: var(--theme-colors-surface); + box-shadow: 4px 4px 10px 0 rgb(0 0 0 / 20%); +} diff --git a/src/renderer/features/player/components/playerbar-waveform.tsx b/src/renderer/features/player/components/playerbar-waveform.tsx index e402ade0d..1f2cdd4c7 100644 --- a/src/renderer/features/player/components/playerbar-waveform.tsx +++ b/src/renderer/features/player/components/playerbar-waveform.tsx @@ -1,4 +1,5 @@ import { useWavesurfer } from '@wavesurfer/react'; +import formatDuration from 'format-duration'; import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -17,6 +18,7 @@ import { } from '/@/renderer/store'; import { useColorScheme } from '/@/renderer/themes/use-app-theme'; import { Spinner } from '/@/shared/components/spinner/spinner'; +import { Text } from '/@/shared/components/text/text'; export const PlayerbarWaveform = () => { const currentSong = usePlayerSong(); @@ -26,6 +28,12 @@ export const PlayerbarWaveform = () => { const containerRef = useRef(null); const { mediaSeekToTimestamp } = usePlayer(); const [isLoading, setIsLoading] = useState(true); + const [isDragging, setIsDragging] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState(null); + const [tooltipValue, setTooltipValue] = useState(0); + const seekTimeoutRef = useRef(null); + const lastSeekValueRef = useRef(null); + const containerPositionRef = useRef(null); const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0; @@ -73,7 +81,7 @@ export const PlayerbarWaveform = () => { cursorWidth: 2, fillParent: true, height: 18, - interact: true, + interact: false, normalize: false, progressColor: primaryColor, url: streamUrl || undefined, @@ -120,36 +128,193 @@ export const PlayerbarWaveform = () => { }; }, [wavesurfer]); - // Handle seeking when user clicks on waveform + // Handle drag start on waveform useEffect(() => { - if (!wavesurfer || !songDuration) return; + if (!wavesurfer || !songDuration || !containerRef.current) return; - const handleInteraction = () => { - const seekTime = wavesurfer.getCurrentTime(); + const container = containerRef.current; + let isDraggingLocal = false; + + const handleMouseDown = (e: MouseEvent) => { + if (!wavesurfer) return; const duration = wavesurfer.getDuration(); + if (duration <= 0) return; - if (duration > 0) { + isDraggingLocal = true; + setIsDragging(true); + + // Cancel any pending timeout + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + seekTimeoutRef.current = null; + } + + const rect = container.getBoundingClientRect(); + containerPositionRef.current = rect; + const clickX = e.clientX - rect.left; + const ratio = Math.max(0, Math.min(1, clickX / rect.width)); + const seekTime = ratio * duration; + lastSeekValueRef.current = seekTime; + setTooltipPosition({ x: rect.left + clickX, y: rect.top }); + setTooltipValue(seekTime); + wavesurfer.seekTo(ratio); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isDraggingLocal || !wavesurfer) return; + + const duration = wavesurfer.getDuration(); + if (duration <= 0) return; + + const rect = container.getBoundingClientRect(); + containerPositionRef.current = rect; + const clickX = e.clientX - rect.left; + const ratio = Math.max(0, Math.min(1, clickX / rect.width)); + const seekTime = ratio * duration; + lastSeekValueRef.current = seekTime; + setTooltipPosition({ x: rect.left + clickX, y: rect.top }); + setTooltipValue(seekTime); + wavesurfer.seekTo(ratio); + }; + + const handleMouseUp = () => { + if (!isDraggingLocal || !wavesurfer) return; + + isDraggingLocal = false; + const duration = wavesurfer.getDuration(); + const seekTime = wavesurfer.getCurrentTime(); + + setTooltipPosition(null); + + if (duration > 0 && seekTime >= 0) { mediaSeekToTimestamp(seekTime); + lastSeekValueRef.current = seekTime; + + // Set a fallback timeout to clear dragging state + seekTimeoutRef.current = setTimeout(() => { + setIsDragging(false); + lastSeekValueRef.current = null; + seekTimeoutRef.current = null; + }, 1000); + } else { + setIsDragging(false); } }; - wavesurfer.on('interaction', handleInteraction); + // Handle touch events for mobile + const handleTouchStart = (e: TouchEvent) => { + if (!wavesurfer) return; + const duration = wavesurfer.getDuration(); + if (duration <= 0) return; + + isDraggingLocal = true; + setIsDragging(true); + + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + seekTimeoutRef.current = null; + } + + const touch = e.touches[0]; + const rect = container.getBoundingClientRect(); + containerPositionRef.current = rect; + const clickX = touch.clientX - rect.left; + const ratio = Math.max(0, Math.min(1, clickX / rect.width)); + const seekTime = ratio * duration; + lastSeekValueRef.current = seekTime; + setTooltipPosition({ x: rect.left + clickX, y: rect.top }); + setTooltipValue(seekTime); + wavesurfer.seekTo(ratio); + }; + + const handleTouchMove = (e: TouchEvent) => { + if (!isDraggingLocal || !wavesurfer) return; + e.preventDefault(); + + const duration = wavesurfer.getDuration(); + if (duration <= 0) return; + + const touch = e.touches[0]; + const rect = container.getBoundingClientRect(); + containerPositionRef.current = rect; + const clickX = touch.clientX - rect.left; + const ratio = Math.max(0, Math.min(1, clickX / rect.width)); + const seekTime = ratio * duration; + lastSeekValueRef.current = seekTime; + setTooltipPosition({ x: rect.left + clickX, y: rect.top }); + setTooltipValue(seekTime); + wavesurfer.seekTo(ratio); + }; + + const handleTouchEnd = () => { + if (!isDraggingLocal || !wavesurfer) return; + + isDraggingLocal = false; + const duration = wavesurfer.getDuration(); + const seekTime = wavesurfer.getCurrentTime(); + + setTooltipPosition(null); + + if (duration > 0 && seekTime >= 0) { + mediaSeekToTimestamp(seekTime); + lastSeekValueRef.current = seekTime; + + seekTimeoutRef.current = setTimeout(() => { + setIsDragging(false); + lastSeekValueRef.current = null; + seekTimeoutRef.current = null; + }, 1000); + } else { + setIsDragging(false); + } + }; + + container.addEventListener('mousedown', handleMouseDown); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + container.addEventListener('touchstart', handleTouchStart, { passive: false }); + container.addEventListener('touchmove', handleTouchMove, { passive: false }); + container.addEventListener('touchend', handleTouchEnd); return () => { - wavesurfer.un('interaction', handleInteraction); + container.removeEventListener('mousedown', handleMouseDown); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + container.removeEventListener('touchstart', handleTouchStart); + container.removeEventListener('touchmove', handleTouchMove); + container.removeEventListener('touchend', handleTouchEnd); + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + } }; }, [wavesurfer, songDuration, mediaSeekToTimestamp]); - // Update waveform progress based on player current time + // Sync dragging state when currentTime catches up to seek value useEffect(() => { - if (!wavesurfer || !songDuration) return; + if (isDragging && lastSeekValueRef.current !== null) { + const timeDiff = Math.abs(currentTime - lastSeekValueRef.current); + if (timeDiff < 0.5) { + setIsDragging(false); + setTooltipPosition(null); + lastSeekValueRef.current = null; + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + seekTimeoutRef.current = null; + } + } + } + }, [currentTime, isDragging]); + + // Update waveform progress based on player current time (only when not dragging) + useEffect(() => { + if (!wavesurfer || !songDuration || isDragging) return; const duration = wavesurfer.getDuration(); if (duration > 0 && currentTime >= 0) { const ratio = currentTime / duration; wavesurfer.seekTo(ratio); } - }, [wavesurfer, currentTime, songDuration]); + }, [wavesurfer, currentTime, songDuration, isDragging]); // Show disabled slider when there's no current song if (!currentSong) { @@ -202,6 +367,24 @@ export const PlayerbarWaveform = () => { )} + {tooltipPosition && isDragging && ( + + + {formatDuration(tooltipValue * 1000)} + + + )} ); };