ui video selector
This commit is contained in:
121
ui/timeline/src/Timeline.tsx
Normal file
121
ui/timeline/src/Timeline.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useRef, useCallback, useState, useEffect } from "react";
|
||||
|
||||
interface TimelineProps {
|
||||
duration: number;
|
||||
currentTime: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
onTrimChange: (start: number, end: number) => void;
|
||||
onSeek: (time: number) => void;
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
const ms = Math.floor((seconds % 1) * 10);
|
||||
return `${m}:${s.toString().padStart(2, "0")}.${ms}`;
|
||||
}
|
||||
|
||||
export default function Timeline({
|
||||
duration,
|
||||
currentTime,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
onTrimChange,
|
||||
onSeek,
|
||||
}: TimelineProps) {
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const [dragging, setDragging] = useState<"in" | "out" | null>(null);
|
||||
|
||||
const timeToPercent = (t: number) => (duration > 0 ? (t / duration) * 100 : 0);
|
||||
|
||||
const positionToTime = useCallback(
|
||||
(clientX: number) => {
|
||||
const track = trackRef.current;
|
||||
if (!track || duration <= 0) return 0;
|
||||
const rect = track.getBoundingClientRect();
|
||||
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
||||
return ratio * duration;
|
||||
},
|
||||
[duration],
|
||||
);
|
||||
|
||||
const handleTrackClick = (e: React.MouseEvent) => {
|
||||
if (dragging) return;
|
||||
onSeek(positionToTime(e.clientX));
|
||||
};
|
||||
|
||||
const handleMouseDown = (handle: "in" | "out") => (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setDragging(handle);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dragging) return;
|
||||
|
||||
const minGap = 0.1;
|
||||
|
||||
const handleMove = (e: MouseEvent) => {
|
||||
const time = positionToTime(e.clientX);
|
||||
if (dragging === "in") {
|
||||
onTrimChange(Math.min(time, trimEnd - minGap), trimEnd);
|
||||
} else {
|
||||
onTrimChange(trimStart, Math.max(time, trimStart + minGap));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUp = () => setDragging(null);
|
||||
|
||||
document.addEventListener("mousemove", handleMove);
|
||||
document.addEventListener("mouseup", handleUp);
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMove);
|
||||
document.removeEventListener("mouseup", handleUp);
|
||||
};
|
||||
}, [dragging, trimStart, trimEnd, positionToTime, onTrimChange]);
|
||||
|
||||
const inPct = timeToPercent(trimStart);
|
||||
const outPct = timeToPercent(trimEnd);
|
||||
const playheadPct = timeToPercent(currentTime);
|
||||
const selectionDuration = trimEnd - trimStart;
|
||||
|
||||
return (
|
||||
<div className="timeline">
|
||||
<div className="timeline-times">
|
||||
<span>In: {formatTime(trimStart)}</span>
|
||||
<span>Selection: {formatTime(selectionDuration)}</span>
|
||||
<span>Out: {formatTime(trimEnd)}</span>
|
||||
</div>
|
||||
<div className="timeline-track" ref={trackRef} onClick={handleTrackClick}>
|
||||
{/* Dimmed regions */}
|
||||
<div className="timeline-dim" style={{ left: 0, width: `${inPct}%` }} />
|
||||
<div className="timeline-dim" style={{ left: `${outPct}%`, width: `${100 - outPct}%` }} />
|
||||
|
||||
{/* Selection highlight */}
|
||||
<div
|
||||
className="timeline-selection"
|
||||
style={{ left: `${inPct}%`, width: `${outPct - inPct}%` }}
|
||||
/>
|
||||
|
||||
{/* Playhead */}
|
||||
<div className="timeline-playhead" style={{ left: `${playheadPct}%` }} />
|
||||
|
||||
{/* Handles */}
|
||||
<div
|
||||
className={`timeline-handle timeline-handle-in ${dragging === "in" ? "dragging" : ""}`}
|
||||
style={{ left: `${inPct}%` }}
|
||||
onMouseDown={handleMouseDown("in")}
|
||||
/>
|
||||
<div
|
||||
className={`timeline-handle timeline-handle-out ${dragging === "out" ? "dragging" : ""}`}
|
||||
style={{ left: `${outPct}%` }}
|
||||
onMouseDown={handleMouseDown("out")}
|
||||
/>
|
||||
</div>
|
||||
<div className="timeline-duration">
|
||||
<span>0:00</span>
|
||||
<span>{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user