Lesemodus auf Mobilgeräten und Start an der zuletzt gelesenen Stelle

- Der Scroll-Beobachter schaltete beim Wechsel ins Vollbild den Tab zurück
  auf „Buch“; er schweigt jetzt im Lesemodus, und das nr-Ereignis erzwingt
  keinen Tab mehr.
- Die Leseposition merkt sich, welche Ansicht zuletzt gelesen wurde. Buch-
  ansicht und Lesemodus starten beide an dieser Stelle; die Adresse zählt
  nur noch als Rückfall.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3
2026-10-08 16:08:34 +02:00
parent 7e78649a41
commit 156d5fe69c
4 changed files with 27 additions and 7 deletions

View File

@@ -34,6 +34,7 @@ class ThemaNeu(BaseModel):
class Position(BaseModel):
abschnitt: str | None = None
lesemodus: str | int | None = None # neu: key des Bildschirms, alt: Index
zuletzt: str | None = None # 'buch' oder 'lesen': welche Ansicht zuletzt gelesen wurde
def _thema(slug: str) -> dict:

View File

@@ -201,7 +201,7 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
@neue-version="neueVersion" />
<Guide v-else-if="tab !== 'lauf' && slug && versionAktiv" :slug="slug" :version="versionAktiv" :tab="tab" :nr="nr"
:laeuft="laeuft" :fortschritt="fortschritt"
@tab="(t) => geheZu({ tab: t })" @nr="(e) => geheZu({ tab: 'buch', nr: e.nr }, !e.push)" />
@tab="(t) => geheZu({ tab: t })" @nr="(e) => geheZu({ nr: e.nr }, !e.push)" />
<div v-else style="padding: 24px; color: var(--dim)">Thema links anlegen oder wählen.</div>
</main>
</div>

View File

@@ -19,6 +19,17 @@ function idVonNr(nr) {
}
return ''
}
// Lesemodus-Schlüssel (vorwort, k3, a03-4:2, ende) → Element-ID der Buchansicht, wenn zuletzt dort gelesen wurde
function idVonLesemodus(pos) {
if (pos.zuletzt !== 'lesen' || !pos.lesemodus || !daten.value) return ''
const m = pos.lesemodus
if (m === 'vorwort') return 'vorwort'
const kap = /^k(\d+)$/.exec(m)
if (kap) return 'k' + (daten.value.kapitel[Number(kap[1]) - 1]?.id || '')
const abs = /^(a[\d-]+):/.exec(m)
if (abs) return abs[1]
return ''
}
function nrVonId(id) {
if (!id || !daten.value) return ''
if (id === 'vorwort') return 'vorwort'
@@ -61,7 +72,7 @@ let ladeSeq = 0
let merkTimer = null
function merken(id) {
clearTimeout(merkTimer)
merkTimer = setTimeout(() => api.positionSetzen(props.slug, props.version, { abschnitt: id }).catch(() => {}), 1500)
merkTimer = setTimeout(() => api.positionSetzen(props.slug, props.version, { abschnitt: id, zuletzt: 'buch' }).catch(() => {}), 1500)
}
const cache = new Map() // Schlüssel: Abschnitt + Textlänge → unveränderte Abschnitte werden nicht neu gerendert
@@ -75,8 +86,8 @@ async function laden() {
daten.value = d
await nextTick()
spy()
// Abschnitt in der Adresse hat Vorrang vor der gemerkten Position; beides nur beim ersten Laden
const ziel = idVonNr(props.nr) || pos.abschnitt
// Gemerkte Position hat Vorrang (die Adresse spiegelt nur den letzten Scrollstand); zuletzt im Lesemodus gelesen → dessen Stelle
const ziel = idVonLesemodus(pos) || pos.abschnitt || idVonNr(props.nr)
if (ziel && !aktiv.value) document.getElementById(ziel)?.scrollIntoView({ block: 'start' })
}
watch(() => [props.slug, props.version], () => { aktiv.value = ''; cache.clear(); laden() }, { immediate: true })
@@ -118,6 +129,7 @@ const kapBadge = { geschrieben: ['gelb', 'geschrieben'], geprueft: ['blau', 'gep
function spy() {
beobachter?.disconnect()
beobachter = new IntersectionObserver((es) => {
if (lesemodus.value) return
for (const e of es) if (e.isIntersecting) { aktiv.value = e.target.id; merken(e.target.id); emit('nr', { nr: nrVonId(e.target.id), push: false }) }
}, { root: lese.value, rootMargin: '0px 0px -75% 0px' })
document.querySelectorAll('.buch [data-spy]').forEach((el) => beobachter.observe(el))

View File

@@ -53,7 +53,7 @@ watch(pos, (p) => {
buehne.value?.scrollTo({ top: 0 })
clearTimeout(merkTimer)
const key = screens.value[p]?.key
if (key) merkTimer = setTimeout(() => api.positionSetzen(props.slug, props.version, { lesemodus: key }).catch(() => {}), 1500)
if (key) merkTimer = setTimeout(() => api.positionSetzen(props.slug, props.version, { lesemodus: key, zuletzt: 'lesen' }).catch(() => {}), 1500)
})
function taste(e) {
@@ -82,9 +82,16 @@ function wischEnde(e) {
}
onMounted(async () => {
window.addEventListener('keydown', taste)
const merk = (await api.position(props.slug, props.version).catch(() => ({}))).lesemodus
const p = await api.position(props.slug, props.version).catch(() => ({}))
const merk = p.lesemodus
// Zuletzt in der Buchansicht gelesen → beim passenden Abschnitt bzw. Kapitel beginnen
let i = -1
if (p.zuletzt === 'buch' && p.abschnitt) {
const a = p.abschnitt
i = screens.value.findIndex((s) => s.key === a || s.key.startsWith(a + ':') || (s.typ === 'kapitel' && 'k' + s.kap.id === a))
}
// Alte Positionen waren ein Index, neue sind ein key.
const i = typeof merk === 'string' ? screens.value.findIndex((s) => s.key === merk) : Number(merk || 0)
if (i < 0) i = typeof merk === 'string' ? screens.value.findIndex((s) => s.key === merk) : Number(merk || 0)
pos.value = Math.min(Math.max(i, 0), screens.value.length - 1)
})
onUnmounted(() => window.removeEventListener('keydown', taste))