0.10.1: audio clip push-cascade + track-swap ghost, Media drawer rename

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-22 12:52:47 +02:00
1 parent 04f867547d
commit c5b1dd566a
8 files changed
+538 -43

No files matched your search

+55 -4
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from 'vue'
import { PhArrowClockwise, PhCaretLeft, PhCaretRight, PhImagesSquare, PhMusicNote } from '@phosphor-icons/vue'
import { PhArrowClockwise, PhCaretLeft, PhCaretRight, PhFolderOpen, PhImagesSquare, PhMusicNote } from '@phosphor-icons/vue'
import { useClipartStore } from '@/stores/clipart'
import { readAudioDuration, readAsDataUrl } from '@/lib/fileLoader'
import { clearAudioDragSource, setAudioDragSource } from '@/composables/useAudioDragSource'
import { t } from '@/i18n'
const clipart = useClipartStore()
@@ -22,6 +24,22 @@ function onItemDragStart(e: DragEvent, index: number) {
e.dataTransfer.setData('application/x-animatr-clipart', String(index))
e.dataTransfer.setData('text/plain', item.name)
if (item.kind === 'audio') {
// Populate the shared drag-source so Timeline can render a ghost
// preview on hover. We don't know the audio duration until the
// file decodes, so seed with 0 (ghost renders at minimum width)
// and upgrade asynchronously. dragend clears both.
setAudioDragSource({ duration: 0, name: item.name })
const el = e.currentTarget as HTMLElement
el.addEventListener('dragend', clearAudioDragSource, { once: true })
void (async () => {
try {
const dataUrl = await readAsDataUrl(item.file)
const d = await readAudioDuration(dataUrl)
setAudioDragSource({ duration: d, name: item.name })
} catch {
/* leave the placeholder width — still a visible hint */
}
})()
// No image ghost for audio — browsers render the row itself as the
// default drag preview, which is what we want.
return
@@ -75,9 +93,13 @@ function onItemDragStart(e: DragEvent, index: number) {
<p v-if="!clipart.supported" class="empty-state">
{{ t('clipart.unsupported') }}
</p>
<p v-else-if="!clipart.handle" class="empty-state">
{{ t('clipart.empty') }}
</p>
<div v-else-if="!clipart.handle" class="empty-state empty-state-action">
<p>{{ t('clipart.empty') }}</p>
<button class="choose-folder-btn" @click="clipart.pickFolder()">
<PhFolderOpen :size="14" />
<span>{{ t('clipart.chooseFolder') }}</span>
</button>
</div>
<p v-else-if="clipart.error" class="error-state">{{ clipart.error }}</p>
<p v-else-if="clipart.loading" class="empty-state">{{ t('clipart.loading') }}</p>
<p v-else-if="clipart.items.length === 0" class="empty-state">
@@ -263,6 +285,35 @@ function onItemDragStart(e: DragEvent, index: number) {
font-style: italic;
}
.empty-state-action {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.empty-state-action p {
margin: 0;
}
.choose-folder-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--text);
border-radius: var(--radius-sm);
font-size: 12px;
cursor: pointer;
font-style: normal;
}
.choose-folder-btn:hover {
background: color-mix(in srgb, var(--accent) 22%, transparent);
}
.error-state {
margin: 0;
padding: 16px 14px;