diff options
Diffstat (limited to 'apps/web/src/hooks/use-paste-media.ts')
| -rw-r--r-- | apps/web/src/hooks/use-paste-media.ts | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/apps/web/src/hooks/use-paste-media.ts b/apps/web/src/hooks/use-paste-media.ts new file mode 100644 index 00000000..4533ab28 --- /dev/null +++ b/apps/web/src/hooks/use-paste-media.ts @@ -0,0 +1,99 @@ +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]); +} |
