optimize playerbar slider component

This commit is contained in:
jeffvli
2025-11-18 21:52:22 -08:00
parent d977407766
commit 6c785c7ea2
2 changed files with 101 additions and 49 deletions
@@ -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<NodeJS.Timeout | null>(null);
const lastSeekValueRef = useRef<null | number>(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 (
<CustomPlayerbarSlider
label={(value) => 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%"
/>
);
};
@@ -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<NodeJS.Timeout | null>(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 ? (
<PlayerbarWaveform />
) : (
<CustomPlayerbarSlider
label={(value) => 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%"
/>
<PlayerbarSeekSlider max={songDuration} min={0} />
)}
</div>
<div className={styles.sliderValueWrapper}>