From 6c785c7ea2ba3271f3b9e2f0bc1fa27d2b851c48 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 18 Nov 2025 21:52:22 -0800 Subject: [PATCH] optimize playerbar slider component --- .../components/playerbar-seek-slider.tsx | 99 +++++++++++++++++++ .../player/components/playerbar-slider.tsx | 51 +--------- 2 files changed, 101 insertions(+), 49 deletions(-) create mode 100644 src/renderer/features/player/components/playerbar-seek-slider.tsx diff --git a/src/renderer/features/player/components/playerbar-seek-slider.tsx b/src/renderer/features/player/components/playerbar-seek-slider.tsx new file mode 100644 index 000000000..a953a0444 --- /dev/null +++ b/src/renderer/features/player/components/playerbar-seek-slider.tsx @@ -0,0 +1,99 @@ +import formatDuration from 'format-duration'; +import { useEffect, useRef, useState } from 'react'; + +import styles from './playerbar-slider.module.css'; + +import { usePlayer } from '/@/renderer/features/player/context/player-context'; +import { usePlayerTimestamp } from '/@/renderer/store'; +import { CustomPlayerbarSlider } from './playerbar-slider'; + +interface PlayerbarSeekSliderProps { + max: number; + min: number; +} + +export const PlayerbarSeekSlider = ({ max, min }: PlayerbarSeekSliderProps) => { + const [isSeeking, setIsSeeking] = useState(false); + const [seekValue, setSeekValue] = useState(0); + const currentTime = usePlayerTimestamp(); + const seekTimeoutRef = useRef(null); + const lastSeekValueRef = useRef(null); + + const { mediaSeekToTimestamp } = usePlayer(); + + const handleSeekToTimestamp = (timestamp: number) => { + mediaSeekToTimestamp(timestamp); + }; + + // Sync isSeeking state when currentTime catches up to seek value + useEffect(() => { + if (isSeeking && lastSeekValueRef.current !== null) { + const timeDiff = Math.abs(currentTime - lastSeekValueRef.current); + if (timeDiff < 0.5) { + setIsSeeking(false); + lastSeekValueRef.current = null; + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + seekTimeoutRef.current = null; + } + } + } + }, [currentTime, isSeeking]); + + useEffect(() => { + return () => { + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + } + }; + }, []); + + return ( + formatDuration(value * 1000)} + max={max} + min={min} + onChange={(e) => { + // Cancel any pending timeout if user starts seeking again + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + seekTimeoutRef.current = null; + } + setIsSeeking(true); + setSeekValue(e); + }} + onChangeEnd={(e) => { + setSeekValue(e); + lastSeekValueRef.current = e; + handleSeekToTimestamp(e); + + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + } + + // Keep isSeeking true to prevent slider from snapping back. + // The useEffect will detect when currentTime catches up and clear isSeeking. + // Also set a fallback timeout to clear isSeeking after a max delay + // in case the seek doesn't complete (e.g., network issues). + seekTimeoutRef.current = setTimeout(() => { + setIsSeeking(false); + lastSeekValueRef.current = null; + seekTimeoutRef.current = null; + }, 1000); + }} + onClick={(e) => { + e?.stopPropagation(); + }} + size={6} + value={ + isSeeking + ? seekValue + : lastSeekValueRef.current !== null && + Math.abs(currentTime - lastSeekValueRef.current) > 0.5 + ? lastSeekValueRef.current + : currentTime + } + w="100%" + /> + ); +}; diff --git a/src/renderer/features/player/components/playerbar-slider.tsx b/src/renderer/features/player/components/playerbar-slider.tsx index 564946ed0..0d1849b22 100644 --- a/src/renderer/features/player/components/playerbar-slider.tsx +++ b/src/renderer/features/player/components/playerbar-slider.tsx @@ -1,12 +1,11 @@ import formatDuration from 'format-duration'; -import { useEffect, useRef, useState } from 'react'; +import { PlayerbarSeekSlider } from './playerbar-seek-slider'; import styles from './playerbar-slider.module.css'; import { PlayerbarWaveform } from './playerbar-waveform'; import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player'; import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player'; -import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { useRemote } from '/@/renderer/features/remote/hooks/use-remote'; import { useAppStore, @@ -27,10 +26,7 @@ export const PlayerbarSlider = () => { const playerbarSlider = usePlayerbarSlider(); const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0; - const [isSeeking, setIsSeeking] = useState(false); - const [seekValue, setSeekValue] = useState(0); const currentTime = usePlayerTimestamp(); - const seekTimeoutRef = useRef(null); const formattedDuration = formatDuration(songDuration * 1000 || 0); const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0); @@ -39,20 +35,6 @@ export const PlayerbarSlider = () => { const { showTimeRemaining } = useAppStore(); const { setShowTimeRemaining } = useAppStoreActions(); - const { mediaSeekToTimestamp } = usePlayer(); - - const handleSeekToTimestamp = (timestamp: number) => { - mediaSeekToTimestamp(timestamp); - }; - - useEffect(() => { - return () => { - if (seekTimeoutRef.current) { - clearTimeout(seekTimeoutRef.current); - } - }; - }, []); - useRemote(); const isWaveform = playerbarSlider?.type === PlayerbarSliderType.WAVEFORM; @@ -76,36 +58,7 @@ export const PlayerbarSlider = () => { {isWaveform ? ( ) : ( - formatDuration(value * 1000)} - max={songDuration} - min={0} - onChange={(e) => { - // Cancel any pending timeout if user starts seeking again - if (seekTimeoutRef.current) { - clearTimeout(seekTimeoutRef.current); - seekTimeoutRef.current = null; - } - setIsSeeking(true); - setSeekValue(e); - }} - onChangeEnd={(e) => { - setSeekValue(e); - handleSeekToTimestamp(e); - - // Delay resetting isSeeking to allow currentTime to catch up - seekTimeoutRef.current = setTimeout(() => { - setIsSeeking(false); - seekTimeoutRef.current = null; - }, 300); - }} - onClick={(e) => { - e?.stopPropagation(); - }} - size={6} - value={!isSeeking ? currentTime : seekValue} - w="100%" - /> + )}