93 lines
2.7 KiB
JavaScript
93 lines
2.7 KiB
JavaScript
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
|
|
import Slider from "../Slider";
|
|
|
|
const clamp01 = (value) => Math.min(1, Math.max(0, value || 0));
|
|
|
|
const formatTime = (ms) => {
|
|
const totalSeconds = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
|
|
const minutes = Math.floor(totalSeconds / 60)
|
|
.toString()
|
|
.padStart(1, "0");
|
|
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
|
|
return `${minutes}:${seconds}`;
|
|
};
|
|
|
|
const ProgressSlider = ({
|
|
positionMs = 0,
|
|
durationMs = 0,
|
|
isPlaying = false,
|
|
onSeek, // (targetMs) => void | Promise<void>
|
|
onPause, // () => void | Promise<void>
|
|
onPlay, // () => void | Promise<void>
|
|
onSeekStart, // () => void | Promise<void>
|
|
onSeekEnd, // () => void | Promise<void>
|
|
disabled = false,
|
|
}) => {
|
|
const wasPlayingRef = useRef(false);
|
|
const [isSeeking, setIsSeeking] = useState(false);
|
|
const [previewRatio, setPreviewRatio] = useState(null);
|
|
|
|
const { safePosition, safeDuration, progress } = useMemo(() => {
|
|
const dur = Math.max(0, Number(durationMs) || 0);
|
|
const pos = Math.max(0, Math.min(dur, Number(positionMs) || 0));
|
|
const ratio = dur > 0 ? clamp01(pos / dur) : 0;
|
|
return { safePosition: pos, safeDuration: dur, progress: ratio };
|
|
}, [durationMs, positionMs]);
|
|
|
|
const handleSeekStart = useCallback(() => {
|
|
if (disabled || !safeDuration) return;
|
|
wasPlayingRef.current = !!isPlaying;
|
|
setIsSeeking(true);
|
|
setPreviewRatio(progress);
|
|
if (typeof onSeekStart === "function") {
|
|
onSeekStart();
|
|
}
|
|
if (isPlaying && typeof onPause === "function") {
|
|
onPause();
|
|
}
|
|
}, [disabled, safeDuration, isPlaying, onPause, onSeekStart]);
|
|
|
|
const handleSeek = useCallback(
|
|
(ratio) => {
|
|
if (disabled || !safeDuration || typeof onSeek !== "function") return;
|
|
const bounded = clamp01(ratio);
|
|
setPreviewRatio(bounded);
|
|
const targetMs = safeDuration * bounded;
|
|
onSeek(targetMs);
|
|
},
|
|
[disabled, onSeek, safeDuration],
|
|
);
|
|
|
|
const handleSeekEnd = useCallback(() => {
|
|
if (disabled || !safeDuration) return;
|
|
setIsSeeking(false);
|
|
setPreviewRatio(null);
|
|
if (typeof onSeekEnd === "function") {
|
|
onSeekEnd();
|
|
}
|
|
if (wasPlayingRef.current && typeof onPlay === "function") {
|
|
onPlay();
|
|
}
|
|
wasPlayingRef.current = false;
|
|
}, [disabled, onPlay, onSeekEnd, safeDuration]);
|
|
|
|
return (
|
|
<Slider
|
|
value={formatTime(
|
|
isSeeking && previewRatio != null
|
|
? safeDuration * previewRatio
|
|
: safePosition,
|
|
)}
|
|
maxValue={formatTime(safeDuration)}
|
|
progress={progress}
|
|
seekEnabled={!disabled && safeDuration > 0}
|
|
onSeekStart={handleSeekStart}
|
|
onSeek={handleSeek}
|
|
onSeekEnd={handleSeekEnd}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default ProgressSlider;
|