bak
This commit is contained in:
1 parent
94e9846c13
commit
e4aa0dce86
11 files changed
+2227
-32
No files matched your search
+198
-1
@@ -2,6 +2,8 @@
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import {
|
||||
PhExport,
|
||||
PhFolderOpen,
|
||||
PhFloppyDisk,
|
||||
PhGearSix,
|
||||
PhPause,
|
||||
PhPlay,
|
||||
@@ -15,11 +17,32 @@ import DocumentSettings from './components/DocumentSettings.vue'
|
||||
import ExportDialog from './components/ExportDialog.vue'
|
||||
import PropertiesPanel from './components/PropertiesPanel.vue'
|
||||
import Timeline from './components/Timeline.vue'
|
||||
import WelcomeDialog from './components/WelcomeDialog.vue'
|
||||
import TestOverlay from './devtools/TestOverlay.vue'
|
||||
import { usePlaybackLoop } from './composables/usePlaybackLoop'
|
||||
import {
|
||||
ProjectFileError,
|
||||
downloadProject,
|
||||
pickProjectFile,
|
||||
readProjectFile,
|
||||
} from './lib/projectFile'
|
||||
import {
|
||||
getLastOpenedId,
|
||||
listRecentProjects,
|
||||
saveProjectToStore,
|
||||
setLastOpenedId,
|
||||
type StoredProject,
|
||||
} from './lib/projectStore'
|
||||
import { useSceneStore } from './stores/scene'
|
||||
|
||||
const settingsOpen = ref(false)
|
||||
const exportOpen = ref(false)
|
||||
const testsOpen = ref(false)
|
||||
const welcomeOpen = ref(false)
|
||||
const loadError = ref<string | null>(null)
|
||||
const savedToast = ref<string | null>(null)
|
||||
const currentProjectId = ref<string | null>(null)
|
||||
const currentProjectName = ref<string | null>(null)
|
||||
|
||||
usePlaybackLoop()
|
||||
const store = useSceneStore()
|
||||
@@ -32,6 +55,87 @@ function stop() {
|
||||
store.pause()
|
||||
}
|
||||
|
||||
async function saveProject() {
|
||||
const scene = JSON.parse(JSON.stringify(store.scene))
|
||||
let name = currentProjectName.value
|
||||
if (!name) {
|
||||
const suggested = `Projekt ${new Date().toLocaleDateString()}`
|
||||
const entered = prompt('Projektname?', suggested)
|
||||
if (entered === null) return
|
||||
name = entered.trim() || suggested
|
||||
}
|
||||
const rec = await saveProjectToStore(scene, {
|
||||
id: currentProjectId.value ?? undefined,
|
||||
name,
|
||||
})
|
||||
currentProjectId.value = rec.id
|
||||
currentProjectName.value = rec.name
|
||||
setLastOpenedId(rec.id)
|
||||
showSavedToast(`"${rec.name}" gespeichert`)
|
||||
}
|
||||
|
||||
function exportProjectFile() {
|
||||
const scene = JSON.parse(JSON.stringify(store.scene))
|
||||
const base = currentProjectName.value ?? 'animatr'
|
||||
const safe = base.replace(/[^a-z0-9äöüß_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'animatr'
|
||||
downloadProject(scene, `${safe}.json`)
|
||||
}
|
||||
|
||||
async function importFromFile() {
|
||||
const file = await pickProjectFile()
|
||||
if (!file) return
|
||||
const hasContent = store.scene.layers.length > 0 || store.scene.assets.length > 0
|
||||
if (hasContent) {
|
||||
const ok = confirm(
|
||||
'Aktuelles Projekt wird durch die geladene Datei ersetzt. Ungespeicherte Änderungen gehen verloren. Fortfahren?',
|
||||
)
|
||||
if (!ok) return
|
||||
}
|
||||
try {
|
||||
const scene = await readProjectFile(file)
|
||||
store.loadScene(scene)
|
||||
currentProjectId.value = null
|
||||
currentProjectName.value = file.name.replace(/\.[^.]+$/, '')
|
||||
loadError.value = null
|
||||
} catch (err) {
|
||||
const msg =
|
||||
err instanceof ProjectFileError ? err.message : (err as Error).message || String(err)
|
||||
loadError.value = msg
|
||||
setTimeout(() => {
|
||||
loadError.value = null
|
||||
}, 6000)
|
||||
}
|
||||
}
|
||||
|
||||
function openFromRecent(project: StoredProject) {
|
||||
store.loadScene(JSON.parse(JSON.stringify(project.scene)))
|
||||
currentProjectId.value = project.id
|
||||
currentProjectName.value = project.name
|
||||
setLastOpenedId(project.id)
|
||||
loadError.value = null
|
||||
}
|
||||
|
||||
function showSavedToast(msg: string) {
|
||||
savedToast.value = msg
|
||||
setTimeout(() => {
|
||||
if (savedToast.value === msg) savedToast.value = null
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
async function bootWelcome() {
|
||||
const recents = await listRecentProjects()
|
||||
if (recents.length > 0) {
|
||||
welcomeOpen.value = true
|
||||
const lastId = getLastOpenedId()
|
||||
if (lastId) {
|
||||
const last = recents.find((p) => p.id === lastId)
|
||||
if (last) {
|
||||
currentProjectName.value = last.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEditable(el: EventTarget | null): boolean {
|
||||
if (!(el instanceof HTMLElement)) return false
|
||||
const tag = el.tagName
|
||||
@@ -41,13 +145,21 @@ function isEditable(el: EventTarget | null): boolean {
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.ctrlKey && e.shiftKey && e.altKey && (e.code === 'KeyT' || e.key.toLowerCase() === 't')) {
|
||||
e.preventDefault()
|
||||
testsOpen.value = true
|
||||
return
|
||||
}
|
||||
if (e.code !== 'Space') return
|
||||
if (isEditable(e.target)) return
|
||||
e.preventDefault()
|
||||
store.togglePlay()
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', onKeyDown))
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
bootWelcome()
|
||||
})
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
|
||||
</script>
|
||||
|
||||
@@ -79,6 +191,15 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
|
||||
<span class="time">{{ store.playhead.toFixed(2) }}s / {{ store.scene.duration }}s</span>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<span v-if="currentProjectName" class="project-name" :title="currentProjectName">
|
||||
{{ currentProjectName }}
|
||||
</span>
|
||||
<button class="icon-btn" title="Zuletzt verwendete Projekte" @click="welcomeOpen = true">
|
||||
<PhFolderOpen :size="16" />
|
||||
</button>
|
||||
<button class="icon-btn" title="Projekt speichern (in Browser-Speicher)" @click="saveProject">
|
||||
<PhFloppyDisk :size="16" />
|
||||
</button>
|
||||
<button class="icon-btn" title="Document Settings" @click="settingsOpen = true">
|
||||
<PhGearSix :size="16" />
|
||||
</button>
|
||||
@@ -94,6 +215,23 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
|
||||
<footer class="timeline"><Timeline /></footer>
|
||||
<DocumentSettings v-model="settingsOpen" />
|
||||
<ExportDialog v-model="exportOpen" />
|
||||
<TestOverlay v-model="testsOpen" />
|
||||
<WelcomeDialog
|
||||
v-model="welcomeOpen"
|
||||
@open="openFromRecent"
|
||||
@new="() => { currentProjectId = null; currentProjectName = null }"
|
||||
@import="importFromFile"
|
||||
@export="exportProjectFile"
|
||||
/>
|
||||
<Transition name="fade">
|
||||
<div v-if="loadError" class="load-error" role="alert">
|
||||
<strong>Projekt konnte nicht geladen werden.</strong>
|
||||
<span>{{ loadError }}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="fade">
|
||||
<div v-if="savedToast" class="saved-toast" role="status">{{ savedToast }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -259,4 +397,63 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
|
||||
border-top: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.load-error {
|
||||
position: fixed;
|
||||
bottom: 240px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 14px;
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
border: 1px solid var(--danger);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-size: 12px;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.saved-toast {
|
||||
position: fixed;
|
||||
bottom: 240px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1500;
|
||||
padding: 8px 14px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 220px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim-strong);
|
||||
background: var(--panel-alt);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 140ms var(--ease);
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user