update
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@@ -27,6 +28,11 @@ class ThemaNeu(BaseModel):
|
||||
hinweise: str = ""
|
||||
|
||||
|
||||
class Position(BaseModel):
|
||||
abschnitt: str | None = None
|
||||
lesemodus: int | None = None
|
||||
|
||||
|
||||
def _pruefen(name: str) -> dict:
|
||||
if not state.name_ok(name) or not (t := state.thema(name)):
|
||||
raise HTTPException(404, "Thema unbekannt")
|
||||
@@ -94,6 +100,18 @@ async def zustand(name: str):
|
||||
"deutung": st.get("deutung") or state.deutung(name), "ereignisse": st["ereignisse"][-100:]}
|
||||
|
||||
|
||||
@app.get("/api/topics/{name}/position")
|
||||
async def position_lesen(name: str):
|
||||
_pruefen(name)
|
||||
return state.position(name)
|
||||
|
||||
|
||||
@app.put("/api/topics/{name}/position")
|
||||
async def position_schreiben(name: str, body: Position):
|
||||
_pruefen(name)
|
||||
return state.position_setzen(name, {k: v for k, v in body.model_dump().items() if v is not None})
|
||||
|
||||
|
||||
@app.get("/api/topics/{name}/guide")
|
||||
async def guide(name: str):
|
||||
_pruefen(name)
|
||||
|
||||
@@ -167,3 +167,15 @@ def slug(text: str) -> str:
|
||||
t = t.replace(a, b)
|
||||
t = re.sub(r"[^a-z0-9]+", "-", t).strip("-")
|
||||
return t[:60] or "kapitel"
|
||||
|
||||
|
||||
def position(name: str) -> dict:
|
||||
"""Leseposition (Abschnitt der Buchansicht, Bildschirm des Lesemodus) – geräteübergreifend auf dem Server."""
|
||||
return _lesen(verzeichnis(name) / "position.json", {})
|
||||
|
||||
|
||||
def position_setzen(name: str, werte: dict) -> dict:
|
||||
p = position(name)
|
||||
p.update(werte)
|
||||
_schreiben(verzeichnis(name) / "position.json", p)
|
||||
return p
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "primer-frontend",
|
||||
"name": "buchwerk-frontend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "primer-frontend",
|
||||
"name": "buchwerk-frontend",
|
||||
"dependencies": {
|
||||
"dompurify": "^3.2.0",
|
||||
"highlight.js": "^11.11.0",
|
||||
|
||||
@@ -23,6 +23,8 @@ export const api = {
|
||||
reset: (t) => req(`/topics/${e(t)}/reset`, { method: 'POST' }),
|
||||
state: (t) => req(`/topics/${e(t)}/state`),
|
||||
guide: (t) => req(`/topics/${e(t)}/guide`),
|
||||
position: (t) => req(`/topics/${e(t)}/position`),
|
||||
positionSetzen: (t, daten) => req(`/topics/${e(t)}/position`, { method: 'PUT', body: daten }),
|
||||
guideMdUrl: (t) => `/api/topics/${e(t)}/guide.md`,
|
||||
pdfUrl: (t) => `/api/topics/${e(t)}/buch.pdf`,
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ const aktiv = ref('')
|
||||
const lese = ref(null)
|
||||
let beobachter = null
|
||||
let ladeSeq = 0
|
||||
// Leseposition auf dem Server merken (geräteübergreifend), gebündelt alle 1,5 s
|
||||
let merkTimer = null
|
||||
function merken(id) {
|
||||
clearTimeout(merkTimer)
|
||||
merkTimer = setTimeout(() => api.positionSetzen(props.topic, { abschnitt: id }).catch(() => {}), 1500)
|
||||
}
|
||||
const cache = new Map() // Schlüssel: Abschnitt + Textlänge → unveränderte Abschnitte werden nicht neu gerendert
|
||||
|
||||
onUnmounted(() => { beobachter?.disconnect() })
|
||||
@@ -33,13 +39,13 @@ onUnmounted(() => { beobachter?.disconnect() })
|
||||
async function laden() {
|
||||
if (!props.topic) { daten.value = null; return }
|
||||
const seq = ++ladeSeq
|
||||
const d = await api.guide(props.topic)
|
||||
const [d, pos] = await Promise.all([api.guide(props.topic), api.position(props.topic).catch(() => ({}))])
|
||||
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)
|
||||
const ziel = location.hash.slice(1) || pos.abschnitt
|
||||
if (ziel && !aktiv.value) document.getElementById(ziel)?.scrollIntoView({ block: 'start' })
|
||||
}
|
||||
watch(() => props.topic, () => { aktiv.value = ''; cache.clear(); laden() }, { immediate: true })
|
||||
@@ -73,7 +79,7 @@ const kapBadge = { geschrieben: ['gelb', 'geschrieben'], geprueft: ['blau', 'gep
|
||||
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) }
|
||||
for (const e of es) if (e.isIntersecting) { aktiv.value = e.target.id; merken(e.target.id) }
|
||||
}, { root: lese.value, rootMargin: '0px 0px -75% 0px' })
|
||||
document.querySelectorAll('.buch [data-spy]').forEach((el) => beobachter.observe(el))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { render, zeit } from '../markdown.js'
|
||||
import { api } from '../api.js'
|
||||
|
||||
const props = defineProps({ daten: Object, topic: String })
|
||||
const emit = defineEmits(['schliessen'])
|
||||
@@ -31,7 +32,12 @@ function html(a) {
|
||||
|
||||
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 }) })
|
||||
let merkTimer = null
|
||||
watch(pos, (p) => {
|
||||
buehne.value?.scrollTo({ top: 0 })
|
||||
clearTimeout(merkTimer)
|
||||
merkTimer = setTimeout(() => api.positionSetzen(props.topic, { lesemodus: p }).catch(() => {}), 1500)
|
||||
})
|
||||
|
||||
function taste(e) {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') { e.preventDefault(); vor() }
|
||||
@@ -57,9 +63,9 @@ function wischEnde(e) {
|
||||
if (Math.abs(dx) < 60 || Math.abs(dx) < 2 * Math.abs(dy)) return
|
||||
dx < 0 ? vor() : zurueck()
|
||||
}
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', taste)
|
||||
const merk = Number(localStorage.getItem('lm:' + props.topic) || 0)
|
||||
const merk = Number((await api.position(props.topic).catch(() => ({}))).lesemodus || 0)
|
||||
pos.value = Math.min(merk, screens.value.length - 1)
|
||||
})
|
||||
onUnmounted(() => window.removeEventListener('keydown', taste))
|
||||
|
||||
Reference in New Issue
Block a user