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:
1 parent
cbc309bc8c
commit
07b0ef2659
8 files changed
+126
-65
No files matched your search
+38
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user