mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
match drag behavior on waveform playerbar to original
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user