diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-02 13:13:07 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-02 13:13:07 +0100 |
| commit | e7dcb586c0408255a0bf788e355fdb6d50d9b923 (patch) | |
| tree | 74ff0fc0f1ba501a075342db9b4e454450926154 /apps/web/src/hooks/timeline/use-timeline-drag-drop.ts | |
| parent | 93bea01c9ed3fcbebc64c9cef382a51d24c925ed (diff) | |
| download | opencut-e7dcb586c0408255a0bf788e355fdb6d50d9b923.tar.gz | |
feat: Clip effects, asset sorting, and timeline improvements
Major features and improvements:
* **Clip Effects**:
* Added UI in Properties Panel to manage effects on video/image clips (add, remove, toggle, reorder).
* Implemented dynamic parameter fields for effects.
* Added support for keyframing effect parameters.
* **Assets Panel**:
* Added sorting options: Name, Type, Duration, and File Size.
* Persisted view preferences (grid/list mode, sort order) to local storage.
* Refactored media item rendering and drag interactions.
* **Timeline & Interaction**:
* **Keyframe Dragging**: Added ability to drag keyframes directly on the timeline element.
* **Resizing**: Improved resize logic to respect neighboring clips (prevents overlaps).
* **Visuals**: Implemented tiled background rendering for video/image clips on the timeline.
* **Shortcuts**: Added "Deselect All" action bound to the `Escape` key.
* **Fixes**: Corrected drag-and-drop coordinate calculations when the timeline track area is scrolled.
* **Text Elements**:
* Refactored text background storage to use an explicit `enabled` flag.
* Added `V8toV9` storage migration to update existing projects.
* **Architecture**:
* Moved export state management to `ProjectManager` for better lifecycle handling.
* Refactored `PropertiesPanel` sections to be more composable (custom headers, borders).
Diffstat (limited to 'apps/web/src/hooks/timeline/use-timeline-drag-drop.ts')
| -rw-r--r-- | apps/web/src/hooks/timeline/use-timeline-drag-drop.ts | 163 |
1 files changed, 100 insertions, 63 deletions
diff --git a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts index ee739298..8a30785e 100644 --- a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts +++ b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts @@ -26,12 +26,14 @@ import type { interface UseTimelineDragDropProps { containerRef: RefObject<HTMLDivElement | null>; headerRef?: RefObject<HTMLElement | null>; + tracksScrollRef?: RefObject<HTMLDivElement | null>; zoomLevel: number; } export function useTimelineDragDrop({ containerRef, headerRef, + tracksScrollRef, zoomLevel, }: UseTimelineDragDropProps) { const editor = useEditor(); @@ -104,11 +106,16 @@ export function useTimelineDragDrop({ (e: React.DragEvent) => { e.preventDefault(); - const rect = containerRef.current?.getBoundingClientRect(); - if (!rect) return; + const scrollContainer = tracksScrollRef?.current; + const referenceRect = + scrollContainer?.getBoundingClientRect() ?? + containerRef.current?.getBoundingClientRect(); + if (!referenceRect) return; const headerHeight = headerRef?.current?.getBoundingClientRect().height ?? 0; + const scrollLeft = scrollContainer?.scrollLeft ?? 0; + const scrollTop = scrollContainer?.scrollTop ?? 0; const hasFiles = e.dataTransfer.types.includes("Files"); const isExternal = hasFiles && !hasDragData({ dataTransfer: e.dataTransfer }); @@ -131,8 +138,8 @@ export function useTimelineDragDrop({ mediaId: dragData?.type === "media" ? dragData.id : undefined, }); - const mouseX = e.clientX - rect.left; - const mouseY = Math.max(0, e.clientY - rect.top - headerHeight); + const mouseX = e.clientX - referenceRect.left + scrollLeft; + const mouseY = e.clientY - referenceRect.top + scrollTop - headerHeight; const targetElementTypes = dragData?.type === "effect" @@ -162,6 +169,7 @@ export function useTimelineDragDrop({ [ containerRef, headerRef, + tracksScrollRef, tracks, currentTime, zoomLevel, @@ -200,19 +208,6 @@ export function useTimelineDragDrop({ target: DropTarget; dragData: { name?: string; content?: string }; }) => { - let trackId: string; - - if (target.isNewTrack) { - trackId = editor.timeline.addTrack({ - type: "text", - index: target.trackIndex, - }); - } else { - const track = tracks[target.trackIndex]; - if (!track) return; - trackId = track.id; - } - const element = buildTextElement({ raw: { name: dragData.name ?? "", @@ -221,12 +216,26 @@ export function useTimelineDragDrop({ startTime: target.xPosition, }); + if (target.isNewTrack) { + const addTrackCmd = new AddTrackCommand("text", target.trackIndex); + const insertCmd = new InsertElementCommand({ + element, + placement: { mode: "explicit", trackId: addTrackCmd.getTrackId() }, + }); + editor.command.execute({ + command: new BatchCommand([addTrackCmd, insertCmd]), + }); + return; + } + + const track = tracks[target.trackIndex]; + if (!track) return; editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, + placement: { mode: "explicit", trackId: track.id }, element, }); }, - [editor.timeline, tracks], + [editor.command, editor.timeline, tracks], ); const executeStickerDrop = useCallback( @@ -237,31 +246,32 @@ export function useTimelineDragDrop({ target: DropTarget; dragData: StickerDragData; }) => { - let trackId: string; - - if (target.isNewTrack) { - trackId = editor.timeline.addTrack({ - type: "sticker", - index: target.trackIndex, - }); - } else { - const track = tracks[target.trackIndex]; - if (!track) return; - trackId = track.id; - } - const element = buildStickerElement({ stickerId: dragData.stickerId, name: dragData.name, startTime: target.xPosition, }); + if (target.isNewTrack) { + const addTrackCmd = new AddTrackCommand("sticker", target.trackIndex); + const insertCmd = new InsertElementCommand({ + element, + placement: { mode: "explicit", trackId: addTrackCmd.getTrackId() }, + }); + editor.command.execute({ + command: new BatchCommand([addTrackCmd, insertCmd]), + }); + return; + } + + const track = tracks[target.trackIndex]; + if (!track) return; editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, + placement: { mode: "explicit", trackId: track.id }, element, }); }, - [editor.timeline, tracks], + [editor.command, editor.timeline, tracks], ); const executeMediaDrop = useCallback( @@ -276,18 +286,6 @@ export function useTimelineDragDrop({ const trackType: TrackType = dragData.mediaType === "audio" ? "audio" : "video"; - let trackId: string; - - if (target.isNewTrack) { - trackId = editor.timeline.addTrack({ - type: trackType, - index: target.trackIndex, - }); - } else { - const track = tracks[target.trackIndex]; - if (!track) return; - trackId = track.id; - } const duration = mediaAsset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION; @@ -299,31 +297,63 @@ export function useTimelineDragDrop({ startTime: target.xPosition, }); + if (target.isNewTrack) { + const addTrackCmd = new AddTrackCommand(trackType, target.trackIndex); + const insertCmd = new InsertElementCommand({ + element, + placement: { mode: "explicit", trackId: addTrackCmd.getTrackId() }, + }); + editor.command.execute({ + command: new BatchCommand([addTrackCmd, insertCmd]), + }); + return; + } + + const track = tracks[target.trackIndex]; + if (!track) return; editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, + placement: { mode: "explicit", trackId: track.id }, element, }); }, - [editor.timeline, mediaAssets, tracks], + [editor.command, editor.timeline, mediaAssets, tracks], ); const executeEffectDrop = useCallback( - ({ target, dragData }: { target: DropTarget; dragData: EffectDragData }) => { + ({ + target, + dragData, + }: { + target: DropTarget; + dragData: EffectDragData; + }) => { + if (target.targetElement) { + editor.timeline.addClipEffect({ + trackId: target.targetElement.trackId, + elementId: target.targetElement.elementId, + effectType: dragData.effectType, + }); + return; + } + const effectTrack = tracks.find((t) => t.type === "effect"); let trackId: string; - if (effectTrack && !target.targetElement) { + if (effectTrack) { trackId = effectTrack.id; - } else if (target.targetElement) { - trackId = effectTrack?.id ?? editor.timeline.addTrack({ - type: "effect", - index: 0, - }); } else if (target.isNewTrack) { - trackId = editor.timeline.addTrack({ - type: "effect", - index: target.trackIndex, + const addTrackCmd = new AddTrackCommand("effect", target.trackIndex); + const insertCmd = new InsertElementCommand({ + element: buildEffectElement({ + effectType: dragData.effectType, + startTime: target.xPosition, + }), + placement: { mode: "explicit", trackId: addTrackCmd.getTrackId() }, }); + editor.command.execute({ + command: new BatchCommand([addTrackCmd, insertCmd]), + }); + return; } else { const track = tracks[target.trackIndex]; if (!track || track.type !== "effect") return; @@ -340,7 +370,7 @@ export function useTimelineDragDrop({ element, }); }, - [editor.timeline, tracks], + [editor.command, editor.timeline, tracks], ); const executeFileDrop = useCallback( @@ -452,12 +482,18 @@ export function useTimelineDragDrop({ executeMediaDrop({ target: currentTarget, dragData }); } } else if (hasFiles) { - const rect = containerRef.current?.getBoundingClientRect(); - if (!rect) return; - const mouseX = e.clientX - rect.left; + const scrollContainer = tracksScrollRef?.current; + const referenceRect = + scrollContainer?.getBoundingClientRect() ?? + containerRef.current?.getBoundingClientRect(); + if (!referenceRect) return; + const scrollLeft = scrollContainer?.scrollLeft ?? 0; + const scrollTop = scrollContainer?.scrollTop ?? 0; + const mouseX = e.clientX - referenceRect.left + scrollLeft; const headerHeight = headerRef?.current?.getBoundingClientRect().height ?? 0; - const mouseY = Math.max(0, e.clientY - rect.top - headerHeight); + const mouseY = + e.clientY - referenceRect.top + scrollTop - headerHeight; await executeFileDrop({ files: Array.from(e.dataTransfer.files), mouseX, @@ -478,6 +514,7 @@ export function useTimelineDragDrop({ executeFileDrop, containerRef, headerRef, + tracksScrollRef, ], ); |
