This commit is contained in:
th committed 2026-04-19 20:31:53 +02:00
1 parent e4aa0dce86
commit cc6b11e61d
23 files changed
+2047 -524

No files matched your search

+95 -168
View File
@@ -1,19 +1,19 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, ref } from 'vue'
import {
PhCode,
PhDownloadSimple,
PhEngine,
PhFileHtml,
PhFilmSlate,
PhX,
} from '@phosphor-icons/vue'
import { useSceneStore } from '@/stores/scene'
import { downloadExampleHtml, downloadRuntime, downloadSceneExport } from '@/lib/exportPlayer'
import { downloadSceneAsVideo, pickMimeType } from '@/lib/videoExport'
import { downloadFromServer } from '@/lib/serverExport'
import { Modal } from '@/components/ui'
const props = defineProps<{ modelValue: boolean }>()
defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const store = useSceneStore()
@@ -34,17 +34,17 @@ function sceneClone() {
function exportScene() {
downloadSceneExport(sceneClone())
close()
emit('update:modelValue', false)
}
function exportRuntime() {
downloadRuntime()
close()
emit('update:modelValue', false)
}
function exportHtml() {
downloadExampleHtml(sceneClone())
close()
emit('update:modelValue', false)
}
async function exportWebm() {
@@ -111,174 +111,110 @@ function dismissError() {
progress.value = null
}
const isLocked = () => !!progress.value && !progress.value.error
const isLocked = computed(() => !!progress.value && !progress.value.error)
function close() {
if (isLocked()) return
emit('update:modelValue', false)
function onUpdate(v: boolean) {
if (!v && isLocked.value) return
emit('update:modelValue', v)
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape' && props.modelValue) close()
}
onMounted(() => document.addEventListener('keydown', onKeyDown))
onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
</script>
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="modelValue" class="backdrop" @mousedown.self="close">
<div class="dialog" role="dialog" aria-modal="true">
<header class="dialog-header">
<h2>Export</h2>
<button class="close-btn" :disabled="isLocked()" title="Schließen" @click="close">
<PhX :size="14" weight="bold" />
</button>
</header>
<div v-if="!progress" class="options">
<button class="opt" @click="exportRuntime">
<div class="opt-icon"><PhEngine :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
animatr-runtime.js
<span class="badge">1× pro Seite</span>
</div>
<div class="opt-desc">Engine — einmal einbinden, beliebig viele Szenen teilen sich die Runtime</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<button class="opt" @click="exportScene">
<div class="opt-icon"><PhCode :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
animation.js
<span class="badge">Szene</span>
</div>
<div class="opt-desc">Nur Szenen-Daten — benötigt geladene Runtime</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<button class="opt" @click="exportHtml">
<div class="opt-icon"><PhFileHtml :size="20" /></div>
<div class="opt-text">
<div class="opt-title">Beispiel HTML</div>
<div class="opt-desc">Standalone HTML — Browser öffnen und fertig</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<button class="opt" @click="exportMp4Server">
<div class="opt-icon"><PhFilmSlate :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
MP4 Video
<span class="badge accent">Server · Frame-exakt</span>
</div>
<div class="opt-desc">H.264, beliebige Auflösung — benötigt Render-Server</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<button class="opt" :disabled="!videoSupported" @click="exportWebm">
<div class="opt-icon"><PhFilmSlate :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
WebM Video
<span class="badge">Browser · Realtime</span>
</div>
<div class="opt-desc">
{{ videoSupported ? 'Ohne Server — Aufnahme in Echtzeit' : 'Browser unterstützt MediaRecorder nicht' }}
</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<Modal
:model-value="modelValue"
title="Export"
size="md"
:dismissable="!isLocked"
:close-on-backdrop="!isLocked"
:close-on-esc="!isLocked"
@update:model-value="onUpdate"
>
<div v-if="!progress" class="options">
<button class="opt" @click="exportRuntime">
<div class="opt-icon"><PhEngine :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
animatr-runtime.js
<span class="badge">1× pro Seite</span>
</div>
<div class="opt-desc">Engine — einmal einbinden, beliebig viele Szenen teilen sich die Runtime</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<div v-else-if="!progress.error" class="rendering">
<div class="r-label">
<PhFilmSlate :size="14" />
<span class="r-text">{{ progress.label }}</span>
<span class="r-percent">{{ Math.round(progress.progress * 100) }}%</span>
</div>
<div class="r-bar">
<div class="r-bar-fill" :style="{ width: progress.progress * 100 + '%' }"></div>
</div>
<div class="r-actions">
<button class="secondary" @click="cancel">Abbrechen</button>
</div>
<button class="opt" @click="exportScene">
<div class="opt-icon"><PhCode :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
animation.js
<span class="badge">Szene</span>
</div>
<div class="opt-desc">Nur Szenen-Daten — benötigt geladene Runtime</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<div v-else class="error-view">
<div class="error-title">Export fehlgeschlagen</div>
<div class="error-msg">{{ progress.error }}</div>
<div class="error-actions">
<button class="primary" @click="dismissError">Zurück</button>
</div>
<button class="opt" @click="exportHtml">
<div class="opt-icon"><PhFileHtml :size="20" /></div>
<div class="opt-text">
<div class="opt-title">Beispiel HTML</div>
<div class="opt-desc">Standalone HTML — Browser öffnen und fertig</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<button class="opt" @click="exportMp4Server">
<div class="opt-icon"><PhFilmSlate :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
MP4 Video
<span class="badge accent">Server · Frame-exakt</span>
</div>
<div class="opt-desc">H.264, beliebige Auflösung — benötigt Render-Server</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
<button class="opt" :disabled="!videoSupported" @click="exportWebm">
<div class="opt-icon"><PhFilmSlate :size="20" /></div>
<div class="opt-text">
<div class="opt-title">
WebM Video
<span class="badge">Browser · Realtime</span>
</div>
<div class="opt-desc">
{{ videoSupported ? 'Ohne Server — Aufnahme in Echtzeit' : 'Browser unterstützt MediaRecorder nicht' }}
</div>
</div>
<PhDownloadSimple :size="14" class="opt-arrow" />
</button>
</div>
<div v-else-if="!progress.error" class="rendering">
<div class="r-label">
<PhFilmSlate :size="14" />
<span class="r-text">{{ progress.label }}</span>
<span class="r-percent">{{ Math.round(progress.progress * 100) }}%</span>
</div>
</Transition>
</Teleport>
<div class="r-bar">
<div class="r-bar-fill" :style="{ width: progress.progress * 100 + '%' }"></div>
</div>
<div class="r-actions">
<button class="secondary" @click="cancel">Abbrechen</button>
</div>
</div>
<div v-else class="error-view">
<div class="error-title">Export fehlgeschlagen</div>
<div class="error-msg">{{ progress.error }}</div>
<div class="error-actions">
<button class="primary" @click="dismissError">Zurück</button>
</div>
</div>
</Modal>
</template>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 900;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.dialog {
width: min(520px, calc(100% - 32px));
background: var(--panel);
border: 1px solid var(--border-strong);
border-radius: 10px;
box-shadow: var(--shadow-md);
display: flex;
flex-direction: column;
max-height: calc(100vh - 64px);
overflow: hidden;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.dialog-header h2 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.close-btn {
width: 26px;
height: 26px;
padding: 0;
justify-content: center;
background: transparent;
border: 1px solid transparent;
color: var(--text-dim);
}
.close-btn:hover:not(:disabled) {
background: var(--panel-alt);
color: var(--text);
}
.options {
display: flex;
flex-direction: column;
@@ -466,13 +402,4 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
.error-actions .primary:hover:not(:disabled) {
background: var(--accent-soft-strong);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 140ms var(--ease);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>