diff --git a/package.json b/package.json index e20d3fe..c9dfb47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "animatr", "private": true, - "version": "0.5.1", + "version": "0.5.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index feba5e2..ae6fec9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -44,6 +44,8 @@ import { setLastOpenedId, type StoredProject, } from './lib/projectStore' +import { importImageFile } from './lib/fileLoader' +import { useClipartStore } from './stores/clipart' import { useSceneStore } from './stores/scene' const settingsOpen = ref(false) @@ -131,6 +133,7 @@ const currentProjectName = ref(null) usePlaybackLoop() const store = useSceneStore() +const clipart = useClipartStore() const history = useHistory() function rewind() { @@ -278,15 +281,50 @@ function onKeyDown(e: KeyboardEvent) { store.togglePlay() } +function isTextInputTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false + if (target.isContentEditable) return true + const tag = target.tagName + if (tag === 'INPUT') { + const t = (target as HTMLInputElement).type + // Only block typed text inputs — color / range / checkbox shouldn't swallow paste + return ['text', 'search', 'email', 'number', 'tel', 'url', 'password'].includes(t) + } + if (tag === 'TEXTAREA') return true + return false +} + +async function onPaste(e: ClipboardEvent) { + if (isTextInputTarget(e.target)) return + const data = e.clipboardData + if (!data) return + const items = Array.from(data.items) + const imageItems = items.filter((it) => it.kind === 'file' && it.type.startsWith('image/')) + if (imageItems.length === 0) return + e.preventDefault() + for (const it of imageItems) { + const file = it.getAsFile() + if (!file) continue + try { + await importImageFile(store, file) + } catch { + /* ignore invalid paste payloads */ + } + } +} + onMounted(() => { document.addEventListener('keydown', onKeyDown) document.addEventListener('contextmenu', onDocumentContextMenu) + document.addEventListener('paste', onPaste) window.addEventListener('resize', onWindowResize) + void clipart.initialize() bootWelcome() }) onBeforeUnmount(() => { document.removeEventListener('keydown', onKeyDown) document.removeEventListener('contextmenu', onDocumentContextMenu) + document.removeEventListener('paste', onPaste) window.removeEventListener('resize', onWindowResize) }) diff --git a/src/components/AssetSidebar.vue b/src/components/AssetSidebar.vue index 0cd102d..3749bf7 100644 --- a/src/components/AssetSidebar.vue +++ b/src/components/AssetSidebar.vue @@ -16,7 +16,7 @@ import { PhUploadSimple, } from '@phosphor-icons/vue' import { useSceneStore } from '@/stores/scene' -import { isSvgFile, loadImage, parseSvgDimensions, readAsDataUrl } from '@/lib/fileLoader' +import { importImageFile } from '@/lib/fileLoader' import type { Asset } from '@/types/scene' import { confirmDialog, promptDialog } from '@/components/ui' import { t } from '@/i18n' @@ -161,34 +161,7 @@ async function handleFileSelected(event: Event) { } async function importFile(file: File) { - const dataUrl = await readAsDataUrl(file) - - let width = 0 - let height = 0 - - if (isSvgFile(file)) { - const text = await file.text() - const dims = parseSvgDimensions(text) - if (dims) { - width = dims.width - height = dims.height - } - } - - if (!width || !height) { - const img = await loadImage(dataUrl) - width = img.naturalWidth || 300 - height = img.naturalHeight || 300 - } - - const asset = store.addAsset({ - kind: 'image', - name: file.name, - src: dataUrl, - width, - height, - }) - store.addLayer(asset.id) + await importImageFile(store, file) } function triggerUpload() { diff --git a/src/components/CanvasStage.vue b/src/components/CanvasStage.vue index 4cafcd2..6490841 100644 --- a/src/components/CanvasStage.vue +++ b/src/components/CanvasStage.vue @@ -4,7 +4,7 @@ import type Konva from 'konva' import { PhCornersIn, PhCrop, PhMaskHappy, PhPencilSimple } from '@phosphor-icons/vue' import { useSceneStore } from '@/stores/scene' import { sampleLayer } from '@/lib/interpolate' -import { isSvgFile, loadImage, parseSvgDimensions, readAsDataUrl } from '@/lib/fileLoader' +import { importImageFile, loadImage } from '@/lib/fileLoader' import { useClipartStore } from '@/stores/clipart' import { drawSceneFrame, loadSceneImages } from '@/lib/render' import type { AnimatableProps, Asset, Layer, Point } from '@/types/scene' @@ -1834,33 +1834,7 @@ function onContainerDragOver(e: DragEvent) { } async function importClipartAt(file: File, worldX: number, worldY: number) { - const dataUrl = await readAsDataUrl(file) - let width = 0 - let height = 0 - if (isSvgFile(file)) { - const text = await file.text() - const dims = parseSvgDimensions(text) - if (dims) { - width = dims.width - height = dims.height - } - } - if (!width || !height) { - const img = await loadImage(dataUrl) - width = img.naturalWidth || 300 - height = img.naturalHeight || 300 - } - const asset = store.addAsset({ - kind: 'image', - name: file.name, - src: dataUrl, - width, - height, - }) - const layer = store.addLayer(asset.id) - if (layer) { - store.upsertKeyframe(layer.id, store.playhead, { x: worldX, y: worldY }) - } + await importImageFile(store, file, { position: { x: worldX, y: worldY } }) } function onContainerDrop(e: DragEvent) { diff --git a/src/components/DocumentSettings.vue b/src/components/DocumentSettings.vue index a4ae85a..ff61167 100644 --- a/src/components/DocumentSettings.vue +++ b/src/components/DocumentSettings.vue @@ -1,5 +1,5 @@