refactor: move timeline element context menu into timeline-element.tsx

This commit is contained in:
Maze Winther
2025-07-08 23:51:46 +02:00
parent 66da1e20d3
commit 813dbcb9c2
2 changed files with 113 additions and 81 deletions

View File

@ -11,6 +11,7 @@ import {
ChevronRight, ChevronRight,
ChevronLeft, ChevronLeft,
Type, Type,
Copy,
} from "lucide-react"; } from "lucide-react";
import { useMediaStore } from "@/stores/media-store"; import { useMediaStore } from "@/stores/media-store";
import { useTimelineStore } from "@/stores/timeline-store"; import { useTimelineStore } from "@/stores/timeline-store";
@ -33,6 +34,13 @@ import {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "../ui/context-menu";
export function TimelineElement({ export function TimelineElement({
element, element,
@ -52,6 +60,7 @@ export function TimelineElement({
splitAndKeepLeft, splitAndKeepLeft,
splitAndKeepRight, splitAndKeepRight,
separateAudio, separateAudio,
addElementToTrack,
} = useTimelineStore(); } = useTimelineStore();
const { currentTime } = usePlaybackStore(); const { currentTime } = usePlaybackStore();
@ -172,6 +181,38 @@ export function TimelineElement({
return mediaItem?.type === "video" && track.type === "media"; 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 = () => { const renderElementContent = () => {
if (element.type === "text") { if (element.type === "text") {
return ( return (
@ -255,47 +296,69 @@ export function TimelineElement({
}; };
return ( return (
<div <ContextMenu>
className={`absolute top-0 h-full select-none timeline-element ${ <ContextMenuTrigger asChild>
isBeingDragged ? "z-50" : "z-10" <div
}`} className={`absolute top-0 h-full select-none timeline-element ${
style={{ isBeingDragged ? "z-50" : "z-10"
left: `${elementLeft}px`, }`}
width: `${elementWidth}px`, style={{
}} left: `${elementLeft}px`,
onMouseMove={resizing ? handleResizeMove : undefined} width: `${elementWidth}px`,
onMouseUp={resizing ? handleResizeEnd : undefined} }}
onMouseLeave={resizing ? handleResizeEnd : undefined} onMouseMove={resizing ? handleResizeMove : undefined}
> onMouseUp={resizing ? handleResizeEnd : undefined}
<div onMouseLeave={resizing ? handleResizeEnd : undefined}
className={`relative h-full rounded-[0.15rem] cursor-pointer overflow-hidden ${getTrackElementClasses( >
track.type <div
)} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${ className={`relative h-full rounded-[0.15rem] cursor-pointer overflow-hidden ${getTrackElementClasses(
isBeingDragged ? "z-50" : "z-10" track.type
}`} )} ${isSelected ? "border-b-[0.5px] border-t-[0.5px] border-foreground" : ""} ${
onClick={(e) => onElementClick && onElementClick(e, element)} isBeingDragged ? "z-50" : "z-10"
onMouseDown={handleElementMouseDown} }`}
onContextMenu={(e) => onClick={(e) => onElementClick && onElementClick(e, element)}
onElementMouseDown && onElementMouseDown(e, element) onMouseDown={handleElementMouseDown}
} onContextMenu={(e) =>
> onElementMouseDown && onElementMouseDown(e, element)
<div className="absolute inset-0 flex items-center h-full"> }
{renderElementContent()} >
</div> <div className="absolute inset-0 flex items-center h-full">
{renderElementContent()}
</div>
{isSelected && ( {isSelected && (
<> <>
<div <div
className="absolute left-0 top-0 bottom-0 w-1 cursor-w-resize bg-foreground z-50" className="absolute left-0 top-0 bottom-0 w-1 cursor-w-resize bg-foreground z-50"
onMouseDown={(e) => handleResizeStart(e, element.id, "left")} onMouseDown={(e) => handleResizeStart(e, element.id, "left")}
/> />
<div <div
className="absolute right-0 top-0 bottom-0 w-1 cursor-e-resize bg-foreground z-50" className="absolute right-0 top-0 bottom-0 w-1 cursor-e-resize bg-foreground z-50"
onMouseDown={(e) => handleResizeStart(e, element.id, "right")} onMouseDown={(e) => handleResizeStart(e, element.id, "right")}
/> />
</> </>
)} )}
</div> </div>
</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>
); );
} }

View File

@ -4,15 +4,7 @@ import { useRef, useState, useEffect } from "react";
import { useTimelineStore } from "@/stores/timeline-store"; import { useTimelineStore } from "@/stores/timeline-store";
import { useMediaStore } from "@/stores/media-store"; import { useMediaStore } from "@/stores/media-store";
import { toast } from "sonner"; import { toast } from "sonner";
import { Copy, Scissors, Trash2 } from "lucide-react";
import { TimelineElement } from "./timeline-element"; import { TimelineElement } from "./timeline-element";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "../ui/context-menu";
import { import {
TimelineTrack, TimelineTrack,
sortTracksByOrder, sortTracksByOrder,
@ -882,38 +874,15 @@ export function TimelineTrackContent({
}; };
return ( return (
<ContextMenu key={element.id}> <TimelineElement
<ContextMenuTrigger asChild> key={element.id}
<div> element={element}
<TimelineElement track={track}
element={element} zoomLevel={zoomLevel}
track={track} isSelected={isSelected}
zoomLevel={zoomLevel} onElementMouseDown={handleElementMouseDown}
isSelected={isSelected} onElementClick={handleElementClick}
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>
); );
})} })}
</> </>