match drag behavior on waveform playerbar to original

This commit is contained in:
jeffvli
2025-11-18 22:07:27 -08:00
parent 6c785c7ea2
commit dde2e4e780
2 changed files with 209 additions and 11 deletions
@@ -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%);
}
@@ -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<HTMLDivElement>(null);
const { mediaSeekToTimestamp } = usePlayer();
const [isLoading, setIsLoading] = useState(true);
const [isDragging, setIsDragging] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState<null | { x: number; y: number }>(null);
const [tooltipValue, setTooltipValue] = useState(0);
const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastSeekValueRef = useRef<null | number>(null);
const containerPositionRef = useRef<DOMRect | null>(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 = () => {
</motion.div>
)}
</AnimatePresence>
{tooltipPosition && isDragging && (
<motion.div
animate={{ opacity: 1, scale: 1, x: '-50%' }}
className={styles.tooltip}
initial={{ opacity: 0, scale: 0.8, x: '-50%' }}
style={{
left: `${tooltipPosition.x}px`,
position: 'fixed',
top: `${tooltipPosition.y - 40}px`,
zIndex: 1000,
}}
transition={{ duration: 0.15 }}
>
<Text isNoSelect size="md">
{formatDuration(tooltipValue * 1000)}
</Text>
</motion.div>
)}
</div>
);
};