diff options
Diffstat (limited to 'apps/web/src/components/editor/timeline-track.tsx')
| -rw-r--r-- | apps/web/src/components/editor/timeline-track.tsx | 220 |
1 files changed, 129 insertions, 91 deletions
diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index f1766593..c3a16b9a 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -4,16 +4,8 @@ import { useRef, useState, useEffect } from "react"; import { useTimelineStore } from "@/stores/timeline-store"; import { useMediaStore } from "@/stores/media-store"; import { toast } from "sonner"; -import { Copy, Scissors, Trash2 } from "lucide-react"; import { TimelineElement } from "./timeline-element"; import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger, -} from "../ui/context-menu"; -import { TimelineTrack, sortTracksByOrder, ensureMainTrack, @@ -25,7 +17,11 @@ import type { TimelineElement as TimelineElementType, DragData, } from "@/types/timeline"; -import { TIMELINE_CONSTANTS } from "@/lib/timeline-constants"; +import { + snapTimeToFrame, + TIMELINE_CONSTANTS, +} from "@/constants/timeline-constants"; +import { useProjectStore } from "@/stores/project-store"; export function TimelineTrackContent({ track, @@ -88,7 +84,10 @@ export function TimelineTrackContent({ mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel) ); const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime); - const snappedTime = Math.round(adjustedTime * 10) / 10; + // Use frame snapping if project has FPS, otherwise use decimal snapping + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(adjustedTime, projectFps); updateDragTime(snappedTime); }; @@ -96,65 +95,115 @@ export function TimelineTrackContent({ const handleMouseUp = (e: MouseEvent) => { if (!dragState.elementId || !dragState.trackId) return; - // Check if the mouse is actually over this track + // If this track initiated the drag, we should handle the mouse up regardless of where it occurs + const isTrackThatStartedDrag = dragState.trackId === track.id; + const timelineRect = timelineRef.current?.getBoundingClientRect(); - if (!timelineRect) return; + if (!timelineRect) { + if (isTrackThatStartedDrag) { + updateElementStartTime( + track.id, + dragState.elementId, + dragState.currentTime + ); + endDragAction(); + } + return; + } const isMouseOverThisTrack = e.clientY >= timelineRect.top && e.clientY <= timelineRect.bottom; - // Only handle if mouse is over this track - if (!isMouseOverThisTrack) return; + if (!isMouseOverThisTrack && !isTrackThatStartedDrag) return; const finalTime = dragState.currentTime; - // Check for overlaps and update position - const sourceTrack = tracks.find((t) => t.id === dragState.trackId); - const movingElement = sourceTrack?.elements.find( - (c) => c.id === dragState.elementId - ); + if (isMouseOverThisTrack) { + const sourceTrack = tracks.find((t) => t.id === dragState.trackId); + const movingElement = sourceTrack?.elements.find( + (c) => c.id === dragState.elementId + ); - if (movingElement) { - const movingElementDuration = - movingElement.duration - - movingElement.trimStart - - movingElement.trimEnd; - const movingElementEnd = finalTime + movingElementDuration; + if (movingElement) { + const movingElementDuration = + movingElement.duration - + movingElement.trimStart - + movingElement.trimEnd; + const movingElementEnd = finalTime + movingElementDuration; - const targetTrack = tracks.find((t) => t.id === track.id); - const hasOverlap = targetTrack?.elements.some((existingElement) => { - if ( - dragState.trackId === track.id && - existingElement.id === dragState.elementId - ) { - return false; + const targetTrack = tracks.find((t) => t.id === track.id); + const hasOverlap = targetTrack?.elements.some((existingElement) => { + if ( + dragState.trackId === track.id && + existingElement.id === dragState.elementId + ) { + return false; + } + const existingStart = existingElement.startTime; + const existingEnd = + existingElement.startTime + + (existingElement.duration - + existingElement.trimStart - + existingElement.trimEnd); + return finalTime < existingEnd && movingElementEnd > existingStart; + }); + + if (!hasOverlap) { + if (dragState.trackId === track.id) { + updateElementStartTime(track.id, dragState.elementId, finalTime); + } else { + moveElementToTrack( + dragState.trackId, + track.id, + dragState.elementId + ); + requestAnimationFrame(() => { + updateElementStartTime( + track.id, + dragState.elementId!, + finalTime + ); + }); + } } - const existingStart = existingElement.startTime; - const existingEnd = - existingElement.startTime + - (existingElement.duration - - existingElement.trimStart - - existingElement.trimEnd); - return finalTime < existingEnd && movingElementEnd > existingStart; - }); + } + } else if (isTrackThatStartedDrag) { + // Mouse is not over this track, but this track started the drag + // This means user released over ruler/outside - update position within same track + const sourceTrack = tracks.find((t) => t.id === dragState.trackId); + const movingElement = sourceTrack?.elements.find( + (c) => c.id === dragState.elementId + ); + + if (movingElement) { + const movingElementDuration = + movingElement.duration - + movingElement.trimStart - + movingElement.trimEnd; + const movingElementEnd = finalTime + movingElementDuration; - if (!hasOverlap) { - if (dragState.trackId === track.id) { + const hasOverlap = track.elements.some((existingElement) => { + if (existingElement.id === dragState.elementId) { + return false; + } + const existingStart = existingElement.startTime; + const existingEnd = + existingElement.startTime + + (existingElement.duration - + existingElement.trimStart - + existingElement.trimEnd); + return finalTime < existingEnd && movingElementEnd > existingStart; + }); + + if (!hasOverlap) { updateElementStartTime(track.id, dragState.elementId, finalTime); - } else { - moveElementToTrack( - dragState.trackId, - track.id, - dragState.elementId - ); - requestAnimationFrame(() => { - updateElementStartTime(track.id, dragState.elementId!, finalTime); - }); } } } - endDragAction(); + if (isTrackThatStartedDrag) { + endDragAction(); + } }; document.addEventListener("mousemove", handleMouseMove); @@ -300,7 +349,9 @@ export function TimelineTrackContent({ if (dragData.type === "text") { // Text elements have default duration of 5 seconds const newElementDuration = 5; - const snappedTime = Math.round(dropTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(dropTime, projectFps); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -319,7 +370,9 @@ export function TimelineTrackContent({ ); if (mediaItem) { const newElementDuration = mediaItem.duration || 5; - const snappedTime = Math.round(dropTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(dropTime, projectFps); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -359,7 +412,9 @@ export function TimelineTrackContent({ movingElement.duration - movingElement.trimStart - movingElement.trimEnd; - const snappedTime = Math.round(dropTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(dropTime, projectFps); const movingElementEnd = snappedTime + movingElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -386,13 +441,17 @@ export function TimelineTrackContent({ if (wouldOverlap) { e.dataTransfer.dropEffect = "none"; setWouldOverlap(true); - setDropPosition(Math.round(dropTime * 10) / 10); + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + setDropPosition(snapTimeToFrame(dropTime, projectFps)); return; } e.dataTransfer.dropEffect = hasTimelineElement ? "move" : "copy"; setWouldOverlap(false); - setDropPosition(Math.round(dropTime * 10) / 10); + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + setDropPosition(snapTimeToFrame(dropTime, projectFps)); }; const handleTrackDragEnter = (e: React.DragEvent) => { @@ -460,7 +519,9 @@ export function TimelineTrackContent({ const mouseY = e.clientY - rect.top; // Get Y position relative to this track const newStartTime = mouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel); - const snappedTime = Math.round(newStartTime * 10) / 10; + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + const snappedTime = snapTimeToFrame(newStartTime, projectFps); // Calculate drop position relative to tracks const currentTrackIndex = tracks.findIndex((t) => t.id === track.id); @@ -506,7 +567,7 @@ export function TimelineTrackContent({ const adjustedStartTime = snappedTime - clickOffsetTime; const finalStartTime = Math.max( 0, - Math.round(adjustedStartTime * 10) / 10 + snapTimeToFrame(adjustedStartTime, projectFps) ); // Check for overlaps with existing elements (excluding the moving element itself) @@ -882,38 +943,15 @@ export function TimelineTrackContent({ }; return ( - <ContextMenu key={element.id}> - <ContextMenuTrigger asChild> - <div> - <TimelineElement - element={element} - track={track} - zoomLevel={zoomLevel} - isSelected={isSelected} - onElementMouseDown={handleElementMouseDown} - onElementClick={handleElementClick} - /> - </div> - </ContextMenuTrigger> - <ContextMenuContent> - <ContextMenuItem onClick={handleElementSplit}> - <Scissors className="h-4 w-4 mr-2" /> - Split at playhead - </ContextMenuItem> - <ContextMenuItem onClick={handleElementDuplicate}> - <Copy className="h-4 w-4 mr-2" /> - Duplicate {element.type === "text" ? "text" : "clip"} - </ContextMenuItem> - <ContextMenuSeparator /> - <ContextMenuItem - onClick={handleElementDelete} - className="text-destructive focus:text-destructive" - > - <Trash2 className="h-4 w-4 mr-2" /> - Delete {element.type === "text" ? "text" : "clip"} - </ContextMenuItem> - </ContextMenuContent> - </ContextMenu> + <TimelineElement + key={element.id} + element={element} + track={track} + zoomLevel={zoomLevel} + isSelected={isSelected} + onElementMouseDown={handleElementMouseDown} + onElementClick={handleElementClick} + /> ); })} </> |
