diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-07-08 23:51:46 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-07-08 23:51:46 +0200 |
| commit | 813dbcb9c2ad301d7a0018ae705dcb17f833e332 (patch) | |
| tree | 01c118f61b1ec4cb55a40277daf1adf161ee62e1 /apps/web/src/components/editor | |
| parent | 66da1e20d35dca5b9168cab9e2f3555c210a7116 (diff) | |
| download | opencut-813dbcb9c2ad301d7a0018ae705dcb17f833e332.tar.gz | |
refactor: move timeline element context menu into timeline-element.tsx
Diffstat (limited to 'apps/web/src/components/editor')
| -rw-r--r-- | apps/web/src/components/editor/timeline-element.tsx | 145 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline-track.tsx | 49 |
2 files changed, 113 insertions, 81 deletions
diff --git a/apps/web/src/components/editor/timeline-element.tsx b/apps/web/src/components/editor/timeline-element.tsx index 4a909ce4..1e1674bf 100644 --- a/apps/web/src/components/editor/timeline-element.tsx +++ b/apps/web/src/components/editor/timeline-element.tsx @@ -11,6 +11,7 @@ import { ChevronRight, ChevronLeft, Type, + Copy, } from "lucide-react"; import { useMediaStore } from "@/stores/media-store"; import { useTimelineStore } from "@/stores/timeline-store"; @@ -33,6 +34,13 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger, } from "../ui/dropdown-menu"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "../ui/context-menu"; export function TimelineElement({ element, @@ -52,6 +60,7 @@ export function TimelineElement({ splitAndKeepLeft, splitAndKeepRight, separateAudio, + addElementToTrack, } = useTimelineStore(); const { currentTime } = usePlaybackStore(); @@ -172,6 +181,38 @@ export function TimelineElement({ return mediaItem?.type === "video" && track.type === "media"; }; + const handleElementSplitContext = () => { + const effectiveStart = element.startTime; + const effectiveEnd = + element.startTime + + (element.duration - element.trimStart - element.trimEnd); + + if (currentTime > effectiveStart && currentTime < effectiveEnd) { + const secondElementId = splitElement(track.id, element.id, currentTime); + if (!secondElementId) { + toast.error("Failed to split element"); + } + } else { + toast.error("Playhead must be within element to split"); + } + }; + + const handleElementDuplicateContext = () => { + const { id, ...elementWithoutId } = element; + addElementToTrack(track.id, { + ...elementWithoutId, + name: element.name + " (copy)", + startTime: + element.startTime + + (element.duration - element.trimStart - element.trimEnd) + + 0.1, + }); + }; + + const handleElementDeleteContext = () => { + removeElementFromTrack(track.id, element.id); + }; + const renderElementContent = () => { if (element.type === "text") { return ( @@ -255,47 +296,69 @@ export function TimelineElement({ }; return ( - <div - className={`absolute top-0 h-full select-none timeline-element ${ - isBeingDragged ? "z-50" : "z-10" - }`} - style={{ - left: `${elementLeft}px`, - width: `${elementWidth}px`, - }} - onMouseMove={resizing ? handleResizeMove : undefined} - onMouseUp={resizing ? handleResizeEnd : undefined} - onMouseLeave={resizing ? handleResizeEnd : undefined} - > - <div - className={`relative h-full rounded-[0.15rem] cursor-pointer overflow-hidden ${getTrackElementClasses( - track.type - )} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${ - isBeingDragged ? "z-50" : "z-10" - }`} - onClick={(e) => onElementClick && onElementClick(e, element)} - onMouseDown={handleElementMouseDown} - onContextMenu={(e) => - onElementMouseDown && onElementMouseDown(e, element) - } - > - <div className="absolute inset-0 flex items-center h-full"> - {renderElementContent()} - </div> + <ContextMenu> + <ContextMenuTrigger asChild> + <div + className={`absolute top-0 h-full select-none timeline-element ${ + isBeingDragged ? "z-50" : "z-10" + }`} + style={{ + left: `${elementLeft}px`, + width: `${elementWidth}px`, + }} + onMouseMove={resizing ? handleResizeMove : undefined} + onMouseUp={resizing ? handleResizeEnd : undefined} + onMouseLeave={resizing ? handleResizeEnd : undefined} + > + <div + className={`relative h-full rounded-[0.15rem] cursor-pointer overflow-hidden ${getTrackElementClasses( + track.type + )} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${ + isBeingDragged ? "z-50" : "z-10" + }`} + onClick={(e) => onElementClick && onElementClick(e, element)} + onMouseDown={handleElementMouseDown} + onContextMenu={(e) => + onElementMouseDown && onElementMouseDown(e, element) + } + > + <div className="absolute inset-0 flex items-center h-full"> + {renderElementContent()} + </div> - {isSelected && ( - <> - <div - className="absolute left-0 top-0 bottom-0 w-1 cursor-w-resize bg-foreground z-50" - onMouseDown={(e) => handleResizeStart(e, element.id, "left")} - /> - <div - className="absolute right-0 top-0 bottom-0 w-1 cursor-e-resize bg-foreground z-50" - onMouseDown={(e) => handleResizeStart(e, element.id, "right")} - /> - </> - )} - </div> - </div> + {isSelected && ( + <> + <div + className="absolute left-0 top-0 bottom-0 w-1 cursor-w-resize bg-foreground z-50" + onMouseDown={(e) => handleResizeStart(e, element.id, "left")} + /> + <div + className="absolute right-0 top-0 bottom-0 w-1 cursor-e-resize bg-foreground z-50" + onMouseDown={(e) => handleResizeStart(e, element.id, "right")} + /> + </> + )} + </div> + </div> + </ContextMenuTrigger> + <ContextMenuContent> + <ContextMenuItem onClick={handleElementSplitContext}> + <Scissors className="h-4 w-4 mr-2" /> + Split at playhead + </ContextMenuItem> + <ContextMenuItem onClick={handleElementDuplicateContext}> + <Copy className="h-4 w-4 mr-2" /> + Duplicate {element.type === "text" ? "text" : "clip"} + </ContextMenuItem> + <ContextMenuSeparator /> + <ContextMenuItem + onClick={handleElementDeleteContext} + className="text-destructive focus:text-destructive" + > + <Trash2 className="h-4 w-4 mr-2" /> + Delete {element.type === "text" ? "text" : "clip"} + </ContextMenuItem> + </ContextMenuContent> + </ContextMenu> ); } diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index f1766593..008768d4 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, @@ -882,38 +874,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} + /> ); })} </> |
