Merge pull request #40 from pulkitgarg04/main

feat: Enable Playhead Dragging for Video Navigation
This commit is contained in:
iza
2025-06-24 08:22:20 +03:00
committed by GitHub
3 changed files with 58 additions and 32 deletions

View File

@ -45,6 +45,7 @@
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.1", "vaul": "^1.1.1",
"zod": "^3.25.67",
"zustand": "^5.0.2" "zustand": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -27,7 +27,7 @@ import { useMediaStore } from "@/stores/media-store";
import { usePlaybackStore } from "@/stores/playback-store"; import { usePlaybackStore } from "@/stores/playback-store";
import { processMediaFiles } from "@/lib/media-processing"; import { processMediaFiles } from "@/lib/media-processing";
import { toast } from "sonner"; import { toast } from "sonner";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -69,6 +69,10 @@ export function Timeline() {
additive: boolean; additive: boolean;
} | null>(null); } | null>(null);
// Playhead scrubbing state
const [isScrubbing, setIsScrubbing] = useState(false);
const [scrubTime, setScrubTime] = useState<number | null>(null);
// Update timeline duration when tracks change // Update timeline duration when tracks change
useEffect(() => { useEffect(() => {
const totalDuration = getTotalDuration(); const totalDuration = getTotalDuration();
@ -345,6 +349,41 @@ export function Timeline() {
// Otherwise, allow normal scrolling // 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 = { const dragProps = {
onDragEnter: handleDragEnter, onDragEnter: handleDragEnter,
onDragOver: handleDragOver, onDragOver: handleDragOver,
@ -686,28 +725,11 @@ export function Timeline() {
}).filter(Boolean); }).filter(Boolean);
})()} })()}
{/* Playhead in ruler */} {/* Playhead in ruler (scrubbable) */}
<div <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 className="absolute top-0 bottom-0 w-0.5 bg-red-500 pointer-events-auto z-10 cursor-ew-resize"
style={{ left: `${currentTime * 50 * zoomLevel}px` }} style={{ left: `${playheadPosition * 50 * zoomLevel}px` }}
onMouseDown={(e) => { onMouseDown={handlePlayheadMouseDown}
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
}}
> >
<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 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> </div>
@ -815,14 +837,17 @@ export function Timeline() {
</div> </div>
))} ))}
{/* Playhead for tracks area */} {/* Playhead for tracks area (scrubbable) */}
<div {tracks.length > 0 && (
className="absolute top-0 w-0.5 bg-red-500 cursor-ew-resize z-20" <div
style={{ className="absolute top-0 w-0.5 bg-red-500 pointer-events-auto z-20 cursor-ew-resize"
left: `${currentTime * 50 * zoomLevel}px`, style={{
height: `${tracks.length * 60}px`, left: `${playheadPosition * 50 * zoomLevel}px`,
}} height: `${tracks.length * 60}px`,
/> }}
onMouseDown={handlePlayheadMouseDown}
/>
)}
</> </>
)} )}
</div> </div>

View File

@ -42,7 +42,7 @@ export function VideoPlayer({
if (!video) return; if (!video) return;
const handleSeekEvent = (e: CustomEvent) => { const handleSeekEvent = (e: CustomEvent) => {
if (!isInClipRange) return; // Always update video time, even if outside clip range
const timelineTime = e.detail.time; const timelineTime = e.detail.time;
const newVideoTime = Math.max(trimStart, Math.min( const newVideoTime = Math.max(trimStart, Math.min(
clipDuration - trimEnd, clipDuration - trimEnd,
@ -52,7 +52,7 @@ export function VideoPlayer({
}; };
const handleUpdateEvent = (e: CustomEvent) => { const handleUpdateEvent = (e: CustomEvent) => {
if (!isInClipRange) return; // Always update video time, even if outside clip range
const timelineTime = e.detail.time; const timelineTime = e.detail.time;
const targetVideoTime = Math.max(trimStart, Math.min( const targetVideoTime = Math.max(trimStart, Math.min(
clipDuration - trimEnd, clipDuration - trimEnd,