summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-25 16:47:22 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:48:22 +0200
commit8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch)
treee9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/hooks/timeline/use-timeline-drag-drop.ts
parent39ea298a9ccba3104c549c0ca079c231981811f5 (diff)
downloadopencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz
feat: masks, properties refactor, shaders, storage migrations, and more
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.ts223
1 files changed, 150 insertions, 73 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 8a30785e..e8f6e49d 100644
--- a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts
+++ b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts
@@ -2,26 +2,28 @@ import { useState, useCallback, type RefObject } from "react";
import { useEditor } from "@/hooks/use-editor";
import { processMediaAssets } from "@/lib/media/processing";
import { toast } from "sonner";
+import { showMediaUploadToast } from "@/lib/media/upload-toast";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { snapTimeToFrame } from "@/lib/time";
import {
buildTextElement,
+ buildGraphicElement,
buildStickerElement,
buildElementFromMedia,
buildEffectElement,
} from "@/lib/timeline/element-utils";
-import type { Command } from "@/lib/commands/base-command";
-import { AddMediaAssetCommand } from "@/lib/commands/media";
import { AddTrackCommand, InsertElementCommand } from "@/lib/commands/timeline";
import { BatchCommand } from "@/lib/commands";
import { computeDropTarget } from "@/lib/timeline/drop-utils";
import { getDragData, hasDragData } from "@/lib/drag-data";
-import type { TrackType, DropTarget, ElementType } from "@/types/timeline";
+import { isMainTrack } from "@/lib/timeline/track-utils";
+import type { TrackType, DropTarget, ElementType } from "@/lib/timeline";
import type {
MediaDragData,
+ GraphicDragData,
StickerDragData,
EffectDragData,
-} from "@/types/drag";
+} from "@/lib/timeline/drag";
interface UseTimelineDragDropProps {
containerRef: RefObject<HTMLDivElement | null>;
@@ -41,17 +43,12 @@ export function useTimelineDragDrop({
const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
const [dragElementType, setElementType] = useState<ElementType | null>(null);
- const tracks = editor.timeline.getTracks();
- const currentTime = editor.playback.getCurrentTime();
- const mediaAssets = editor.media.getAssets();
- const activeProject = editor.project.getActive();
-
const getSnappedTime = useCallback(
({ time }: { time: number }) => {
- const projectFps = activeProject.settings.fps;
+ const projectFps = editor.project.getActive().settings.fps;
return snapTimeToFrame({ time, fps: projectFps });
},
- [activeProject.settings.fps],
+ [editor],
);
const getElementType = useCallback(
@@ -60,6 +57,7 @@ export function useTimelineDragDrop({
if (!dragData) return null;
if (dragData.type === "text") return "text";
+ if (dragData.type === "graphic") return "graphic";
if (dragData.type === "sticker") return "sticker";
if (dragData.type === "effect") return "effect";
if (dragData.type === "media") {
@@ -80,18 +78,20 @@ export function useTimelineDragDrop({
}): number => {
if (
elementType === "text" ||
+ elementType === "graphic" ||
elementType === "sticker" ||
elementType === "effect"
) {
return TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
}
if (mediaId) {
+ const mediaAssets = editor.media.getAssets();
const media = mediaAssets.find((m) => m.id === mediaId);
return media?.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
}
return TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
},
- [mediaAssets],
+ [editor],
);
const handleDragEnter = useCallback((e: React.DragEvent) => {
@@ -148,6 +148,8 @@ export function useTimelineDragDrop({
? (dragData as MediaDragData).targetElementTypes
: undefined;
+ const tracks = editor.timeline.getTracks();
+ const currentTime = editor.playback.getCurrentTime();
const target = computeDropTarget({
elementType,
mouseX,
@@ -170,12 +172,11 @@ export function useTimelineDragDrop({
containerRef,
headerRef,
tracksScrollRef,
- tracks,
- currentTime,
zoomLevel,
getElementType,
getElementDuration,
getSnappedTime,
+ editor,
],
);
@@ -228,6 +229,7 @@ export function useTimelineDragDrop({
return;
}
+ const tracks = editor.timeline.getTracks();
const track = tracks[target.trackIndex];
if (!track) return;
editor.timeline.insertElement({
@@ -235,7 +237,7 @@ export function useTimelineDragDrop({
element,
});
},
- [editor.command, editor.timeline, tracks],
+ [editor],
);
const executeStickerDrop = useCallback(
@@ -253,7 +255,45 @@ export function useTimelineDragDrop({
});
if (target.isNewTrack) {
- const addTrackCmd = new AddTrackCommand("sticker", target.trackIndex);
+ const addTrackCmd = new AddTrackCommand("graphic", target.trackIndex);
+ const insertCmd = new InsertElementCommand({
+ element,
+ placement: { mode: "explicit", trackId: addTrackCmd.getTrackId() },
+ });
+ editor.command.execute({
+ command: new BatchCommand([addTrackCmd, insertCmd]),
+ });
+ return;
+ }
+
+ const tracks = editor.timeline.getTracks();
+ const track = tracks[target.trackIndex];
+ if (!track) return;
+ editor.timeline.insertElement({
+ placement: { mode: "explicit", trackId: track.id },
+ element,
+ });
+ },
+ [editor],
+ );
+
+ const executeGraphicDrop = useCallback(
+ ({
+ target,
+ dragData,
+ }: {
+ target: DropTarget;
+ dragData: GraphicDragData;
+ }) => {
+ const element = buildGraphicElement({
+ definitionId: dragData.definitionId,
+ name: dragData.name,
+ startTime: target.xPosition,
+ params: dragData.params,
+ });
+
+ if (target.isNewTrack) {
+ const addTrackCmd = new AddTrackCommand("graphic", target.trackIndex);
const insertCmd = new InsertElementCommand({
element,
placement: { mode: "explicit", trackId: addTrackCmd.getTrackId() },
@@ -264,6 +304,7 @@ export function useTimelineDragDrop({
return;
}
+ const tracks = editor.timeline.getTracks();
const track = tracks[target.trackIndex];
if (!track) return;
editor.timeline.insertElement({
@@ -271,7 +312,7 @@ export function useTimelineDragDrop({
element,
});
},
- [editor.command, editor.timeline, tracks],
+ [editor],
);
const executeMediaDrop = useCallback(
@@ -281,6 +322,7 @@ export function useTimelineDragDrop({
return;
}
+ const mediaAssets = editor.media.getAssets();
const mediaAsset = mediaAssets.find((m) => m.id === dragData.id);
if (!mediaAsset) return;
@@ -309,6 +351,7 @@ export function useTimelineDragDrop({
return;
}
+ const tracks = editor.timeline.getTracks();
const track = tracks[target.trackIndex];
if (!track) return;
editor.timeline.insertElement({
@@ -316,7 +359,7 @@ export function useTimelineDragDrop({
element,
});
},
- [editor.command, editor.timeline, mediaAssets, tracks],
+ [editor],
);
const executeEffectDrop = useCallback(
@@ -336,6 +379,7 @@ export function useTimelineDragDrop({
return;
}
+ const tracks = editor.timeline.getTracks();
const effectTrack = tracks.find((t) => t.type === "effect");
let trackId: string;
@@ -370,7 +414,7 @@ export function useTimelineDragDrop({
element,
});
},
- [editor.command, editor.timeline, tracks],
+ [editor],
);
const executeFileDrop = useCallback(
@@ -383,70 +427,98 @@ export function useTimelineDragDrop({
mouseX: number;
mouseY: number;
}) => {
+ const activeProject = editor.project.getActiveOrNull();
if (!activeProject) return;
- const processedAssets = await processMediaAssets({ files });
- const projectId = activeProject.metadata.id;
+ await showMediaUploadToast({
+ filesCount: files.length,
+ promise: async () => {
+ const processedAssets = await processMediaAssets({ files });
+ const projectId = activeProject.metadata.id;
- for (const asset of processedAssets) {
- const duration =
- asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
- const currentTracks = editor.timeline.getTracks();
- const dropTarget = computeDropTarget({
- elementType: asset.type,
- mouseX,
- mouseY,
- tracks: currentTracks,
- playheadTime: currentTime,
- isExternalDrop: true,
- elementDuration: duration,
- pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
- zoomLevel,
- });
+ for (const asset of processedAssets) {
+ const createdAsset = await editor.media.addMediaAsset({
+ projectId,
+ asset,
+ });
+ if (!createdAsset) continue;
- const trackType: TrackType = asset.type === "audio" ? "audio" : "video";
- const addMediaCmd = new AddMediaAssetCommand(projectId, asset);
- const assetId = addMediaCmd.getAssetId();
+ const duration =
+ createdAsset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
+ const currentTracks = editor.timeline.getTracks();
+ const currentTime = editor.playback.getCurrentTime();
+ const onlyTrack = currentTracks[0];
+ const reuseMainTrackId =
+ createdAsset.type !== "audio" &&
+ currentTracks.length === 1 &&
+ !!onlyTrack &&
+ isMainTrack(onlyTrack) &&
+ onlyTrack.elements.length === 0
+ ? onlyTrack.id
+ : null;
+ const dropTarget = reuseMainTrackId
+ ? null
+ : computeDropTarget({
+ elementType: createdAsset.type,
+ mouseX,
+ mouseY,
+ tracks: currentTracks,
+ playheadTime: currentTime,
+ isExternalDrop: true,
+ elementDuration: duration,
+ pixelsPerSecond: TIMELINE_CONSTANTS.PIXELS_PER_SECOND,
+ zoomLevel,
+ });
- const commands: Command[] = [addMediaCmd];
+ const trackType: TrackType =
+ createdAsset.type === "audio" ? "audio" : "video";
- let trackId: string | undefined;
- if (dropTarget.isNewTrack) {
- const addTrackCmd = new AddTrackCommand(
- trackType,
- dropTarget.trackIndex,
- );
- trackId = addTrackCmd.getTrackId();
- commands.unshift(addTrackCmd);
- } else {
- trackId = currentTracks[dropTarget.trackIndex]?.id;
- }
+ let trackId: string | undefined;
+ if (reuseMainTrackId) {
+ trackId = reuseMainTrackId;
+ } else {
+ if (!dropTarget) continue;
+ if (dropTarget.isNewTrack) {
+ const addTrackCmd = new AddTrackCommand(
+ trackType,
+ dropTarget.trackIndex,
+ );
+ trackId = addTrackCmd.getTrackId();
+ editor.command.execute({ command: addTrackCmd });
+ } else {
+ trackId = currentTracks[dropTarget.trackIndex]?.id;
+ }
+ }
- if (!trackId) return;
+ if (!trackId) continue;
- const element = buildElementFromMedia({
- mediaId: assetId,
- mediaType: asset.type,
- name: asset.name,
- duration,
- startTime: dropTarget.xPosition,
- buffer:
- asset.type === "audio"
- ? new AudioBuffer({ length: 1, sampleRate: 44100 })
- : undefined,
- });
+ const element = buildElementFromMedia({
+ mediaId: createdAsset.id,
+ mediaType: createdAsset.type,
+ name: createdAsset.name,
+ duration,
+ startTime: dropTarget?.xPosition ?? currentTime,
+ buffer:
+ createdAsset.type === "audio"
+ ? new AudioBuffer({ length: 1, sampleRate: 44100 })
+ : undefined,
+ });
- const insertCmd = new InsertElementCommand({
- element,
- placement: { mode: "explicit", trackId },
- });
- commands.push(insertCmd);
+ const insertCmd = new InsertElementCommand({
+ element,
+ placement: { mode: "explicit", trackId },
+ });
+ editor.command.execute({ command: insertCmd });
+ }
- const batchCmd = new BatchCommand(commands);
- editor.command.execute({ command: batchCmd });
- }
+ return {
+ uploadedCount: processedAssets.length,
+ assetNames: processedAssets.map((asset) => asset.name),
+ };
+ },
+ });
},
- [activeProject, editor.command, editor.timeline, currentTime, zoomLevel],
+ [editor, zoomLevel],
);
const handleDrop = useCallback(
@@ -471,6 +543,11 @@ export function useTimelineDragDrop({
if (dragData.type === "text") {
executeTextDrop({ target: currentTarget, dragData });
+ } else if (dragData.type === "graphic") {
+ executeGraphicDrop({
+ target: currentTarget,
+ dragData: dragData as GraphicDragData,
+ });
} else if (dragData.type === "sticker") {
executeStickerDrop({ target: currentTarget, dragData });
} else if (dragData.type === "effect") {
@@ -502,12 +579,12 @@ export function useTimelineDragDrop({
}
} catch (err) {
console.error("Failed to process drop:", err);
- toast.error("Failed to process drop");
}
},
[
dropTarget,
executeTextDrop,
+ executeGraphicDrop,
executeStickerDrop,
executeMediaDrop,
executeEffectDrop,