diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/package-lock.json | 1 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 85 | ||||
| -rw-r--r-- | apps/web/src/components/ui/video-player.tsx | 4 |
3 files changed, 58 insertions, 32 deletions
diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 7718a971..40178823 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -45,6 +45,7 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.1", + "zod": "^3.25.67", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 41e842bb..ce81f9e3 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();
@@ -345,6 +349,41 @@ export function Timeline() { // Otherwise, allow normal scrolling
};
+ // --- 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,
@@ -686,28 +725,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 cursor-ew-resize z-1000" // Remove pointer-events-none to allow dragging
- style={{ left: `${currentTime * 50 * zoomLevel}px` }}
- onMouseDown={(e) => {
- e.preventDefault();
- e.stopPropagation();
- const handleMouseMove = (e: MouseEvent) => {
- const timeline = timelineRef.current; // Get timeline element ref to track the position
- if (!timeline) return; // If no timeline element, exit
- const rect = timeline.getBoundingClientRect(); // Get the bounding rect of the timeline element
- const mouseX = Math.max(0, e.clientX - rect.left); // Calculate the mouse position relative to the timeline element
- const newTime = mouseX / (50 * zoomLevel); // Calculate the time based on the mouse position
- seek(newTime); // Set the current time
- };
- const handleMouseUp = () => {
- window.removeEventListener("mousemove", handleMouseMove); // Remove the mousemove event listener
- window.removeEventListener("mouseup", handleMouseUp); // Remove the mouseup event listener
- };
- window.addEventListener("mousemove", handleMouseMove); // Add the mousemove event listener
- window.addEventListener("mouseup", handleMouseUp); // Add the mouseup event listener
- }}
+ 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>
@@ -815,14 +837,17 @@ export function Timeline() { </div>
))}
- {/* Playhead for tracks area */}
- <div
- className="absolute top-0 w-0.5 bg-red-500 cursor-ew-resize 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, |
