This commit is contained in:
th committed 2026-04-19 22:32:06 +02:00
1 parent fcefd258eb
commit 6b6157d786
23 files changed
+1140 -237

No files matched your search

+63 -74
View File
@@ -16,6 +16,8 @@ import {
import { useSceneStore } from '@/stores/scene'
import { isSvgFile, loadImage, parseSvgDimensions, readAsDataUrl } from '@/lib/fileLoader'
import type { Asset } from '@/types/scene'
import { confirmDialog, promptDialog } from '@/components/ui'
import { t } from '@/i18n'
const store = useSceneStore()
@@ -54,10 +56,15 @@ function addSampleCircle() {
store.addLayer(asset.id)
}
function addSampleText() {
const input = prompt('Text?', 'Hallo')
async function addSampleText() {
const input = await promptDialog({
title: t('sidebar.newTextDialog.title'),
message: t('sidebar.newTextDialog.message'),
defaultValue: t('sidebar.newTextDialog.default'),
confirmLabel: t('sidebar.newTextDialog.confirm'),
})
if (input === null) return
const text = input.trim() || 'Text'
const text = input.trim() || t('sidebar.newTextDialog.fallback')
const asset = store.addAsset({
kind: 'text',
name: text.length > 24 ? text.slice(0, 24) + '…' : text,
@@ -70,20 +77,35 @@ function addSampleText() {
const clips = computed(() => store.scene.clips ?? [])
function addClip() {
const input = prompt('Name des Clips?', 'Clip')
async function addClip() {
const input = await promptDialog({
title: t('sidebar.newClipDialog.title'),
message: t('sidebar.newClipDialog.message'),
defaultValue: t('sidebar.newClipDialog.default'),
confirmLabel: t('sidebar.newClipDialog.confirm'),
})
if (input === null) return
store.addClip(input)
}
function renameClipPrompt(clipId: string, current: string) {
const input = prompt('Neuer Name?', current)
async function renameClipPrompt(clipId: string, current: string) {
const input = await promptDialog({
title: t('sidebar.renameClipDialog.title'),
defaultValue: current,
confirmLabel: t('sidebar.renameClipDialog.confirm'),
})
if (input === null) return
store.renameClip(clipId, input)
}
function deleteClip(clipId: string) {
if (!confirm('Clip und alle Instanzen wirklich löschen?')) return
async function deleteClip(clipId: string) {
const ok = await confirmDialog({
title: t('sidebar.deleteClipDialog.title'),
message: t('sidebar.deleteClipDialog.message'),
confirmLabel: t('sidebar.deleteClipDialog.confirm'),
variant: 'danger',
})
if (!ok) return
store.removeClip(clipId)
}
@@ -149,34 +171,17 @@ const assetIconMap: Record<Asset['kind'], typeof PhSquare> = {
text: PhTextT,
}
const assetsWithUsage = computed(() =>
store.scene.assets.map((a) => ({
asset: a,
inUse: store.scene.layers.some((l) => l.assetId === a.id),
})),
const imageAssets = computed(() =>
store.scene.assets.filter((a): a is Extract<Asset, { kind: 'image' }> => a.kind === 'image'),
)
const THUMB_SIZE = 40
function rectPreviewStyle(asset: Extract<Asset, { kind: 'rect' }>) {
const scale = Math.min(THUMB_SIZE / asset.width, THUMB_SIZE / asset.height) * 0.9
return {
width: `${Math.max(6, asset.width * scale)}px`,
height: `${Math.max(6, asset.height * scale)}px`,
background: asset.fill,
borderRadius: '2px',
} as const
function onAssetDragStart(e: DragEvent, assetId: string) {
if (!e.dataTransfer) return
e.dataTransfer.effectAllowed = 'copy'
e.dataTransfer.setData('application/x-animatr-asset', assetId)
e.dataTransfer.setData('text/plain', assetId)
}
function circlePreviewStyle(asset: Extract<Asset, { kind: 'circle' }>) {
const d = Math.min(THUMB_SIZE, asset.radius * 2) * 0.9
return {
width: `${d}px`,
height: `${d}px`,
background: asset.fill,
borderRadius: '50%',
} as const
}
</script>
<template>
@@ -185,21 +190,21 @@ function circlePreviewStyle(asset: Extract<Asset, { kind: 'circle' }>) {
<button class="upload-btn" @click="triggerUpload">
<PhUploadSimple :size="16" />
<span>SVG / Bild hinzufügen</span>
<span>{{ t('sidebar.addFile') }}</span>
</button>
<div class="actions">
<button class="shape-btn" @click="addSampleRect">
<PhSquare :size="14" />
Rect
{{ t('sidebar.shape.rect') }}
</button>
<button class="shape-btn" @click="addSampleCircle">
<PhCircle :size="14" />
Circle
{{ t('sidebar.shape.circle') }}
</button>
<button class="shape-btn" @click="addSampleText">
<PhTextT :size="14" />
Text
{{ t('sidebar.shape.text') }}
</button>
</div>
@@ -214,10 +219,10 @@ function circlePreviewStyle(asset: Extract<Asset, { kind: 'circle' }>) {
<p v-if="uploadError" class="error">{{ uploadError }}</p>
<div class="lib-header">
<h4>Bibliothek</h4>
<h4>{{ t('sidebar.library') }}</h4>
<button
class="thumb-toggle"
:title="showThumbs ? 'Thumbnails ausblenden' : 'Thumbnails anzeigen'"
:title="showThumbs ? t('sidebar.thumbnailsHide') : t('sidebar.thumbnailsShow')"
@click="showThumbs = !showThumbs"
>
<PhImageSquare v-if="showThumbs" :size="13" />
@@ -225,54 +230,38 @@ function circlePreviewStyle(asset: Extract<Asset, { kind: 'circle' }>) {
</button>
</div>
<ul class="assets" :class="{ compact: !showThumbs }">
<li v-for="item in assetsWithUsage" :key="item.asset.id" :class="{ unused: !item.inUse }">
<li
v-for="asset in imageAssets"
:key="asset.id"
draggable="true"
@dragstart="onAssetDragStart($event, asset.id)"
>
<template v-if="showThumbs">
<div class="thumb">
<img
v-if="item.asset.kind === 'image'"
:src="item.asset.src"
:alt="item.asset.name"
draggable="false"
/>
<div
v-else-if="item.asset.kind === 'rect'"
class="shape-preview"
:style="rectPreviewStyle(item.asset)"
/>
<div
v-else-if="item.asset.kind === 'circle'"
class="shape-preview"
:style="circlePreviewStyle(item.asset)"
/>
<span
v-else-if="item.asset.kind === 'text'"
class="text-preview"
:style="{ color: item.asset.fill }"
>Aa</span>
<img :src="asset.src" :alt="asset.name" draggable="false" />
</div>
</template>
<template v-else>
<component :is="assetIconMap[item.asset.kind]" :size="14" class="asset-icon" />
<component :is="assetIconMap[asset.kind]" :size="14" class="asset-icon" />
</template>
<div class="meta">
<span class="name" :title="item.asset.name">{{ item.asset.name }}</span>
<span v-if="showThumbs" class="sub">{{ item.asset.kind }}</span>
<span class="name" :title="asset.name">{{ asset.name }}</span>
<span v-if="showThumbs" class="sub">{{ t('sidebar.imageLabel') }}</span>
</div>
<button
v-if="!item.inUse"
class="add-layer"
title="Als Layer einfügen"
@click="store.addLayer(item.asset.id)"
:title="t('sidebar.addLayerTooltip')"
@click="store.addLayer(asset.id)"
>
<PhPlus :size="12" weight="bold" />
</button>
</li>
<li v-if="!store.scene.assets.length" class="empty">keine Assets</li>
<li v-if="!imageAssets.length" class="empty">{{ t('sidebar.noImages') }}</li>
</ul>
<div class="lib-header clips-header">
<h4>Clips</h4>
<button class="thumb-toggle" title="Neuen Clip anlegen" @click="addClip">
<h4>{{ t('sidebar.clips') }}</h4>
<button class="thumb-toggle" :title="t('sidebar.createClipTooltip')" @click="addClip">
<PhPlus :size="12" weight="bold" />
</button>
</div>
@@ -292,23 +281,23 @@ function circlePreviewStyle(asset: Extract<Asset, { kind: 'circle' }>) {
<div class="clip-meta">
<span class="clip-name" :title="clip.name">{{ clip.name }}</span>
<span class="clip-sub">
{{ clip.layers.length }} Layer · {{ clip.duration.toFixed(1) }}s
{{ t('sidebar.clipLayerCount', { n: clip.layers.length }) }} · {{ clip.duration.toFixed(1) }}s
</span>
</div>
<div class="clip-actions">
<button
class="clip-btn"
title="Umbenennen"
:title="t('sidebar.renameTooltip')"
@click.stop="renameClipPrompt(clip.id, clip.name)"
>
<PhPencilSimple :size="11" />
</button>
<button class="clip-btn danger" title="Löschen" @click.stop="deleteClip(clip.id)">
<button class="clip-btn danger" :title="t('sidebar.deleteTooltip')" @click.stop="deleteClip(clip.id)">
<PhTrash :size="11" />
</button>
</div>
</li>
<li v-if="!clips.length" class="empty">keine Clips</li>
<li v-if="!clips.length" class="empty">{{ t('sidebar.noClips') }}</li>
</ul>
</div>
</template>