Merge pull request #40 from pulkitgarg04/main
feat: Enable Playhead Dragging for Video Navigation
This commit is contained in:
1
apps/web/package-lock.json
generated
1
apps/web/package-lock.json
generated
@ -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": {
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user