mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-17 06:00:20 +02:00
match drag behavior on waveform playerbar to original
This commit is contained in:
@@ -8,3 +8,18 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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 { useWavesurfer } from '@wavesurfer/react';
|
||||||
|
import formatDuration from 'format-duration';
|
||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { useColorScheme } from '/@/renderer/themes/use-app-theme';
|
import { useColorScheme } from '/@/renderer/themes/use-app-theme';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
export const PlayerbarWaveform = () => {
|
export const PlayerbarWaveform = () => {
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
@@ -26,6 +28,12 @@ export const PlayerbarWaveform = () => {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const { mediaSeekToTimestamp } = usePlayer();
|
const { mediaSeekToTimestamp } = usePlayer();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
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;
|
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
|
||||||
|
|
||||||
@@ -73,7 +81,7 @@ export const PlayerbarWaveform = () => {
|
|||||||
cursorWidth: 2,
|
cursorWidth: 2,
|
||||||
fillParent: true,
|
fillParent: true,
|
||||||
height: 18,
|
height: 18,
|
||||||
interact: true,
|
interact: false,
|
||||||
normalize: false,
|
normalize: false,
|
||||||
progressColor: primaryColor,
|
progressColor: primaryColor,
|
||||||
url: streamUrl || undefined,
|
url: streamUrl || undefined,
|
||||||
@@ -120,36 +128,193 @@ export const PlayerbarWaveform = () => {
|
|||||||
};
|
};
|
||||||
}, [wavesurfer]);
|
}, [wavesurfer]);
|
||||||
|
|
||||||
// Handle seeking when user clicks on waveform
|
// Handle drag start on waveform
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!wavesurfer || !songDuration) return;
|
if (!wavesurfer || !songDuration || !containerRef.current) return;
|
||||||
|
|
||||||
const handleInteraction = () => {
|
const container = containerRef.current;
|
||||||
const seekTime = wavesurfer.getCurrentTime();
|
let isDraggingLocal = false;
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
if (!wavesurfer) return;
|
||||||
const duration = wavesurfer.getDuration();
|
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);
|
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 () => {
|
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]);
|
}, [wavesurfer, songDuration, mediaSeekToTimestamp]);
|
||||||
|
|
||||||
// Update waveform progress based on player current time
|
// Sync dragging state when currentTime catches up to seek value
|
||||||
useEffect(() => {
|
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();
|
const duration = wavesurfer.getDuration();
|
||||||
if (duration > 0 && currentTime >= 0) {
|
if (duration > 0 && currentTime >= 0) {
|
||||||
const ratio = currentTime / duration;
|
const ratio = currentTime / duration;
|
||||||
wavesurfer.seekTo(ratio);
|
wavesurfer.seekTo(ratio);
|
||||||
}
|
}
|
||||||
}, [wavesurfer, currentTime, songDuration]);
|
}, [wavesurfer, currentTime, songDuration, isDragging]);
|
||||||
|
|
||||||
// Show disabled slider when there's no current song
|
// Show disabled slider when there's no current song
|
||||||
if (!currentSong) {
|
if (!currentSong) {
|
||||||
@@ -202,6 +367,24 @@ export const PlayerbarWaveform = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user