0.5.2: paste images, shared import helper, clipart persistence

- Clipboard paste creates an image layer: a document-level paste
  listener detects image items in the clipboard (Strg+V after a
  screenshot, e.g.) and imports them via the shared helper. Skipped
  when focus is in a real text input / textarea / contenteditable.
- Shared importImageFile() helper in fileLoader.ts replaces the
  duplicated image-import logic in AssetSidebar and CanvasStage's
  clipart-drop path. Optional position argument for drop-at-point.
- Clipart folder persistence hardened for reloads:
  - initialize() is now safely re-runnable (no-ops only once a handle
    is actually loaded, tolerates refresh failures so the folder name
    survives even if permission isn't auto-granted)
  - App.vue's onMounted kicks off initialize() so it runs regardless
    of whether the drawer component has mounted
  - DocumentSettings re-invokes initialize() each time the modal
    opens — covers app-start races and gives the user gesture needed
    for FS Access permission prompts
  - Opening the drawer refreshes thumbnails when handle is set but
    items are empty, so the user-gesture context restores permission

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 14:05:48 +02:00
1 parent cbc309bc8c
commit 07b0ef2659
8 files changed
+126 -65

No files matched your search

+38
View File
@@ -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<string | null>(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)
})
</script>