diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-02-23 03:24:02 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-23 03:24:02 +0100 |
| commit | 93d1e3383ce47e45157d70f9ba6251239dae6bc2 (patch) | |
| tree | 1e33a2d7533b65f0e7c8927f03905a1a99937bd1 /apps/web/src/hooks/timeline/use-timeline-drag-drop.ts | |
| parent | fca99d6126c31fbb18ed9f1034cee6f940b040e8 (diff) | |
| download | opencut-93d1e3383ce47e45157d70f9ba6251239dae6bc2.tar.gz | |
feat: major editor overhaul (assets, properties, timeline, fonts) (#709)
* feat: major editor overhaul (assets, properties, timeline, fonts)
Refactor editor core systems to standardize UI architecture and improve performance.
Assets & Properties:
- Replace monolithic property items with composable `Section` architecture.
- Add specialized sections for Transform, Blending, and Text.
- Implement `NumberField` with scrubbing and math evaluation.
- Add new ColorPicker with EyeDropper and multiple format support.
- Standardize asset panels using new `PanelView` layout.
Fonts & Stickers:
- Implement custom font atlas/sprite system for high-performance previews.
- Add virtualized FontPicker with search and favorites.
- Refactor stickers to use a provider-based architecture (icons, emoji, flags, shapes).
- Standardize sticker IDs to `provider:value` format.
Timeline & Interaction:
- Convert bookmarks to rich objects with notes, colors, and duration.
- Refactor drag-and-drop to use Command pattern (enabling proper undo/redo).
- Add Shift modifier to disable snapping during moves/resizes.
- Add new overlays for layout guides and text editing.
Renderer:
- Add support for multi-line text, custom line-height, and letter-spacing.
- Implement global composite operation (blend modes).
- Update sticker node to resolve dynamic provider IDs.
Infrastructure:
- Add storage migrations (v3->v6) for text weights, sticker IDs, and bookmarks.
- Update global styles and core UI components (Button, Input, Popover).
* add ts-nocheck directive to settings-legacy.tsx to suppress TypeScript errors
* fix: correct global composite operation assignment in TextNode to ensure proper blend mode handling
* deleted shadcn components with errors
* formatting
* fix linter issues
* migrate from next middleware to proxy
* add missing component back
* add breadcrumb back
* chore: add @radix-ui/react-primitive deps
* chore: more deps
* chore: add missing env vars to bun-ci
* next env
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 | 168 |
1 files changed, 69 insertions, 99 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 01af9362..632d2c78 100644 --- a/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts +++ b/apps/web/src/hooks/timeline/use-timeline-drag-drop.ts @@ -7,10 +7,12 @@ import { snapTimeToFrame } from "@/lib/time"; import { buildTextElement, buildStickerElement, - buildUploadAudioElement, - buildVideoElement, - buildImageElement, + buildElementFromMedia, } 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"; @@ -231,7 +233,8 @@ export function useTimelineDragDrop({ } const element = buildStickerElement({ - iconName: dragData.iconName, + stickerId: dragData.stickerId, + name: dragData.name, startTime: target.xPosition, }); @@ -265,38 +268,18 @@ export function useTimelineDragDrop({ const duration = mediaAsset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION; + const element = buildElementFromMedia({ + mediaId: mediaAsset.id, + mediaType: mediaAsset.type, + name: mediaAsset.name, + duration, + startTime: target.xPosition, + }); - if (dragData.mediaType === "audio") { - editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, - element: buildUploadAudioElement({ - mediaId: mediaAsset.id, - name: mediaAsset.name, - duration, - startTime: target.xPosition, - }), - }); - } else if (dragData.mediaType === "video") { - editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, - element: buildVideoElement({ - mediaId: mediaAsset.id, - name: mediaAsset.name, - duration, - startTime: target.xPosition, - }), - }); - } else { - editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, - element: buildImageElement({ - mediaId: mediaAsset.id, - name: mediaAsset.name, - duration, - startTime: target.xPosition, - }), - }); - } + editor.timeline.insertElement({ + placement: { mode: "explicit", trackId }, + element, + }); }, [editor.timeline, mediaAssets, tracks], ); @@ -314,80 +297,67 @@ export function useTimelineDragDrop({ if (!activeProject) return; const processedAssets = await processMediaAssets({ files }); + const projectId = activeProject.metadata.id; for (const asset of processedAssets) { - await editor.media.addMediaAsset({ - projectId: activeProject.metadata.id, - asset, + 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, }); - const added = editor.media - .getAssets() - .find((m) => m.name === asset.name && m.url === asset.url); + const trackType: TrackType = asset.type === "audio" ? "audio" : "video"; + const addMediaCmd = new AddMediaAssetCommand(projectId, asset); + const assetId = addMediaCmd.getAssetId(); - if (added) { - const duration = - added.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION; - const currentTracks = editor.timeline.getTracks(); - const dropTarget = computeDropTarget({ - elementType: added.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 = - added.type === "audio" ? "audio" : "video"; - const trackId = dropTarget.isNewTrack - ? editor.timeline.addTrack({ - type: trackType, - index: dropTarget.trackIndex, - }) - : currentTracks[dropTarget.trackIndex]?.id; + 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; + } - if (!trackId) return; + if (!trackId) return; - if (added.type === "audio") { - editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, - element: buildUploadAudioElement({ - mediaId: added.id, - name: added.name, - duration, - startTime: dropTarget.xPosition, - buffer: new AudioBuffer({ length: 1, sampleRate: 44100 }), - }), - }); - } else if (added.type === "video") { - editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, - element: buildVideoElement({ - mediaId: added.id, - name: added.name, - duration, - startTime: dropTarget.xPosition, - }), - }); - } else { - editor.timeline.insertElement({ - placement: { mode: "explicit", trackId }, - element: buildImageElement({ - mediaId: added.id, - name: added.name, - duration, - startTime: dropTarget.xPosition, - }), - }); - } - } + 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 insertCmd = new InsertElementCommand({ + element, + placement: { mode: "explicit", trackId }, + }); + commands.push(insertCmd); + + const batchCmd = new BatchCommand(commands); + editor.command.execute({ command: batchCmd }); } }, - [activeProject, editor.media, editor.timeline, currentTime, zoomLevel], + [activeProject, editor.command, editor.timeline, currentTime, zoomLevel], ); const handleDrop = useCallback( |
