diff options
| author | Pulkit Garg <pulkitgargbnl@gmail.com> | 2025-06-24 10:28:58 +0530 |
|---|---|---|
| committer | Pulkit Garg <pulkitgargbnl@gmail.com> | 2025-06-24 10:28:58 +0530 |
| commit | e664ea0271a9c34bb7290447823367ba6fffac7d (patch) | |
| tree | ecbf5d1840edf21d661183f6ecba53ac55d73ad2 /apps/web/src/components | |
| parent | 4260d4be08817393af96e7843ed828183137119f (diff) | |
| download | opencut-e664ea0271a9c34bb7290447823367ba6fffac7d.tar.gz | |
feat: Enable Playhead Dragging for Video Navigation
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 67 | ||||
| -rw-r--r-- | apps/web/src/components/ui/video-player.tsx | 4 |
2 files changed, 57 insertions, 14 deletions
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 675b398c..e97b9cec 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -27,7 +27,7 @@ import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store";
import { processMediaFiles } from "@/lib/media-processing";
import { toast } from "sonner";
-import { useState, useRef, useEffect } from "react";
+import { useState, useRef, useEffect, useCallback } from "react";
import {
Select,
SelectContent,
@@ -69,6 +69,10 @@ export function Timeline() { additive: boolean;
} | null>(null);
+ // Playhead scrubbing state
+ const [isScrubbing, setIsScrubbing] = useState(false);
+ const [scrubTime, setScrubTime] = useState<number | null>(null);
+
// Update timeline duration when tracks change
useEffect(() => {
const totalDuration = getTotalDuration();
@@ -304,6 +308,41 @@ export function Timeline() { setZoomLevel((prev) => Math.max(0.1, Math.min(10, prev + delta)));
};
+ // --- Playhead Scrubbing Handlers ---
+ const handlePlayheadMouseDown = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ setIsScrubbing(true);
+ handleScrub(e);
+ }, [duration, zoomLevel]);
+
+ const handleScrub = useCallback((e: MouseEvent | React.MouseEvent) => {
+ const timeline = timelineRef.current;
+ if (!timeline) return;
+ const rect = timeline.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const time = Math.max(0, Math.min(duration, x / (50 * zoomLevel)));
+ setScrubTime(time);
+ seek(time); // update video preview in real time
+ }, [duration, zoomLevel, seek]);
+
+ useEffect(() => {
+ if (!isScrubbing) return;
+ const onMouseMove = (e: MouseEvent) => handleScrub(e);
+ const onMouseUp = (e: MouseEvent) => {
+ setIsScrubbing(false);
+ if (scrubTime !== null) seek(scrubTime); // finalize seek
+ setScrubTime(null);
+ };
+ window.addEventListener("mousemove", onMouseMove);
+ window.addEventListener("mouseup", onMouseUp);
+ return () => {
+ window.removeEventListener("mousemove", onMouseMove);
+ window.removeEventListener("mouseup", onMouseUp);
+ };
+ }, [isScrubbing, scrubTime, seek, handleScrub]);
+
+ const playheadPosition = isScrubbing && scrubTime !== null ? scrubTime : currentTime;
+
const dragProps = {
onDragEnter: handleDragEnter,
onDragOver: handleDragOver,
@@ -555,10 +594,11 @@ export function Timeline() { }).filter(Boolean);
})()}
- {/* Playhead in ruler */}
+ {/* Playhead in ruler (scrubbable) */}
<div
- className="absolute top-0 bottom-0 w-0.5 bg-red-500 pointer-events-none z-10"
- style={{ left: `${currentTime * 50 * zoomLevel}px` }}
+ className="absolute top-0 bottom-0 w-0.5 bg-red-500 pointer-events-auto z-10 cursor-ew-resize"
+ style={{ left: `${playheadPosition * 50 * zoomLevel}px` }}
+ onMouseDown={handlePlayheadMouseDown}
>
<div className="absolute top-1 left-1/2 transform -translate-x-1/2 w-3 h-3 bg-red-500 rounded-full border-2 border-white shadow-sm" />
</div>
@@ -668,14 +708,17 @@ export function Timeline() { </div>
))}
- {/* Playhead for tracks area */}
- <div
- className="absolute top-0 w-0.5 bg-red-500 pointer-events-none z-20"
- style={{
- left: `${currentTime * 50 * zoomLevel}px`,
- height: `${tracks.length * 60}px`,
- }}
- />
+ {/* Playhead for tracks area (scrubbable) */}
+ {tracks.length > 0 && (
+ <div
+ className="absolute top-0 w-0.5 bg-red-500 pointer-events-auto z-20 cursor-ew-resize"
+ style={{
+ left: `${playheadPosition * 50 * zoomLevel}px`,
+ height: `${tracks.length * 60}px`,
+ }}
+ onMouseDown={handlePlayheadMouseDown}
+ />
+ )}
</>
)}
</div>
diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx index 8731e1a8..fdc2ed02 100644 --- a/apps/web/src/components/ui/video-player.tsx +++ b/apps/web/src/components/ui/video-player.tsx @@ -42,7 +42,7 @@ export function VideoPlayer({ if (!video) return; const handleSeekEvent = (e: CustomEvent) => { - if (!isInClipRange) return; + // Always update video time, even if outside clip range const timelineTime = e.detail.time; const newVideoTime = Math.max(trimStart, Math.min( clipDuration - trimEnd, @@ -52,7 +52,7 @@ export function VideoPlayer({ }; const handleUpdateEvent = (e: CustomEvent) => { - if (!isInClipRange) return; + // Always update video time, even if outside clip range const timelineTime = e.detail.time; const targetVideoTime = Math.max(trimStart, Math.min( clipDuration - trimEnd, |
