init
This commit is contained in:
174
frontend/src/components/Guide.vue
Normal file
174
frontend/src/components/Guide.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { api } from '../api.js'
|
||||
import { render, zeit } from '../markdown.js'
|
||||
import Lesemodus from './Lesemodus.vue'
|
||||
|
||||
const props = defineProps({ topic: String, laeuft: Boolean, fortschritt: Number })
|
||||
const daten = ref(null)
|
||||
const lesemodus = ref(false)
|
||||
const navAuf = ref(false)
|
||||
// Wischgeste (nur Buchansicht, nicht im Lesemodus): nach rechts öffnet das Inhaltsverzeichnis, nach links schließt es
|
||||
let wisch = null
|
||||
function wischStart(e) {
|
||||
const t = e.touches[0]
|
||||
wisch = { x: t.clientX, y: t.clientY, t: Date.now() }
|
||||
}
|
||||
function wischEnde(e) {
|
||||
if (!wisch || lesemodus.value || window.innerWidth > 800) return
|
||||
const t = e.changedTouches[0]
|
||||
const dx = t.clientX - wisch.x, dy = t.clientY - wisch.y, dauer = Date.now() - wisch.t
|
||||
wisch = null
|
||||
if (dauer > 800 || Math.abs(dx) < 60 || Math.abs(dy) > Math.abs(dx) * 0.6) return
|
||||
navAuf.value = dx > 0
|
||||
}
|
||||
const aktiv = ref('')
|
||||
const lese = ref(null)
|
||||
let beobachter = null
|
||||
let ladeSeq = 0
|
||||
const cache = new Map() // Schlüssel: Abschnitt + Textlänge → unveränderte Abschnitte werden nicht neu gerendert
|
||||
|
||||
onUnmounted(() => { beobachter?.disconnect() })
|
||||
|
||||
async function laden() {
|
||||
if (!props.topic) { daten.value = null; return }
|
||||
const seq = ++ladeSeq
|
||||
const d = await api.guide(props.topic)
|
||||
if (seq !== ladeSeq) return
|
||||
daten.value = d
|
||||
await nextTick()
|
||||
spy()
|
||||
// Deep-Link (#a03-1 / #k03) hat Vorrang vor der gemerkten Position; beides nur beim ersten Laden
|
||||
const ziel = location.hash.slice(1) || localStorage.getItem('pos:' + props.topic)
|
||||
if (ziel && !aktiv.value) document.getElementById(ziel)?.scrollIntoView({ block: 'start' })
|
||||
}
|
||||
watch(() => props.topic, () => { aktiv.value = ''; cache.clear(); laden() }, { immediate: true })
|
||||
watch(() => props.laeuft, (l, alt) => { if (alt && !l) laden() })
|
||||
// Während eines Laufs nur nachladen, wenn sich der Fortschritt geändert hat
|
||||
watch(() => props.fortschritt, (n, alt) => { if (props.laeuft && n !== alt) laden() })
|
||||
|
||||
function html(a) {
|
||||
const key = a.id + ':' + (a.text || '').length
|
||||
if (!cache.has(key)) cache.set(key, render(a.text))
|
||||
return cache.get(key)
|
||||
}
|
||||
const abschnitte = computed(() => (daten.value?.kapitel || []).flatMap((k) => k.abschnitte))
|
||||
const gelesen = computed(() => {
|
||||
const ids = abschnitte.value.map((a) => 'a' + a.id)
|
||||
const i = ids.indexOf(aktiv.value)
|
||||
return ids.length ? Math.round(100 * Math.max(0, i) / ids.length) : 0
|
||||
})
|
||||
// Kapitel, in dem die aktive Stelle liegt — nur dieses zeigt in der Nav seine Abschnitte; sonst das erste
|
||||
const aktivKap = computed(() => {
|
||||
const id = aktiv.value, ks = daten.value?.kapitel || []
|
||||
const k = ks.find((k) => 'k' + k.id === id || k.abschnitte.some((a) => 'a' + a.id === id))
|
||||
return (k || ks[0])?.id || ''
|
||||
})
|
||||
function teilNeu(i) {
|
||||
const ks = daten.value.kapitel
|
||||
return ks[i].teil && (i === 0 || ks[i - 1].teil !== ks[i].teil)
|
||||
}
|
||||
const kapBadge = { geschrieben: ['gelb', 'geschrieben'], geprueft: ['blau', 'geprüft'], fehlt: ['', 'fehlt'] }
|
||||
|
||||
function spy() {
|
||||
beobachter?.disconnect()
|
||||
beobachter = new IntersectionObserver((es) => {
|
||||
for (const e of es) if (e.isIntersecting) { aktiv.value = e.target.id; localStorage.setItem('pos:' + props.topic, e.target.id) }
|
||||
}, { root: lese.value, rootMargin: '0px 0px -75% 0px' })
|
||||
document.querySelectorAll('.buch [data-spy]').forEach((el) => beobachter.observe(el))
|
||||
}
|
||||
function springe(id) {
|
||||
navAuf.value = false
|
||||
aktiv.value = id
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
history.replaceState(null, '', location.pathname + location.search + '#' + id)
|
||||
}
|
||||
function taste(e) { if (e.key === 'Escape') navAuf.value = false }
|
||||
onMounted(() => window.addEventListener('keydown', taste))
|
||||
onUnmounted(() => window.removeEventListener('keydown', taste))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="guide">
|
||||
<div v-if="navAuf" class="backdrop" @click="navAuf = false"></div>
|
||||
<nav class="guide-nav" :class="{ offen: navAuf }">
|
||||
<button class="schliessen sekundaer klein" @click="navAuf = false">✕ Schließen</button>
|
||||
<div class="werkzeuge">
|
||||
<button class="klein" title="Lesemodus: Abschnitt für Abschnitt" @click="lesemodus = true" :disabled="!abschnitte.length">Lesen ▶</button>
|
||||
<a class="knopf klein sekundaer" title="Als Markdown-Datei herunterladen" :href="api.guideMdUrl(topic)" download>MD</a>
|
||||
<a v-if="daten?.pdf" class="knopf klein sekundaer" title="Als PDF herunterladen" :href="api.pdfUrl(topic)" download>PDF</a>
|
||||
</div>
|
||||
<template v-if="daten">
|
||||
<div class="meta">
|
||||
{{ daten.kapitel.length }} Kapitel · {{ daten.woerter.toLocaleString('de-DE') }} Wörter · {{ zeit(daten.minuten) }}
|
||||
<div class="fortschritt"><i :style="{ width: gelesen + '%' }"></i></div>
|
||||
</div>
|
||||
<template v-for="(k, i) in daten.kapitel" :key="k.id">
|
||||
<div v-if="teilNeu(i)" class="teil">{{ k.teil }}</div>
|
||||
<a class="kap" :class="{ aktiv: aktiv === 'k' + k.id }" @click="springe('k' + k.id)">
|
||||
<span class="pfeil">{{ aktivKap === k.id ? '▾' : '▸' }}</span>
|
||||
<span class="t">{{ k.ord }}. {{ k.titel }}</span><span class="zeit">{{ zeit(k.minuten) }}</span></a>
|
||||
<template v-if="aktivKap === k.id">
|
||||
<a v-for="a in k.abschnitte" :key="a.id" class="abs" :class="{ aktiv: aktiv === 'a' + a.id }" @click="springe('a' + a.id)">
|
||||
<span>{{ a.nr }} {{ a.titel }}</span><span class="zeit">{{ zeit(a.minuten) }}</span></a>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="!daten.kapitel.length" class="leer">Noch kein Text — das Buch erscheint hier, sobald die ersten Kapitel geschrieben sind.</div>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<div ref="lese" class="lese" @touchstart.passive="wischStart" @touchend.passive="wischEnde">
|
||||
<div class="mobil-leiste">
|
||||
<button class="sekundaer klein" @click="navAuf = !navAuf">☰ Inhalt</button>
|
||||
<button class="klein" @click="lesemodus = true" :disabled="!abschnitte.length">Lesen ▶</button>
|
||||
<a class="knopf klein sekundaer" :href="api.guideMdUrl(topic)" download>MD</a>
|
||||
<a v-if="daten?.pdf" class="knopf klein sekundaer" :href="api.pdfUrl(topic)" download>PDF</a>
|
||||
</div>
|
||||
<article v-if="daten" class="buch">
|
||||
<h1>{{ daten.titel }}</h1>
|
||||
<div v-if="daten.untertitel" class="untertitel">{{ daten.untertitel }}</div>
|
||||
<div class="meta">{{ daten.kapitel.length }} Kapitel · {{ daten.woerter.toLocaleString('de-DE') }} Wörter · {{ zeit(daten.minuten) }} Lesezeit</div>
|
||||
<p v-if="daten.status && daten.status !== 'fertig'" class="hinweis">
|
||||
Stand: {{ daten.status }} — Kapitel erscheinen, sobald sie geschrieben sind, und werden danach von einem unabhängigen Agenten geprüft.</p>
|
||||
<section v-if="daten.vorwort" id="vorwort" class="vorwort" data-spy>
|
||||
<h2>Vorwort</h2>
|
||||
<div v-html="render(daten.vorwort)"></div>
|
||||
</section>
|
||||
<section v-for="(k, i) in daten.kapitel" :key="k.id" :id="'k' + k.id" class="kapitel" data-spy>
|
||||
<div v-if="teilNeu(i)" class="teil-titel">{{ k.teil }}</div>
|
||||
<h2>Kapitel {{ k.ord }} – {{ k.titel }}<span v-if="k.status !== 'konsistent' && kapBadge[k.status]" class="badge" :class="kapBadge[k.status][0]">{{ kapBadge[k.status][1] }}</span></h2>
|
||||
<p v-if="k.status === 'fehlt'" class="hinweis">Dieses Kapitel wird noch geschrieben.</p>
|
||||
<div v-else class="kap-meta">{{ k.abschnitte.length }} Abschnitte · {{ zeit(k.minuten) }}</div>
|
||||
<div v-if="k.intro" class="kap-intro" v-html="render(k.intro)"></div>
|
||||
<section v-for="a in k.abschnitte" :key="a.id" :id="'a' + a.id" data-spy>
|
||||
<h3>{{ a.nr }} {{ a.titel }}</h3>
|
||||
<div v-html="html(a)"></div>
|
||||
</section>
|
||||
<details v-if="k.berichte?.autor || k.berichte?.pruefer" class="pruefung">
|
||||
<summary>Prüfung</summary>
|
||||
<template v-if="k.berichte.autor">
|
||||
<h4>Autorenbericht</h4>
|
||||
<div class="md" v-html="render(k.berichte.autor)"></div>
|
||||
</template>
|
||||
<template v-if="k.berichte.pruefer">
|
||||
<h4>Prüfbericht</h4>
|
||||
<div class="md" v-html="render(k.berichte.pruefer)"></div>
|
||||
</template>
|
||||
</details>
|
||||
</section>
|
||||
<div v-if="daten.quellen || daten.gliederung" class="ende">
|
||||
<details v-if="daten.quellen">
|
||||
<summary>Quellen und Prüfmethode</summary>
|
||||
<div class="md" v-html="render(daten.quellen)"></div>
|
||||
</details>
|
||||
<details v-if="daten.gliederung">
|
||||
<summary>Gliederung</summary>
|
||||
<div class="md" v-html="render(daten.gliederung)"></div>
|
||||
</details>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<Lesemodus v-if="lesemodus && daten" :daten="daten" :topic="topic" @schliessen="lesemodus = false" />
|
||||
</div>
|
||||
</template>
|
||||
105
frontend/src/components/Lauf.vue
Normal file
105
frontend/src/components/Lauf.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { render } from '../markdown.js'
|
||||
|
||||
const props = defineProps({ state: Object })
|
||||
const emit = defineEmits(['start', 'pause', 'reset'])
|
||||
|
||||
const laeuft = computed(() => props.state?.status === 'laeuft')
|
||||
const fertig = computed(() => props.state?.status === 'fertig')
|
||||
const begonnen = computed(() => ['pausiert', 'fehler', 'fertig'].includes(props.state?.status)
|
||||
|| (props.state?.stufen || []).some((s) => s.status !== 'offen'))
|
||||
|
||||
function usd(n) { return (n || 0).toFixed(2) + ' $' }
|
||||
function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(2) + ' M' : n >= 1e3 ? Math.round(n / 1e3) + ' k' : String(n || 0) }
|
||||
function dauer(s) {
|
||||
if (!s.gestartet) return ''
|
||||
const a = new Date(s.gestartet)
|
||||
const b = s.beendet ? new Date(s.beendet) : new Date()
|
||||
const sek = Math.max(0, Math.round((b - a) / 1000))
|
||||
return sek < 90 ? `${sek} s` : `${Math.round(sek / 60)} min`
|
||||
}
|
||||
function uhr(t) { return t ? new Date(t).toLocaleTimeString('de-DE') : '' }
|
||||
const statusText = { offen: 'wartet', laeuft: 'läuft', fertig: 'fertig', pausiert: 'pausiert', fehler: 'Fehler', abgebrochen: 'abgebrochen' }
|
||||
const statusFarbe = { laeuft: 'blau', fertig: 'gruen', fehler: 'rot', abgebrochen: 'gelb', pausiert: 'gelb' }
|
||||
|
||||
// Laufende Agenten zuerst, danach die übrigen nach Stufe (in Stufenreihenfolge)
|
||||
const gruppen = computed(() => {
|
||||
const ag = props.state?.agenten || []
|
||||
const out = [{ titel: 'Läuft gerade', agenten: ag.filter((a) => a.status === 'laeuft') }]
|
||||
const rest = ag.filter((a) => a.status !== 'laeuft')
|
||||
const stufen = (props.state?.stufen || []).map((s) => s.id)
|
||||
for (const id of stufen) out.push({ titel: props.state.stufen.find((s) => s.id === id).titel, agenten: rest.filter((a) => a.stufe === id) })
|
||||
out.push({ titel: 'Sonstige', agenten: rest.filter((a) => !stufen.includes(a.stufe)) })
|
||||
return out.filter((g) => g.agenten.length)
|
||||
})
|
||||
const ereignisse = computed(() => (props.state?.ereignisse || []).slice(-100).reverse())
|
||||
|
||||
function reset() { if (confirm('Lauf zurücksetzen? Alles außer dem Thema wird gelöscht.')) emit('reset') }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lauf">
|
||||
<div class="aktionen">
|
||||
<button v-if="!laeuft && !fertig" @click="emit('start')">{{ begonnen ? 'Fortsetzen' : 'Buch erzeugen' }}</button>
|
||||
<button v-if="laeuft" class="sekundaer" @click="emit('pause')">Pause</button>
|
||||
<button v-if="!laeuft && begonnen" class="sekundaer" @click="reset">Zurücksetzen</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="badge" :class="statusFarbe[state.status]">{{ statusText[state.status] || state.status }}<template v-if="state.stufe && laeuft"> · {{ state.stufe }}</template></span>
|
||||
<span class="badge">{{ usd(state.kosten_usd) }}</span>
|
||||
<span class="badge">{{ tokens(state.tokens) }} Tokens</span>
|
||||
</div>
|
||||
<div v-if="state.grund" class="grund">{{ state.grund }}</div>
|
||||
|
||||
<div v-if="state.deutung" class="deutung">
|
||||
<div class="titel">{{ state.deutung.titel }}</div>
|
||||
<div v-if="state.deutung.untertitel" class="sub">{{ state.deutung.untertitel }}</div>
|
||||
<p v-if="state.deutung.deutung">{{ state.deutung.deutung }}</p>
|
||||
<div class="fakten">
|
||||
<span v-if="state.deutung.buchtyp"><b>Buchtyp</b> {{ state.deutung.buchtyp === 'werkzeug' ? 'Werkzeug' : state.deutung.buchtyp === 'wissen' ? 'Wissen' : state.deutung.buchtyp }}</span>
|
||||
<span v-if="state.deutung.zielgruppe"><b>Zielgruppe</b> {{ state.deutung.zielgruppe }}</span>
|
||||
<span v-if="state.deutung.referenz"><b>Referenz</b> {{ state.deutung.referenz }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stufen">
|
||||
<div v-for="s in state.stufen" :key="s.id" class="stufe" :class="s.status">
|
||||
<div class="kreis">{{ s.status === 'fertig' ? '✓' : '' }}</div>
|
||||
<div>
|
||||
<div class="name">{{ s.titel }}</div>
|
||||
<div class="sub">{{ statusText[s.status] || s.status }}<template v-if="s.gesamt"> · {{ s.fertig }} / {{ s.gesamt }}</template></div>
|
||||
<div v-if="s.gesamt" class="balken"><i :style="{ width: Math.min(100, 100 * s.fertig / s.gesamt) + '%' }"></i></div>
|
||||
</div>
|
||||
<div class="sub rechts">{{ s.tokens ? usd(s.kosten_usd) + ' · ' + tokens(s.tokens) + ' Tokens' : '' }}</div>
|
||||
<div class="sub rechts">{{ dauer(s) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="gruppen.length" class="agenten">
|
||||
<template v-for="g in gruppen" :key="g.titel">
|
||||
<h3>{{ g.titel }}</h3>
|
||||
<details v-for="a in g.agenten" :key="a.id" class="agent" :class="a.status">
|
||||
<summary>
|
||||
<span class="name">{{ a.name }}</span>
|
||||
<span class="badge" :class="statusFarbe[a.status]">{{ statusText[a.status] || a.status }}</span>
|
||||
<span class="sub">{{ usd(a.kosten_usd) }}</span>
|
||||
<span class="sub">{{ tokens(a.tokens) }} Tokens</span>
|
||||
<span class="sub">{{ a.werkzeuge || 0 }} Werkzeuge</span>
|
||||
<span class="sub">{{ dauer(a) }}</span>
|
||||
</summary>
|
||||
<div v-if="a.bericht" class="bericht md" v-html="render(a.bericht)"></div>
|
||||
<div v-else class="bericht sub">Noch kein Abschlussbericht.</div>
|
||||
</details>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<details v-if="ereignisse.length" class="ereignisse">
|
||||
<summary>Ereignisse ({{ ereignisse.length }})</summary>
|
||||
<div v-for="(e, i) in ereignisse" :key="i" class="ereignis" :class="e.art">
|
||||
<span class="zeit">{{ uhr(e.zeit) }}</span>
|
||||
<span class="wer">{{ e.agent }}</span>
|
||||
<span class="text">{{ e.text }}</span>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
107
frontend/src/components/Lesemodus.vue
Normal file
107
frontend/src/components/Lesemodus.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { render, zeit } from '../markdown.js'
|
||||
|
||||
const props = defineProps({ daten: Object, topic: String })
|
||||
const emit = defineEmits(['schliessen'])
|
||||
const buehne = ref(null)
|
||||
const pos = ref(0)
|
||||
|
||||
// Bildschirme: Vorwort, je Kapitel ein Intro, je Abschnitt ein Bildschirm, Ende.
|
||||
const screens = computed(() => {
|
||||
const out = []
|
||||
const kaps = props.daten?.kapitel || []
|
||||
if (props.daten?.vorwort) out.push({ typ: 'vorwort' })
|
||||
kaps.forEach((kap, ki) => {
|
||||
out.push({ typ: 'kapitel', kap, nr: ki + 1, von: kaps.length })
|
||||
for (const a of kap.abschnitte) out.push({ typ: 'abschnitt', a, kap })
|
||||
})
|
||||
out.push({ typ: 'ende' })
|
||||
return out
|
||||
})
|
||||
const screen = computed(() => screens.value[pos.value] || { typ: 'ende' })
|
||||
const gesamt = computed(() => screens.value.filter((s) => s.typ === 'abschnitt').length)
|
||||
const gelesen = computed(() => screens.value.slice(0, pos.value + 1).filter((s) => s.typ === 'abschnitt').length)
|
||||
const cache = new Map()
|
||||
function html(a) {
|
||||
const key = a.id + ':' + (a.text || '').length
|
||||
if (!cache.has(key)) cache.set(key, render(a.text))
|
||||
return cache.get(key)
|
||||
}
|
||||
|
||||
function vor() { if (pos.value < screens.value.length - 1) pos.value++ }
|
||||
function zurueck() { if (pos.value > 0) pos.value-- }
|
||||
watch(pos, (p) => { localStorage.setItem('lm:' + props.topic, String(p)); buehne.value?.scrollTo({ top: 0 }) })
|
||||
|
||||
function taste(e) {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') { e.preventDefault(); vor() }
|
||||
else if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); zurueck() }
|
||||
else if (e.key === 'Escape') emit('schliessen')
|
||||
else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
buehne.value?.scrollBy({ top: e.key === 'ArrowDown' ? 160 : -160, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
let wisch = null
|
||||
function wischStart(e) {
|
||||
for (let el = e.target; el && el !== e.currentTarget; el = el.parentElement) {
|
||||
if (el.scrollWidth > el.clientWidth + 1) { wisch = null; return }
|
||||
}
|
||||
const t = e.touches[0]; wisch = { x: t.clientX, y: t.clientY }
|
||||
}
|
||||
function wischEnde(e) {
|
||||
if (!wisch) return
|
||||
const t = e.changedTouches[0]
|
||||
const dx = t.clientX - wisch.x, dy = t.clientY - wisch.y
|
||||
wisch = null
|
||||
if (Math.abs(dx) < 60 || Math.abs(dx) < 2 * Math.abs(dy)) return
|
||||
dx < 0 ? vor() : zurueck()
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', taste)
|
||||
const merk = Number(localStorage.getItem('lm:' + props.topic) || 0)
|
||||
pos.value = Math.min(merk, screens.value.length - 1)
|
||||
})
|
||||
onUnmounted(() => window.removeEventListener('keydown', taste))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lesemodus" @touchstart.passive="wischStart" @touchend.passive="wischEnde">
|
||||
<header class="lm-kopf">
|
||||
<span>{{ gelesen }} / {{ gesamt }} Abschnitte</span>
|
||||
<div class="balken"><i :style="{ width: (gesamt ? 100 * gelesen / gesamt : 0) + '%' }"></i></div>
|
||||
<span v-if="screen.typ === 'abschnitt'">{{ screen.kap.titel }}</span>
|
||||
<button title="Schließen (Esc)" @click="emit('schliessen')">✕</button>
|
||||
</header>
|
||||
<main ref="buehne" class="lm-buehne">
|
||||
<section v-if="screen.typ === 'vorwort'" class="lm-screen">
|
||||
<div class="lm-label">Vorwort</div>
|
||||
<h1>{{ daten.titel }}</h1>
|
||||
<div v-if="daten.untertitel" class="stats" style="margin: 0 0 18px">{{ daten.untertitel }}</div>
|
||||
<div class="intro" v-html="render(daten.vorwort)"></div>
|
||||
<div class="stats">{{ daten.kapitel.length }} Kapitel · {{ daten.woerter.toLocaleString('de-DE') }} Wörter · {{ zeit(daten.minuten) }}</div>
|
||||
</section>
|
||||
<section v-else-if="screen.typ === 'kapitel'" class="lm-screen">
|
||||
<div class="lm-label">Kapitel {{ screen.nr }} / {{ screen.von }}</div>
|
||||
<h1>{{ screen.kap.titel }}</h1>
|
||||
<div v-if="screen.kap.intro" class="intro" v-html="render(screen.kap.intro)"></div>
|
||||
<div v-if="screen.kap.status === 'fehlt'" class="stats">Dieses Kapitel wird noch geschrieben.</div>
|
||||
<div v-else class="stats">{{ screen.kap.abschnitte.length }} Abschnitte · {{ zeit(screen.kap.minuten) }}</div>
|
||||
</section>
|
||||
<section v-else-if="screen.typ === 'abschnitt'" class="lm-screen">
|
||||
<div class="lm-label">{{ screen.kap.titel }}</div>
|
||||
<h2>{{ screen.a.nr }} {{ screen.a.titel }}</h2>
|
||||
<div v-html="html(screen.a)"></div>
|
||||
</section>
|
||||
<section v-else class="lm-screen ende">
|
||||
<h1>Fertig gelesen</h1>
|
||||
<p class="intro">Alle {{ gesamt }} Abschnitte von „{{ daten.titel }}“.</p>
|
||||
<button class="sekundaer" style="margin-top: 16px" @click="pos = 0">Von vorn</button>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="lm-fuss">
|
||||
<button :disabled="pos === 0" @click="zurueck">← Zurück</button>
|
||||
<button class="weiter" :disabled="pos >= screens.length - 1" @click="vor">Weiter →<kbd>␣</kbd></button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user