summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks/use-paste-media.ts
blob: 4533ab285181838377c4033d09bd3a3f135c9c1b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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]);
}