feat(view): import images from file or clipboard

This commit is contained in:
syntaxbullet
2026-07-03 16:19:44 +02:00
parent 44295b1b2d
commit ed4d419c4d
2 changed files with 111 additions and 0 deletions

108
view/useImageImport.tsx Normal file
View File

@@ -0,0 +1,108 @@
import { useCallback, useEffect, useRef } from "react";
import { commandIds } from "@commands/ids";
import type { AppStore } from "@editor/store";
export function useImageImport(store: AppStore) {
const inputRef = useRef<HTMLInputElement | null>(null);
const importFile = useCallback(
async (file: File) => {
if (!file.type.startsWith("image/")) return;
const source = URL.createObjectURL(file);
const intrinsicSize = await loadImageSize(source);
const state = store.getState();
const artboard = state.editor.selection.artboardId
? state.document.artboards.find((candidate) => candidate.id === state.editor.selection.artboardId)
: state.document.artboards[0];
if (!artboard) {
URL.revokeObjectURL(source);
return;
}
const assetId = crypto.randomUUID();
const layerId = crypto.randomUUID();
const center = state.editor.viewport.center;
store.dispatch(commandIds.documentAddAsset, {
asset: {
id: assetId,
name: file.name,
mimeType: file.type,
source,
intrinsicSize,
},
});
store.dispatch(commandIds.documentAddImageLayer, {
artboardId: artboard.id,
layer: {
id: layerId,
type: "image",
name: file.name,
visible: true,
locked: false,
opacity: 1,
assetId,
transform: {
position: { x: center.x - intrinsicSize.w / 2, y: center.y - intrinsicSize.h / 2 },
scale: { x: 1, y: 1 },
rotation: 0,
},
},
});
store.dispatch(commandIds.selectionSet, { artboardId: artboard.id, layerIds: [layerId] });
},
[store],
);
const openFilePicker = useCallback(() => inputRef.current?.click(), []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented || event.key.toLowerCase() !== "o" || (!event.metaKey && !event.ctrlKey)) return;
event.preventDefault();
openFilePicker();
};
const handlePaste = (event: ClipboardEvent) => {
const file = [...(event.clipboardData?.files ?? [])].find((candidate) => candidate.type.startsWith("image/"));
if (!file) return;
event.preventDefault();
void importFile(file);
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("paste", handlePaste);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("paste", handlePaste);
};
}, [importFile, openFilePicker]);
const input = (
<input
ref={inputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(event) => {
const file = event.currentTarget.files?.[0];
event.currentTarget.value = "";
if (file) void importFile(file);
}}
/>
);
return { input, openFilePicker, importFile };
}
function loadImageSize(source: string): Promise<{ w: number; h: number }> {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve({ w: image.naturalWidth, h: image.naturalHeight });
image.onerror = () => reject(new Error("Failed to load image"));
image.src = source;
});
}