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:
1 parent
04f867547d
commit
c5b1dd566a
8 files changed
+538
-43
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user