Versionen je Thema
Ein Thema hat Versionen storage/<Thema>/v<N>/ mit eigenem Lauf, Buch und Leseposition; bestehende Ablagen wandern beim Start nach v1/. Intern ist „Thema/vN“ der Schlüssel aller Zustandsfunktionen. API: ?v=N an allen Themen-Endpunkten, POST /version, DELETE ?v=N. Oberfläche: Versionsliste unter dem Thema (Klick auf Thema → Lauf der neuesten, auf Version → Buch), Knopf „Neue Version“ mit gleichen Eingaben. make loeschen kennt V=<Nr>. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import Guide from './components/Guide.vue'
|
||||
const params = new URLSearchParams(location.search)
|
||||
const topics = ref([])
|
||||
const topic = ref(params.get('topic') || localStorage.getItem('topic') || '')
|
||||
const version = ref(Number(params.get('v')) || null) // null = neueste Version
|
||||
const tab = ref((params.get('tab') || localStorage.getItem('tab')) === 'buch' ? 'buch' : 'lauf')
|
||||
const theme = ref(localStorage.getItem('theme') || 'papier')
|
||||
const state = ref(null)
|
||||
@@ -30,14 +31,24 @@ themeAnwenden()
|
||||
function urlSchreiben(hashBehalten) {
|
||||
const p = new URLSearchParams()
|
||||
if (topic.value) p.set('topic', topic.value)
|
||||
if (version.value) p.set('v', version.value)
|
||||
p.set('tab', tab.value)
|
||||
history.replaceState(null, '', location.pathname + '?' + p + (hashBehalten ? location.hash : ''))
|
||||
}
|
||||
watch(topic, (t) => { localStorage.setItem('topic', t); urlSchreiben(false) })
|
||||
watch(version, () => urlSchreiben(false))
|
||||
watch(tab, (t) => { localStorage.setItem('tab', t); urlSchreiben(true) })
|
||||
|
||||
const aktuell = computed(() => topics.value.find((t) => t.name === topic.value))
|
||||
watch([aktuell, tab], ([a, t]) => { if (t !== 'buch') document.title = a ? `${a.titel} – buchwerk` : 'buchwerk' }, { immediate: true })
|
||||
const versionen = computed(() => aktuell.value?.versionen || [])
|
||||
const neueste = computed(() => versionen.value.length ? versionen.value[versionen.value.length - 1].v : null)
|
||||
const versionAktiv = computed(() => version.value || neueste.value)
|
||||
watch([aktuell, tab, versionAktiv], ([a, t, v]) => {
|
||||
if (t !== 'buch') document.title = a ? `${a.titel}${versionen.value.length > 1 ? ' v' + v : ''} – buchwerk` : 'buchwerk'
|
||||
}, { immediate: true })
|
||||
function datum(iso) { return iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }) : '' }
|
||||
function themaWaehlen(name) { topic.value = name; version.value = null; tab.value = 'lauf'; navAuf.value = false }
|
||||
function versionWaehlen(name, v) { topic.value = name; version.value = v; tab.value = 'buch'; navAuf.value = false }
|
||||
const laeuft = computed(() => state.value?.status === 'laeuft')
|
||||
const fortschritt = computed(() => (state.value?.stufen || []).reduce((n, s) => n + (s.fertig || 0), 0))
|
||||
|
||||
@@ -46,6 +57,7 @@ async function topicsLaden() {
|
||||
topics.value = await api.topics()
|
||||
fehler.value = ''
|
||||
if (topic.value && !topics.value.some((t) => t.name === topic.value)) topic.value = ''
|
||||
if (version.value && !versionen.value.some((x) => x.v === version.value)) version.value = null
|
||||
if (!topic.value && topics.value.length) topic.value = topics.value[0].name
|
||||
} catch (e) {
|
||||
// Server gerade nicht erreichbar (z. B. Neustart): alle 3 s erneut versuchen, bis er antwortet
|
||||
@@ -55,7 +67,7 @@ async function topicsLaden() {
|
||||
}
|
||||
async function stateLaden() {
|
||||
if (!topic.value) { state.value = null; return }
|
||||
try { state.value = await api.state(topic.value) } catch (e) { fehler.value = String(e.message || e) }
|
||||
try { state.value = await api.state(topic.value, version.value) } catch (e) { fehler.value = String(e.message || e) }
|
||||
}
|
||||
function pollen() {
|
||||
clearInterval(timer)
|
||||
@@ -65,7 +77,7 @@ function pollen() {
|
||||
if (!laeuft.value) { clearInterval(timer); timer = null; await topicsLaden() }
|
||||
}, 2000)
|
||||
}
|
||||
watch(topic, async () => { await stateLaden(); if (laeuft.value) pollen() })
|
||||
watch([topic, version], async () => { await stateLaden(); if (laeuft.value) pollen() })
|
||||
watch(laeuft, (l) => { if (l && !timer) pollen() })
|
||||
|
||||
async function anlegen() {
|
||||
@@ -76,8 +88,17 @@ async function anlegen() {
|
||||
neuName.value = ''
|
||||
neuHinweise.value = ''
|
||||
await topicsLaden()
|
||||
topic.value = name
|
||||
themaWaehlen(name)
|
||||
} catch (e) { fehler.value = String(e.message || e) }
|
||||
}
|
||||
// Neue Version: gleiche Eingaben, frischer Lauf; die alte Version bleibt samt Leseposition erhalten.
|
||||
async function neueVersion() {
|
||||
try {
|
||||
const r = await api.versionAnlegen(topic.value)
|
||||
await topicsLaden()
|
||||
version.value = r.v
|
||||
tab.value = 'lauf'
|
||||
await aktion(() => api.start(topic.value, r.v))
|
||||
} catch (e) { fehler.value = String(e.message || e) }
|
||||
}
|
||||
async function aktion(fn) {
|
||||
@@ -85,7 +106,7 @@ async function aktion(fn) {
|
||||
catch (e) { fehler.value = String(e.message || e) }
|
||||
}
|
||||
async function loeschen(name) {
|
||||
if (!confirm(`Thema „${name}“ samt Buch löschen?`)) return
|
||||
if (!confirm(`Thema „${name}“ löschen?`)) return
|
||||
await aktion(() => api.loeschen(name))
|
||||
}
|
||||
|
||||
@@ -108,12 +129,21 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
|
||||
<aside class="sidebar" :class="{ offen: navAuf }">
|
||||
<button class="schliessen sekundaer klein" @click="navAuf = false">✕ Schließen</button>
|
||||
<div class="topics">
|
||||
<div v-for="t in topics" :key="t.name" class="topic-eintrag" :class="{ aktiv: t.name === topic }"
|
||||
@click="topic = t.name; navAuf = false">
|
||||
<span class="punkt" :class="t.laeuft ? 'laeuft' : t.status"></span>
|
||||
<span style="flex: 1">{{ t.titel }}</span>
|
||||
<button v-if="t.status !== 'fertig'" class="klein sekundaer" title="Löschen" @click.stop="loeschen(t.name)">✕</button>
|
||||
</div>
|
||||
<template v-for="t in topics" :key="t.name">
|
||||
<div class="topic-eintrag" :class="{ aktiv: t.name === topic && (t.versionen.length === 1 || tab === 'lauf') }"
|
||||
@click="themaWaehlen(t.name)">
|
||||
<span class="punkt" :class="t.laeuft ? 'laeuft' : t.status"></span>
|
||||
<span style="flex: 1">{{ t.titel }}</span>
|
||||
<button v-if="t.versionen.length === 1 && t.status !== 'fertig'" class="klein sekundaer" title="Löschen" @click.stop="loeschen(t.name)">✕</button>
|
||||
</div>
|
||||
<div v-if="t.versionen.length > 1" class="versionen">
|
||||
<div v-for="x in t.versionen" :key="x.v" class="topic-eintrag version"
|
||||
:class="{ aktiv: t.name === topic && tab === 'buch' && x.v === versionAktiv }" @click="versionWaehlen(t.name, x.v)">
|
||||
<span class="punkt" :class="x.laeuft ? 'laeuft' : x.status"></span>
|
||||
<span style="flex: 1">v{{ x.v }} <span class="dim">· {{ datum(x.angelegt) }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!topics.length" style="color: var(--dim); padding: 8px">Noch kein Thema.</div>
|
||||
</div>
|
||||
<div class="neu">
|
||||
@@ -144,10 +174,11 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
|
||||
<div v-if="fehler" class="fehler" @click="fehler = ''">{{ fehler }} — klicken zum Schließen</div>
|
||||
<main class="inhalt" id="inhalt" tabindex="-1">
|
||||
<Lauf v-if="tab === 'lauf' && state" :state="state"
|
||||
@start="aktion(() => api.start(topic))"
|
||||
@pause="aktion(() => api.pause(topic))"
|
||||
@reset="aktion(() => api.reset(topic))" />
|
||||
<Guide v-else-if="tab === 'buch' && topic" :topic="topic" :laeuft="laeuft" :fortschritt="fortschritt" />
|
||||
@start="aktion(() => api.start(topic, version))"
|
||||
@pause="aktion(() => api.pause(topic, version))"
|
||||
@reset="aktion(() => api.reset(topic, version))"
|
||||
@neue-version="neueVersion" />
|
||||
<Guide v-else-if="tab === 'buch' && topic" :topic="topic" :version="version" :laeuft="laeuft" :fortschritt="fortschritt" />
|
||||
<div v-else style="padding: 24px; color: var(--dim)">Thema links anlegen oder wählen.</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user