mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
* 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
100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
import { useEffect } from "react";
|
|
import { toast } from "sonner";
|
|
import { useEditor } from "@/hooks/use-editor";
|
|
import { processMediaAssets } from "@/lib/media/processing";
|
|
import { buildElementFromMedia } from "@/lib/timeline/element-utils";
|
|
import { AddMediaAssetCommand } from "@/lib/commands/media";
|
|
import { InsertElementCommand } from "@/lib/commands/timeline";
|
|
import { BatchCommand } from "@/lib/commands";
|
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|
import { isTypableDOMElement } from "@/utils/browser";
|
|
import type { MediaType } from "@/types/assets";
|
|
|
|
const MEDIA_MIME_PREFIXES: MediaType[] = ["image", "video", "audio"];
|
|
|
|
function isMediaMimeType({ type }: { type: string }): boolean {
|
|
return MEDIA_MIME_PREFIXES.some((prefix) => type.startsWith(`${prefix}/`));
|
|
}
|
|
|
|
function extractMediaFilesFromClipboard({
|
|
clipboardData,
|
|
}: {
|
|
clipboardData: DataTransfer | null;
|
|
}): File[] {
|
|
if (!clipboardData?.items) return [];
|
|
|
|
const files: File[] = [];
|
|
for (const item of clipboardData.items) {
|
|
if (item.kind !== "file") continue;
|
|
if (!isMediaMimeType({ type: item.type })) continue;
|
|
|
|
const file = item.getAsFile();
|
|
if (file) files.push(file);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export function usePasteMedia() {
|
|
const editor = useEditor();
|
|
|
|
useEffect(() => {
|
|
const handlePaste = async (event: ClipboardEvent) => {
|
|
const activeElement = document.activeElement as HTMLElement;
|
|
if (activeElement && isTypableDOMElement({ element: activeElement })) {
|
|
return;
|
|
}
|
|
|
|
const files = extractMediaFilesFromClipboard({
|
|
clipboardData: event.clipboardData,
|
|
});
|
|
if (files.length === 0) return;
|
|
|
|
event.preventDefault();
|
|
|
|
const activeProject = editor.project.getActive();
|
|
if (!activeProject) return;
|
|
|
|
try {
|
|
const processedAssets = await processMediaAssets({ files });
|
|
const startTime = editor.playback.getCurrentTime();
|
|
|
|
for (const asset of processedAssets) {
|
|
const addMediaCmd = new AddMediaAssetCommand(
|
|
activeProject.metadata.id,
|
|
asset,
|
|
);
|
|
const assetId = addMediaCmd.getAssetId();
|
|
const duration =
|
|
asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
|
|
const trackType = asset.type === "audio" ? "audio" : "video";
|
|
|
|
const element = buildElementFromMedia({
|
|
mediaId: assetId,
|
|
mediaType: asset.type,
|
|
name: asset.name,
|
|
duration,
|
|
startTime,
|
|
buffer:
|
|
asset.type === "audio"
|
|
? new AudioBuffer({ length: 1, sampleRate: 44100 })
|
|
: undefined,
|
|
});
|
|
|
|
const insertCmd = new InsertElementCommand({
|
|
element,
|
|
placement: { mode: "auto", trackType },
|
|
});
|
|
const batchCmd = new BatchCommand([addMediaCmd, insertCmd]);
|
|
editor.command.execute({ command: batchCmd });
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to paste media:", error);
|
|
toast.error("Failed to paste media");
|
|
}
|
|
};
|
|
|
|
window.addEventListener("paste", handlePaste);
|
|
return () => window.removeEventListener("paste", handlePaste);
|
|
}, [editor]);
|
|
}
|