mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-28 08:45:41 +00:00
add: When user drags the playhead, it will scrub through, which is an intended from multiple video editor.
scrubbing stops the playhead. as per usual editors
This commit is contained in:
@@ -3275,8 +3275,14 @@ export default function VideoEditor() {
|
||||
}, []);
|
||||
|
||||
function handleSeek(time: number) {
|
||||
const video = videoPlaybackRef.current?.video;
|
||||
const playback = videoPlaybackRef.current;
|
||||
const video = playback?.video;
|
||||
if (!video) return;
|
||||
|
||||
if (!video.paused) {
|
||||
playback?.pause();
|
||||
}
|
||||
|
||||
video.currentTime = mapTimelineTimeToSourceTime(time * 1000) / 1000;
|
||||
}
|
||||
|
||||
|
||||
@@ -323,11 +323,17 @@ function PlaybackCursor({
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!timelineRef.current || !onSeek) return;
|
||||
let frameId: number | null = null;
|
||||
let lastMouseEvent: MouseEvent | null = null;
|
||||
|
||||
const updateSeek = () => {
|
||||
if (!lastMouseEvent || !timelineRef.current || !onSeek) {
|
||||
frameId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = timelineRef.current.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left - sidebarWidth;
|
||||
const clickX = lastMouseEvent.clientX - rect.left - sidebarWidth;
|
||||
|
||||
// Allow dragging outside to 0 or max, but clamp the value
|
||||
const relativeMs = pixelsToValue(clickX);
|
||||
@@ -347,6 +353,14 @@ function PlaybackCursor({
|
||||
}
|
||||
|
||||
onSeek(absoluteMs / 1000);
|
||||
frameId = null;
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
lastMouseEvent = e;
|
||||
if (frameId === null) {
|
||||
frameId = requestAnimationFrame(updateSeek);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
@@ -359,6 +373,9 @@ function PlaybackCursor({
|
||||
document.body.style.cursor = "ew-resize";
|
||||
|
||||
return () => {
|
||||
if (frameId !== null) {
|
||||
cancelAnimationFrame(frameId);
|
||||
}
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
@@ -427,9 +444,11 @@ function PlaybackCursor({
|
||||
function TimelineAxis({
|
||||
videoDurationMs,
|
||||
currentTimeMs,
|
||||
onMouseDown,
|
||||
}: {
|
||||
videoDurationMs: number;
|
||||
currentTimeMs: number;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
const { sidebarWidth, direction, range, valueToPixels } = useTimelineContext();
|
||||
const sideProperty = direction === "rtl" ? "right" : "left";
|
||||
@@ -494,10 +513,11 @@ function TimelineAxis({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-8 bg-editor-bg border-b border-foreground/10 relative overflow-hidden select-none"
|
||||
className="h-8 bg-editor-bg border-b border-foreground/10 relative overflow-hidden select-none cursor-ew-resize"
|
||||
style={{
|
||||
[sideProperty === "right" ? "marginRight" : "marginLeft"]: `${sidebarWidth}px`,
|
||||
}}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
{/* Minor Ticks */}
|
||||
{markers.minorTicks.map((time) => {
|
||||
@@ -645,6 +665,7 @@ function Timeline({
|
||||
const localTimelineRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isTimelineHovered, setIsTimelineHovered] = useState(false);
|
||||
const [timelineHoverMs, setTimelineHoverMs] = useState<number | null>(null);
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
const [isZoomRowHovered, setIsZoomRowHovered] = useState(false);
|
||||
const [zoomRowHoverMs, setZoomRowHoverMs] = useState<number | null>(null);
|
||||
|
||||
@@ -656,12 +677,22 @@ function Timeline({
|
||||
[setTimelineRef],
|
||||
);
|
||||
|
||||
const handleTimelineClick = useCallback(
|
||||
const handleTimelineMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!onSeek || videoDurationMs <= 0) return;
|
||||
if (e.button !== 0 || !onSeek || videoDurationMs <= 0 || !localTimelineRef.current) return;
|
||||
|
||||
// Only clear selection if clicking on empty space (not on items)
|
||||
// This is handled by event propagation - items stop propagation
|
||||
const rect = localTimelineRef.current.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left - sidebarWidth;
|
||||
|
||||
if (clickX < 0) return;
|
||||
|
||||
setIsSeeking(true);
|
||||
|
||||
const relativeMs = pixelsToValue(clickX);
|
||||
const absoluteMs = Math.max(0, Math.min(range.start + relativeMs, videoDurationMs));
|
||||
onSeek(absoluteMs / 1000);
|
||||
|
||||
// Clear selection when clicking on empty space
|
||||
onSelectZoom?.(null);
|
||||
onSelectTrim?.(null);
|
||||
onSelectClip?.(null);
|
||||
@@ -669,20 +700,13 @@ function Timeline({
|
||||
onSelectSpeed?.(null);
|
||||
onSelectAudio?.(null);
|
||||
onClearBlockSelection?.();
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left - sidebarWidth;
|
||||
|
||||
if (clickX < 0) return;
|
||||
|
||||
const relativeMs = pixelsToValue(clickX);
|
||||
const absoluteMs = Math.max(0, Math.min(range.start + relativeMs, videoDurationMs));
|
||||
const timeInSeconds = absoluteMs / 1000;
|
||||
|
||||
onSeek(timeInSeconds);
|
||||
},
|
||||
[
|
||||
onSeek,
|
||||
videoDurationMs,
|
||||
sidebarWidth,
|
||||
range.start,
|
||||
pixelsToValue,
|
||||
onSelectZoom,
|
||||
onSelectTrim,
|
||||
onSelectClip,
|
||||
@@ -690,13 +714,54 @@ function Timeline({
|
||||
onSelectSpeed,
|
||||
onSelectAudio,
|
||||
onClearBlockSelection,
|
||||
videoDurationMs,
|
||||
sidebarWidth,
|
||||
range.start,
|
||||
pixelsToValue,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSeeking) return;
|
||||
|
||||
let frameId: number | null = null;
|
||||
let lastMouseEvent: MouseEvent | null = null;
|
||||
|
||||
const updateSeek = () => {
|
||||
if (!lastMouseEvent || !localTimelineRef.current || !onSeek || videoDurationMs <= 0) {
|
||||
frameId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = localTimelineRef.current.getBoundingClientRect();
|
||||
const clickX = lastMouseEvent.clientX - rect.left - sidebarWidth;
|
||||
|
||||
const relativeMs = pixelsToValue(clickX);
|
||||
const absoluteMs = Math.max(0, Math.min(range.start + relativeMs, videoDurationMs));
|
||||
|
||||
onSeek(absoluteMs / 1000);
|
||||
frameId = null;
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
lastMouseEvent = e;
|
||||
if (frameId === null) {
|
||||
frameId = requestAnimationFrame(updateSeek);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsSeeking(false);
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
if (frameId !== null) {
|
||||
cancelAnimationFrame(frameId);
|
||||
}
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [isSeeking, onSeek, videoDurationMs, sidebarWidth, pixelsToValue, range.start]);
|
||||
|
||||
const zoomItems = items.filter((item) => item.rowId === ZOOM_ROW_ID);
|
||||
const clipItems = items.filter((item) => item.rowId === CLIP_ROW_ID);
|
||||
const annotationItems = items.filter((item) => isAnnotationTrackRowId(item.rowId));
|
||||
@@ -852,14 +917,17 @@ function Timeline({
|
||||
...style,
|
||||
height: `max(100%, ${timelineContentMinHeightPx}px, calc(${TIMELINE_AXIS_HEIGHT_PX}px + (100% - ${TIMELINE_AXIS_HEIGHT_PX}px) * ${timelineViewportStretchFactor}))`,
|
||||
}}
|
||||
className="select-none bg-editor-bg relative cursor-pointer group flex flex-col"
|
||||
onClick={handleTimelineClick}
|
||||
className="select-none bg-editor-bg relative group flex flex-col"
|
||||
onMouseEnter={handleTimelineMouseEnter}
|
||||
onMouseMove={handleTimelineMouseMove}
|
||||
onMouseLeave={handleTimelineMouseLeave}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,hsl(var(--foreground)/0.03)_1px,transparent_1px)] bg-[length:20px_100%] pointer-events-none" />
|
||||
<TimelineAxis videoDurationMs={videoDurationMs} currentTimeMs={currentTimeMs} />
|
||||
<TimelineAxis
|
||||
videoDurationMs={videoDurationMs}
|
||||
currentTimeMs={currentTimeMs}
|
||||
onMouseDown={handleTimelineMouseDown}
|
||||
/>
|
||||
<PlaybackCursor
|
||||
currentTimeMs={currentTimeMs}
|
||||
videoDurationMs={videoDurationMs}
|
||||
|
||||
Reference in New Issue
Block a user