diff --git a/package.json b/package.json index 41cc20b..0a16b3f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "animatr", "private": true, - "version": "0.9.1", + "version": "0.10.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index 8d6f616..c49baf2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -34,6 +34,7 @@ import ruwioLogo from './assets/logo.png' import { confirmDialog, DialogsHost, IconButton, promptDialog, Toast } from './components/ui' import { t } from './i18n' import { useHistory } from './composables/useHistory' +import { useAudioRuntime } from './composables/useAudioRuntime' import { usePlaybackLoop } from './composables/usePlaybackLoop' import { ProjectFileError, @@ -140,6 +141,7 @@ const currentProjectName = ref(null) const isDirty = ref(false) usePlaybackLoop() +useAudioRuntime() const store = useSceneStore() const clipart = useClipartStore() const history = useHistory() diff --git a/src/components/AssetSidebar.vue b/src/components/AssetSidebar.vue index 14096ec..e3800ff 100644 --- a/src/components/AssetSidebar.vue +++ b/src/components/AssetSidebar.vue @@ -7,6 +7,7 @@ import { PhImage, PhImageSquare, PhListBullets, + PhMusicNote, PhPencilSimple, PhPencilSimpleLine, PhPlus, @@ -18,7 +19,7 @@ import { } from '@phosphor-icons/vue' import { useSceneStore } from '@/stores/scene' import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref' -import { importImageFile } from '@/lib/fileLoader' +import { importAudioFile, importImageFile, isAudioFile } from '@/lib/fileLoader' import type { Asset } from '@/types/scene' import { confirmDialog, promptDialog } from '@/components/ui' import { loadSceneImages, renderButtonThumbnail } from '@/lib/render' @@ -245,9 +246,57 @@ async function handleFileSelected(event: Event) { } async function importFile(file: File) { + if (isAudioFile(file)) { + await importAudioFile(store, file) + return + } await importImageFile(store, file) } +// ---- OS-file drop target ---- + +const dropHot = ref(false) +// The browser fires dragleave on child transitions as well — debounce +// by tracking enter depth so the outline doesn't flicker mid-drop. +let dragDepth = 0 + +function panelAcceptsDrop(e: DragEvent): boolean { + if (store.activeButtonEditMode === 'hit') return false + const dt = e.dataTransfer + if (!dt) return false + // Only activate for real filesystem drops — internal Animatr DnD + // (clips, audio assets, layer references) has its own handlers. + return Array.from(dt.types).includes('Files') +} + +function onPanelDragOver(e: DragEvent) { + if (!panelAcceptsDrop(e)) return + e.preventDefault() + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy' + dragDepth++ + dropHot.value = true +} + +function onPanelDragLeave() { + dragDepth = Math.max(0, dragDepth - 1) + if (dragDepth === 0) dropHot.value = false +} + +async function onPanelDrop(e: DragEvent) { + if (!panelAcceptsDrop(e)) return + e.preventDefault() + dragDepth = 0 + dropHot.value = false + const files = Array.from(e.dataTransfer?.files ?? []) + if (!files.length) return + uploadError.value = null + try { + for (const file of files) await importFile(file) + } catch (err) { + uploadError.value = err instanceof Error ? err.message : String(err) + } +} + function triggerUpload() { fileInput.value?.click() } @@ -259,12 +308,39 @@ const assetIconMap: Record = { text: PhTextT, polygon: PhPolygon, button: PhCursorClick, + audio: PhMusicNote, } const imageAssets = computed(() => store.scene.assets.filter((a): a is Extract => a.kind === 'image'), ) +const audioAssets = computed(() => + store.scene.assets.filter((a): a is Extract => a.kind === 'audio'), +) + +function onAudioAssetDragStart(e: DragEvent, asset: Extract) { + if (!e.dataTransfer) return + e.dataTransfer.effectAllowed = 'copy' + // Matches Timeline's AUDIO_CLIP_DND_MIME — dropping on an audio lane + // creates a new AudioClip referencing this asset. + e.dataTransfer.setData('application/x-animatr-audio-asset', asset.id) + e.dataTransfer.setData('text/plain', asset.name) +} + +async function deleteAudioAsset(id: string) { + const asset = audioAssets.value.find((a) => a.id === id) + if (!asset) return + const ok = await confirmDialog({ + title: t('sidebar.deleteAudioDialog.title'), + message: t('sidebar.deleteAudioDialog.message', { name: asset.name }), + confirmLabel: t('sidebar.deleteAudioDialog.confirm'), + variant: 'danger', + }) + if (!ok) return + store.removeAsset(id) +} + function onAssetDragStart(e: DragEvent, asset: Extract) { if (!e.dataTransfer) return e.dataTransfer.effectAllowed = 'copy' @@ -301,8 +377,11 @@ function onAssetDragStart(e: DragEvent, asset: Extract @@ -500,6 +610,11 @@ function onAssetDragStart(e: DragEvent, asset: Extract filter: saturate(0.5); } +.panel.drop-hot { + outline: 2px dashed var(--accent); + outline-offset: -6px; +} + h3 { margin: 0 0 2px; font-size: 13px; diff --git a/src/components/ClipartDrawer.vue b/src/components/ClipartDrawer.vue index 55c87e6..c92bbaa 100644 --- a/src/components/ClipartDrawer.vue +++ b/src/components/ClipartDrawer.vue @@ -1,6 +1,6 @@