2026-03-16 19:19:22 +01:00
/ * *
* diora — radio player
* Handles playback , SSE metadata , search , station management , and affiliate links .
* /
'use strict' ;
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
2026-09-04 11:38:34 +02:00
// Hardcoded for now -- only creamfresh radio gets the vote buttons, since
// only its backend (the DJ) actually does anything with them.
const CREAMFRESH _RADIO _URL = 'https://diora.creamfresh.xyz/radio/creamfresh-stream/' ;
2026-03-16 19:19:22 +01:00
let currentStation = null ; // { url, name, id } | null
let currentTrack = '' ;
let sseSource = null ;
let isPlaying = false ;
let currentPlayId = null ;
const audio = new Audio ( ) ;
2026-04-04 02:27:18 +02:00
// Reconnect audio when the output device changes (e.g. Bluetooth, USB DAC)
if ( navigator . mediaDevices ) {
let _deviceChangeDebounce = null ;
navigator . mediaDevices . addEventListener ( 'devicechange' , ( ) => {
clearTimeout ( _deviceChangeDebounce ) ;
_deviceChangeDebounce = setTimeout ( ( ) => {
if ( ! isPlaying || audio . src === '' ) return ;
const savedTime = audio . currentTime ;
const savedSrc = audio . src ;
audio . src = savedSrc ;
audio . load ( ) ;
if ( savedTime > 0 ) {
audio . addEventListener ( 'loadedmetadata' , function onMeta ( ) {
audio . currentTime = savedTime ;
audio . removeEventListener ( 'loadedmetadata' , onMeta ) ;
} ) ;
}
audio . play ( ) . catch ( ( ) => { } ) ;
} , 500 ) ;
} ) ;
}
2026-03-16 19:19:22 +01:00
// ---------------------------------------------------------------------------
// DOM helpers
// ---------------------------------------------------------------------------
function $ ( id ) { return document . getElementById ( id ) ; }
function getCsrfToken ( ) {
const cookie = document . cookie . split ( '; ' ) . find ( r => r . startsWith ( 'csrftoken=' ) ) ;
return cookie ? cookie . split ( '=' ) [ 1 ] : '' ;
}
function formatDateTime ( iso ) {
if ( ! iso ) return '' ;
const d = new Date ( iso ) ;
const pad = n => String ( n ) . padStart ( 2 , '0' ) ;
return ` ${ d . getFullYear ( ) } - ${ pad ( d . getMonth ( ) + 1 ) } - ${ pad ( d . getDate ( ) ) } `
+ ` ${ pad ( d . getHours ( ) ) } : ${ pad ( d . getMinutes ( ) ) } ` ;
}
function escapeHtml ( str ) {
return String ( str )
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' ) ;
}
2026-08-04 09:01:52 +02:00
// ---------------------------------------------------------------------------
// Modal dialogs — styled replacements for native alert()/confirm()/prompt(),
// which look jarring against the rest of the custom dark UI and can't be
// styled or (in confirm/prompt's case) awaited without blocking the thread.
// All three return a Promise; callers must be async and `await` them.
// ---------------------------------------------------------------------------
function _showModal ( { message , showInput = false , inputValue = '' , showCancel = false , okLabel = 'OK' , danger = false } ) {
return new Promise ( ( resolve ) => {
const overlay = $ ( 'modal-overlay' ) ;
const dialog = $ ( 'modal-dialog' ) ;
const msgEl = $ ( 'modal-message' ) ;
const inputEl = $ ( 'modal-input' ) ;
const okBtn = $ ( 'modal-ok-btn' ) ;
const cancelBtn = $ ( 'modal-cancel-btn' ) ;
if ( ! overlay || ! dialog ) { resolve ( null ) ; return ; }
msgEl . textContent = message ;
inputEl . style . display = showInput ? '' : 'none' ;
inputEl . value = inputValue ;
cancelBtn . style . display = showCancel ? '' : 'none' ;
okBtn . textContent = okLabel ;
okBtn . className = danger ? 'btn btn-danger' : 'btn btn-primary' ;
overlay . style . display = '' ;
dialog . style . display = '' ;
( showInput ? inputEl : okBtn ) . focus ( ) ;
if ( showInput ) inputEl . select ( ) ;
function cleanup ( result ) {
overlay . style . display = 'none' ;
dialog . style . display = 'none' ;
overlay . removeEventListener ( 'click' , onCancel ) ;
okBtn . removeEventListener ( 'click' , onOk ) ;
cancelBtn . removeEventListener ( 'click' , onCancel ) ;
document . removeEventListener ( 'keydown' , onKeydown ) ;
resolve ( result ) ;
}
function onOk ( ) { cleanup ( showInput ? inputEl . value : true ) ; }
function onCancel ( ) { cleanup ( showInput ? null : false ) ; }
function onKeydown ( e ) {
if ( e . key === 'Escape' ) onCancel ( ) ;
if ( e . key === 'Enter' && showInput ) onOk ( ) ;
}
okBtn . addEventListener ( 'click' , onOk ) ;
cancelBtn . addEventListener ( 'click' , onCancel ) ;
overlay . addEventListener ( 'click' , onCancel ) ;
document . addEventListener ( 'keydown' , onKeydown ) ;
} ) ;
}
function customAlert ( message ) {
return _showModal ( { message } ) ;
}
function customConfirm ( message , { danger = false } = { } ) {
return _showModal ( { message , showCancel : true , danger } ) ;
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
// The server answers with short English error codes (an API contract, tests and
// other clients rely on them); what the page shows of them is German.
const _GERMAN _ERRORS = {
'no stations found in file' : 'In der Datei wurden keine Sender gefunden.' ,
'file must be .m3u or .m3u8' : 'Die Datei muss eine .m3u- oder .m3u8-Datei sein.' ,
'no file uploaded' : 'Keine Datei hochgeladen.' ,
'only jpeg, png, or webp allowed' : 'Nur JPEG, PNG oder WebP erlaubt.' ,
'file too large (max 50 MB)' : 'Datei zu groß (höchstens 50 MB).' ,
'file too large (max 5 MB)' : 'Datei zu groß (höchstens 5 MB).' ,
'authentication required' : 'Bitte anmelden.' ,
'not found' : 'Nicht gefunden.' ,
'invalid JSON' : 'Ungültige Anfrage.' ,
'upstream unreachable' : 'Der Server dahinter ist nicht erreichbar.' ,
} ;
function _germanError ( code ) {
return _GERMAN _ERRORS [ code ] || code ;
}
2026-08-04 09:01:52 +02:00
function customPrompt ( message , defaultValue = '' ) {
return _showModal ( { message , showInput : true , inputValue : defaultValue , showCancel : true } ) ;
}
2026-03-16 19:19:22 +01:00
// ---------------------------------------------------------------------------
// Play / Stop
// ---------------------------------------------------------------------------
function playStation ( url , name , stationId ) {
stopPlayback ( false ) ;
2026-10-03 23:08:57 +02:00
if ( window . _ _dioraAmbientStop ) window . _ _dioraAmbientStop ( ) ;
2026-03-16 19:19:22 +01:00
2026-09-29 20:00:42 +02:00
// A secure page can't load plain-http streams (mixed content). Native shells
// that play the stream themselves set window.DIORA_PLAYS_HTTP_STREAMS.
if ( location . protocol === 'https:' && url . startsWith ( 'http://' ) && ! window . DIORA _PLAYS _HTTP _STREAMS ) {
2026-03-22 11:45:40 +01:00
window . open ( url , '_blank' ) ;
return ;
}
2026-03-16 19:19:22 +01:00
currentStation = { url , name , id : stationId || null } ;
isPlaying = true ;
2026-03-21 17:50:12 +01:00
audio . src = url ;
2026-03-16 19:19:22 +01:00
const volSlider = document . getElementById ( 'volume' ) ;
2026-03-16 21:07:12 +01:00
if ( volSlider ) audio . volume = volSlider . value / 255 ;
2026-03-16 19:19:22 +01:00
audio . play ( ) . catch ( ( ) => {
// Browser may block autoplay; the user needs to interact first
console . warn ( 'Audio play blocked by browser policy.' ) ;
} ) ;
$ ( 'now-playing-station' ) . textContent = name ;
$ ( 'now-playing-track' ) . textContent = '' ;
2026-03-16 20:38:08 +01:00
$ ( 'play-stop-btn' ) . style . display = '' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
$ ( 'play-stop-btn' ) . textContent = '⏹ Stopp' ;
2026-03-16 19:19:22 +01:00
$ ( 'play-stop-btn' ) . classList . add ( 'playing' ) ;
$ ( 'save-station-btn' ) . style . display = '' ;
2026-09-04 11:38:34 +02:00
const isCreamfresh = url === CREAMFRESH _RADIO _URL ;
$ ( 'creamfresh-vote-up-btn' ) . style . display = isCreamfresh ? '' : 'none' ;
$ ( 'creamfresh-vote-down-btn' ) . style . display = isCreamfresh ? '' : 'none' ;
$ ( 'creamfresh-vote-up-btn' ) . classList . remove ( 'active' ) ;
$ ( 'creamfresh-vote-down-btn' ) . classList . remove ( 'active' ) ;
2026-03-16 19:19:22 +01:00
startMetadataSSE ( url ) ;
startPlaySession ( name , url ) ;
2026-03-16 20:57:07 +01:00
maybeShowDonationHint ( url , name ) ;
Add podcast enhancements: AntennaPod parity features + inbox management
- Auto-play next episode from queue when current episode ends
- Sleep timer (N minutes or end-of-episode) with countdown in button
- In-feed episode filter (client-side search)
- Auto-queue new episodes per feed (⚡Q toggle, inserts at top of queue)
- More playback speeds: 1¾× and 2½× added
- Progress bars + structured meta line in all episode list views (feed, inbox, queue)
- Queue drag-and-drop reorder
- Feed list search filter and sort options (A–Z, Z–A, recently added/refreshed)
- DB migration: PodcastFeed.auto_queue, EpisodeProgress.dismissed
- Inbox: dismiss episodes without marking played, checkboxes for multi-select,
bulk actions (add to queue, mark played, download, dismiss), load-more pagination
- Refresh button in single feed view header
- Hourly background refresh of all subscribed feeds
- Full Media Session API for radio and podcast: Windows taskbar thumbnail buttons
(play/pause/stop/next/seek) now work correctly for both modes
- Playing an episode auto-adds it to the queue if not already there
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:55:11 +01:00
if ( 'mediaSession' in navigator ) {
navigator . mediaSession . metadata = new MediaMetadata ( { title : name , artist : 'Radio' } ) ;
navigator . mediaSession . setActionHandler ( 'play' , ( ) => { audio . play ( ) ; isPlaying = true ; } ) ;
navigator . mediaSession . setActionHandler ( 'pause' , ( ) => { audio . pause ( ) ; isPlaying = false ; } ) ;
navigator . mediaSession . setActionHandler ( 'stop' , ( ) => stopPlayback ( true ) ) ;
try { navigator . mediaSession . setActionHandler ( 'seekbackward' , null ) ; } catch ( _ ) { }
try { navigator . mediaSession . setActionHandler ( 'seekforward' , null ) ; } catch ( _ ) { }
try { navigator . mediaSession . setActionHandler ( 'nexttrack' , null ) ; } catch ( _ ) { }
try { navigator . mediaSession . setActionHandler ( 'previoustrack' , null ) ; } catch ( _ ) { }
navigator . mediaSession . playbackState = 'playing' ;
}
2026-03-16 19:19:22 +01:00
}
function stopPlayback ( clearStation = true ) {
audio . pause ( ) ;
audio . src = '' ;
isPlaying = false ;
Add podcast enhancements: AntennaPod parity features + inbox management
- Auto-play next episode from queue when current episode ends
- Sleep timer (N minutes or end-of-episode) with countdown in button
- In-feed episode filter (client-side search)
- Auto-queue new episodes per feed (⚡Q toggle, inserts at top of queue)
- More playback speeds: 1¾× and 2½× added
- Progress bars + structured meta line in all episode list views (feed, inbox, queue)
- Queue drag-and-drop reorder
- Feed list search filter and sort options (A–Z, Z–A, recently added/refreshed)
- DB migration: PodcastFeed.auto_queue, EpisodeProgress.dismissed
- Inbox: dismiss episodes without marking played, checkboxes for multi-select,
bulk actions (add to queue, mark played, download, dismiss), load-more pagination
- Refresh button in single feed view header
- Hourly background refresh of all subscribed feeds
- Full Media Session API for radio and podcast: Windows taskbar thumbnail buttons
(play/pause/stop/next/seek) now work correctly for both modes
- Playing an episode auto-adds it to the queue if not already there
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:55:11 +01:00
if ( 'mediaSession' in navigator ) navigator . mediaSession . playbackState = 'none' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
2026-03-16 19:19:22 +01:00
if ( sseSource ) {
sseSource . close ( ) ;
sseSource = null ;
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
$ ( 'play-stop-btn' ) . textContent = '▶ Abspielen' ;
2026-03-16 19:19:22 +01:00
$ ( 'play-stop-btn' ) . classList . remove ( 'playing' ) ;
$ ( 'save-station-btn' ) . style . display = 'none' ;
2026-09-04 11:38:34 +02:00
$ ( 'creamfresh-vote-up-btn' ) . style . display = 'none' ;
$ ( 'creamfresh-vote-down-btn' ) . style . display = 'none' ;
2026-03-16 19:19:22 +01:00
$ ( 'affiliate-section' ) . style . display = 'none' ;
stopPlaySession ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const stationEl = $ ( 'now-playing-station' ) ;
const trackEl = $ ( 'now-playing-track' ) ;
2026-03-16 19:19:22 +01:00
if ( clearStation ) {
currentStation = null ;
currentTrack = '' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
stationEl . textContent = '— kein Sender —' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
trackEl . textContent = '' ;
2026-03-16 20:38:08 +01:00
$ ( 'play-stop-btn' ) . style . display = 'none' ;
2026-03-16 19:19:22 +01:00
}
}
function togglePlayStop ( ) {
if ( isPlaying ) {
stopPlayback ( true ) ;
} else if ( currentStation ) {
playStation ( currentStation . url , currentStation . name , currentStation . id ) ;
}
}
// ---------------------------------------------------------------------------
// Play session tracking
// ---------------------------------------------------------------------------
async function startPlaySession ( stationName , stationUrl ) {
try {
const res = await fetch ( '/radio/play/start/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { station _name : stationName , station _url : stationUrl } )
} ) ;
if ( res . ok ) {
const data = await res . json ( ) ;
currentPlayId = data . play _id ;
}
} catch ( e ) { }
}
async function stopPlaySession ( ) {
2026-09-29 20:00:42 +02:00
// Take the id before awaiting: playStation() calls this without waiting and
// starts the next session right away, whose id must not be cleared here.
const playId = currentPlayId ;
if ( ! playId ) return ;
currentPlayId = null ;
2026-03-16 19:19:22 +01:00
try {
await fetch ( '/radio/play/stop/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
2026-09-29 20:00:42 +02:00
body : JSON . stringify ( { play _id : playId } )
2026-03-16 19:19:22 +01:00
} ) ;
} catch ( e ) { }
}
window . addEventListener ( 'beforeunload' , ( ) => {
if ( currentPlayId ) {
navigator . sendBeacon ( '/radio/play/stop/' , JSON . stringify ( { play _id : currentPlayId } ) ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Flush cached encrypted payloads for reader data (encryption is async so we
// use pre-computed blobs stored by the debounced savers)
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// The last position written (it's in the outbox too, in case the beacon is lost).
if ( _lastProgressBeacon ) navigator . sendBeacon ( ` /books/ ${ _lastProgressBeacon . book } /progress/ ` , _progressBody ( _lastProgressBeacon ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( _lastBookmarkBeacon ) navigator . sendBeacon ( _lastBookmarkBeacon . url , _lastBookmarkBeacon . body ) ;
if ( _lastHighlightBeacon ) navigator . sendBeacon ( _lastHighlightBeacon . url , _lastHighlightBeacon . body ) ;
} ) ;
document . addEventListener ( 'visibilitychange' , ( ) => {
if ( document . visibilityState === 'hidden' && currentBookId ) {
if ( bookmarksDirty ) saveBookmarks ( ) ;
if ( highlightsDirty ) saveHighlights ( ) ;
saveReaderProgress ( ) ;
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Back in front: maybe the book was read on another device meanwhile.
if ( document . visibilityState === 'visible' && currentBookId ) _checkNewerPositionElsewhere ( ) ;
2026-03-16 19:19:22 +01:00
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Cached beacon payloads — updated after each successful encrypt in save functions
let _lastBookmarkBeacon = null ;
let _lastHighlightBeacon = null ;
let _lastProgressBeacon = null ;
2026-03-16 19:19:22 +01:00
// ---------------------------------------------------------------------------
// Recommendations
// ---------------------------------------------------------------------------
async function loadRecommendations ( ) {
const container = document . getElementById ( 'recommendations' ) ;
if ( ! container ) return ;
try {
const res = await fetch ( '/radio/recommendations/' ) ;
const data = await res . json ( ) ;
if ( ! data . recommendations . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
container . innerHTML = '<p class="muted">Hör mehr Sender, dann gibt es hier Empfehlungen.</p>' ;
2026-03-16 19:19:22 +01:00
return ;
}
const label = data . context ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
let html = ` <p class="recommendations-context">Was du ${ label } oft hörst:</p><ul class="recommendations-list"> ` ;
2026-03-16 19:19:22 +01:00
for ( const r of data . recommendations ) {
html += ` <li>
< button class = "btn btn-sm" onclick = "playStation('${escapeAttr(r.station_url)}', '${escapeAttr(r.station_name)}', ${r.saved_id || 'null'})" >
& # 9654 ; $ { escapeHtml ( r . station _name ) }
< / b u t t o n >
< span class = "muted" > $ { r . play _count } & times ; < / s p a n >
< / l i > ` ;
}
html += '</ul>' ;
container . innerHTML = html ;
} catch ( e ) { }
}
function escapeAttr ( s ) {
return String ( s ) . replace ( /'/g , "\\'" ) . replace ( /"/g , '"' ) ;
}
// ---------------------------------------------------------------------------
// Volume
// ---------------------------------------------------------------------------
2026-03-16 21:07:12 +01:00
function setVolume ( val ) {
val = Math . max ( 0 , Math . min ( 255 , Math . round ( val ) ) ) ;
audio . volume = val / 255 ;
localStorage . setItem ( 'diora_volume' , val ) ;
const slider = $ ( 'volume' ) ;
const numInput = $ ( 'volume-num' ) ;
if ( slider ) slider . value = val ;
if ( numInput ) numInput . value = val ;
2026-10-04 00:01:03 +02:00
_ambientVolume ( val ) ;
2026-03-16 21:07:12 +01:00
}
const volSliderEl = document . getElementById ( 'volume' ) ;
if ( volSliderEl ) {
[ 'input' , 'change' , 'mousemove' , 'touchmove' ] . forEach ( evt =>
volSliderEl . addEventListener ( evt , function ( ) { setVolume ( this . value ) ; } )
) ;
}
const volNumEl = document . getElementById ( 'volume-num' ) ;
if ( volNumEl ) {
volNumEl . addEventListener ( 'change' , function ( ) { setVolume ( this . value ) ; } ) ;
volNumEl . addEventListener ( 'input' , function ( ) { setVolume ( this . value ) ; } ) ;
2026-03-16 21:20:31 +01:00
volNumEl . addEventListener ( 'click' , function ( ) { this . select ( ) ; } ) ;
2026-03-16 21:07:12 +01:00
}
2026-03-16 19:19:22 +01:00
2026-03-16 21:35:27 +01:00
const volSliderEl2 = document . getElementById ( 'volume' ) ;
const volWheelTarget = volSliderEl2 || volNumEl ;
if ( volWheelTarget ) {
volWheelTarget . addEventListener ( 'wheel' , function ( e ) {
e . preventDefault ( ) ;
const current = parseInt ( document . getElementById ( 'volume' ) . value , 10 ) ;
setVolume ( current + ( e . deltaY < 0 ? 4 : - 4 ) ) ;
} , { passive : false } ) ;
}
2026-03-16 19:19:22 +01:00
// ---------------------------------------------------------------------------
// SSE metadata
// ---------------------------------------------------------------------------
function startMetadataSSE ( streamUrl ) {
if ( sseSource ) { sseSource . close ( ) ; sseSource = null ; }
const endpoint = '/radio/sse/?url=' + encodeURIComponent ( streamUrl ) ;
sseSource = new EventSource ( endpoint ) ;
sseSource . onmessage = function ( e ) {
let data ;
try { data = JSON . parse ( e . data ) ; } catch ( _ ) { return ; }
if ( data . error ) {
console . warn ( 'SSE stream ended:' , data . error ) ;
return ;
}
if ( data . track && data . track !== currentTrack ) {
currentTrack = data . track ;
updateNowPlayingUI ( data . track ) ;
recordTrack ( currentStation ? currentStation . name : '' , data . track ) ;
fetchAffiliateLinks ( data . track ) ;
}
} ;
sseSource . onerror = function ( ) {
// Connection dropped; the browser will attempt to reconnect automatically
console . warn ( 'SSE connection error, browser will retry.' ) ;
} ;
}
function updateNowPlayingUI ( track ) {
$ ( 'now-playing-track' ) . textContent = track ;
}
// ---------------------------------------------------------------------------
// Record track
// ---------------------------------------------------------------------------
async function recordTrack ( stationName , track ) {
try {
const res = await fetch ( '/radio/record/' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CSRFToken' : getCsrfToken ( ) ,
} ,
body : JSON . stringify ( { station _name : stationName , track , scrobble : true } ) ,
} ) ;
if ( res . ok ) {
addHistoryRow ( stationName , track ) ;
}
} catch ( err ) {
console . error ( 'recordTrack error:' , err ) ;
}
}
function addHistoryRow ( stationName , track ) {
const tbody = $ ( 'history-tbody' ) ;
if ( ! tbody ) return ;
// Remove the "no history" placeholder row if present
const emptyRow = $ ( 'history-empty-row' ) ;
if ( emptyRow ) emptyRow . remove ( ) ;
const tr = document . createElement ( 'tr' ) ;
const now = new Date ( ) . toISOString ( ) ;
tr . innerHTML = `
< td class = "history-time" > $ { escapeHtml ( formatDateTime ( now ) ) } < / t d >
< td > $ { escapeHtml ( stationName ) } < / t d >
< td > $ { escapeHtml ( track ) } < / t d >
< td > < / t d >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< td > < button class = "btn-delete-history" onclick = "deleteHistoryEntry(null, this)" title = "Entfernen" > ✕ < / b u t t o n > < / t d >
2026-03-16 19:19:22 +01:00
` ;
tbody . insertBefore ( tr , tbody . firstChild ) ;
}
2026-03-16 20:16:30 +01:00
async function deleteHistoryEntry ( id , btn ) {
const tr = btn . closest ( 'tr' ) ;
if ( ! id ) { tr . remove ( ) ; return ; }
try {
const res = await fetch ( ` /radio/history/ ${ id } /delete/ ` , {
method : 'POST' ,
headers : { 'X-CSRFToken' : getCsrfToken ( ) } ,
} ) ;
if ( res . ok ) tr . remove ( ) ;
} catch ( e ) { }
}
2026-03-16 19:19:22 +01:00
// ---------------------------------------------------------------------------
// Affiliate links
// ---------------------------------------------------------------------------
async function fetchAffiliateLinks ( track ) {
const section = $ ( 'affiliate-section' ) ;
2026-03-16 20:47:02 +01:00
if ( section && section . dataset . disabled ) return ;
2026-03-16 19:19:22 +01:00
try {
const res = await fetch ( '/radio/affiliate/?track=' + encodeURIComponent ( track ) ) ;
if ( ! res . ok ) return ;
const data = await res . json ( ) ;
const itunes = data . itunes _data || { } ;
$ ( 'affiliate-track-name' ) . textContent = itunes . name || track ;
$ ( 'affiliate-artist-name' ) . textContent = itunes . artist || '' ;
$ ( 'affiliate-album-name' ) . textContent = itunes . album || '' ;
const artEl = $ ( 'affiliate-artwork' ) ;
if ( itunes . artwork ) {
artEl . src = itunes . artwork ;
artEl . style . display = '' ;
} else {
artEl . style . display = 'none' ;
}
const amzLink = $ ( 'affiliate-amazon-link' ) ;
if ( data . amazon _url ) {
amzLink . href = data . amazon _url ;
amzLink . style . display = '' ;
} else {
amzLink . style . display = 'none' ;
}
section . style . display = 'flex' ;
} catch ( err ) {
console . error ( 'fetchAffiliateLinks error:' , err ) ;
section . style . display = 'none' ;
}
}
// ---------------------------------------------------------------------------
// Search (radio-browser.info)
// ---------------------------------------------------------------------------
async function doSearch ( ) {
const query = $ ( 'search-input' ) . value . trim ( ) ;
if ( ! query ) return ;
const statusEl = $ ( 'search-status' ) ;
const tableEl = $ ( 'search-results-table' ) ;
const tbody = $ ( 'search-results-body' ) ;
tableEl . style . display = 'none' ;
tbody . innerHTML = '' ;
2026-09-29 20:33:34 +02:00
// A stream address instead of a name: offer to play or save it directly.
if ( /^https?:\/\/\S+$/i . test ( query ) ) {
showStreamAddressResult ( query ) ;
return ;
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
statusEl . textContent = 'Suche …' ;
2026-09-29 20:33:34 +02:00
2026-03-16 19:19:22 +01:00
try {
const url = ` https://de1.api.radio-browser.info/json/stations/search?name= ${ encodeURIComponent ( query ) } &limit=50&hidebroken=true&order=clickcount&reverse=true ` ;
const res = await fetch ( url ) ;
if ( ! res . ok ) throw new Error ( ` HTTP ${ res . status } ` ) ;
const stations = await res . json ( ) ;
if ( ! stations . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
statusEl . textContent = 'Keine Sender gefunden.' ;
2026-03-16 19:19:22 +01:00
return ;
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
statusEl . textContent = stations . length === 1 ? '1 Treffer' : ` ${ stations . length } Treffer ` ;
2026-03-16 19:19:22 +01:00
tableEl . style . display = '' ;
const curated = document . getElementById ( 'curated-lists' ) ;
if ( curated ) curated . style . display = 'none' ;
stations . forEach ( st => {
const tr = document . createElement ( 'tr' ) ;
const safeName = escapeHtml ( st . name || '' ) ;
const safeUrl = escapeHtml ( st . url _resolved || st . url || '' ) ;
const safeBr = escapeHtml ( st . bitrate ? st . bitrate + ' kbps' : '' ) ;
const safeCC = escapeHtml ( st . countrycode || st . country || '' ) ;
const safeTags = escapeHtml ( ( st . tags || '' ) . split ( ',' ) . slice ( 0 , 3 ) . join ( ', ' ) ) ;
tr . innerHTML = `
< td title = "${safeName}" > $ { safeName } < / t d >
< td > $ { safeBr } < / t d >
< td > $ { safeCC } < / t d >
< td title = "${escapeHtml(st.tags || '')}" > $ { safeTags } < / t d >
< td >
< button class = "btn btn-sm btn-play"
onclick = ' searchPlay ( $ { JSON . stringify ( safeUrl ) } , $ { JSON . stringify ( safeName ) } , $ { JSON . stringify ( {
name : st . name ,
url : st . url _resolved || st . url ,
bitrate : st . bitrate ? String ( st . bitrate ) : '' ,
country : st . country || '' ,
tags : st . tags || '' ,
favicon _url : st . favicon || '' ,
} ) } ) ' >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
& # 9654 ; Abspielen
2026-03-16 19:19:22 +01:00
< / b u t t o n >
< / t d >
` ;
tbody . appendChild ( tr ) ;
} ) ;
} catch ( err ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
statusEl . textContent = 'Suche fehlgeschlagen: ' + err . message ;
2026-03-16 19:19:22 +01:00
}
}
2026-09-29 20:33:34 +02:00
// A name to start from for a pasted address: "klassik.mp3" on radio.example.org
// -> "Klassik (radio.example.org)". The query string is left out on purpose —
// it often carries an access token, which shouldn't end up on screen.
function _streamAddressName ( url ) {
try {
const u = new URL ( url ) ;
const file = decodeURIComponent ( u . pathname . split ( '/' ) . filter ( Boolean ) . pop ( ) || '' ) ;
const base = file . replace ( /\.(mp3|aac|aacp|ogg|opus|m3u8?|pls|flac)$/i , '' ) . replace ( /[-_]+/g , ' ' ) . trim ( ) ;
const nice = base ? base . charAt ( 0 ) . toUpperCase ( ) + base . slice ( 1 ) : '' ;
return nice ? ` ${ nice } ( ${ u . host } ) ` : u . host ;
} catch ( e ) {
return url ;
}
}
function showStreamAddressResult ( url ) {
const statusEl = $ ( 'search-status' ) ;
const tableEl = $ ( 'search-results-table' ) ;
const tbody = $ ( 'search-results-body' ) ;
const name = _streamAddressName ( url ) ;
const shown = url . split ( '?' ) [ 0 ] ;
const data = { name , url , bitrate : '' , country : '' , tags : '' , favicon _url : '' } ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
statusEl . textContent = 'Stream-Adresse' ;
2026-09-29 20:33:34 +02:00
const curated = document . getElementById ( 'curated-lists' ) ;
if ( curated ) curated . style . display = 'none' ;
// Same five columns as a radio-browser result: name, bitrate, country, tags, actions.
const tr = document . createElement ( 'tr' ) ;
tr . className = 'stream-address-row' ;
for ( let i = 0 ; i < 4 ; i ++ ) tr . appendChild ( document . createElement ( 'td' ) ) ;
tr . children [ 0 ] . textContent = name ;
tr . children [ 0 ] . title = shown ;
tr . children [ 3 ] . textContent = shown ;
tr . children [ 3 ] . title = shown ;
const actions = document . createElement ( 'td' ) ;
const play = document . createElement ( 'button' ) ;
play . className = 'btn btn-sm btn-play' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
play . innerHTML = '▶ Abspielen' ;
2026-09-29 20:33:34 +02:00
play . addEventListener ( 'click' , ( ) => searchPlay ( url , name , data ) ) ;
const save = document . createElement ( 'button' ) ;
save . className = 'btn btn-sm' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
save . innerHTML = '★ Speichern' ;
2026-09-29 20:33:34 +02:00
save . style . marginLeft = '4px' ;
save . addEventListener ( 'click' , async e => {
e . stopPropagation ( ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const chosen = await customPrompt ( 'Name für diesen Sender' , name ) ;
2026-09-29 20:33:34 +02:00
if ( chosen == null ) return ;
const finalName = chosen . trim ( ) || name ;
await saveStation ( { ... data , name : finalName } ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
statusEl . textContent = ` Gespeichert als „ ${ finalName } “. ` ;
2026-09-29 20:33:34 +02:00
} ) ;
actions . append ( play , save ) ;
tr . appendChild ( actions ) ;
tbody . appendChild ( tr ) ;
tableEl . style . display = '' ;
}
2026-03-16 19:19:22 +01:00
function searchPlay ( url , name , stationData ) {
// Store station data on the window so saveCurrentStation() can use it
window . _pendingStationData = stationData ;
playStation ( url , name , null ) ;
}
// ---------------------------------------------------------------------------
// Save current station
// ---------------------------------------------------------------------------
async function saveCurrentStation ( ) {
if ( ! currentStation ) return ;
// Use the rich data from search results if available, otherwise minimal data
const data = window . _pendingStationData || {
name : currentStation . name ,
url : currentStation . url ,
bitrate : '' ,
country : '' ,
tags : '' ,
favicon _url : '' ,
} ;
await saveStation ( data ) ;
}
2026-09-04 11:38:34 +02:00
// ---------------------------------------------------------------------------
// creamfresh radio: DJ feedback (hardcoded to this one station, see
// CREAMFRESH_RADIO_URL above)
// ---------------------------------------------------------------------------
async function creamfreshVote ( direction ) {
const upBtn = $ ( 'creamfresh-vote-up-btn' ) ;
const downBtn = $ ( 'creamfresh-vote-down-btn' ) ;
upBtn . disabled = true ;
downBtn . disabled = true ;
try {
const res = await fetch ( '/radio/creamfresh-feedback/' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CSRFToken' : getCsrfToken ( ) ,
} ,
body : JSON . stringify ( { vote : direction } ) ,
} ) ;
if ( ! res . ok ) throw new Error ( ` feedback returned ${ res . status } ` ) ;
upBtn . classList . toggle ( 'active' , direction === 'up' ) ;
downBtn . classList . toggle ( 'active' , direction === 'down' ) ;
} catch ( err ) {
console . warn ( 'creamfresh vote failed' , err ) ;
} finally {
upBtn . disabled = false ;
downBtn . disabled = false ;
}
}
2026-03-16 19:19:22 +01:00
async function saveStation ( station ) {
try {
const res = await fetch ( '/radio/save/' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CSRFToken' : getCsrfToken ( ) ,
} ,
body : JSON . stringify ( station ) ,
} ) ;
if ( res . status === 401 ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
await customAlert ( 'Bitte anmelden, um Sender zu speichern.' ) ;
2026-03-16 19:19:22 +01:00
return ;
}
const data = await res . json ( ) ;
if ( data . ok ) {
if ( data . created ) {
addSavedRow ( { id : data . id , ... station , is _favorite : false } ) ;
}
}
} catch ( err ) {
console . error ( 'saveStation error:' , err ) ;
}
}
function addSavedRow ( station ) {
const tbody = $ ( 'saved-tbody' ) ;
if ( ! tbody ) return ;
const emptyRow = $ ( 'saved-empty-row' ) ;
if ( emptyRow ) emptyRow . remove ( ) ;
// Check for duplicate
if ( document . getElementById ( ` saved-row- ${ station . id } ` ) ) return ;
const tr = document . createElement ( 'tr' ) ;
tr . id = ` saved-row- ${ station . id } ` ;
tr . dataset . id = station . id ;
tr . dataset . url = station . url ;
tr . dataset . name = station . name ;
const safeName = escapeHtml ( station . name || '' ) ;
const safeBr = escapeHtml ( station . bitrate || '' ) ;
const safeCC = escapeHtml ( station . country || '' ) ;
const safeUrl = escapeHtml ( station . url || '' ) ;
tr . innerHTML = `
< td >
< button class = "btn-icon fav-btn${station.is_favorite ? ' active' : ''}"
onclick = "toggleFav(${station.id}, this)"
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
title = "Favorit an/aus" > & # 9733 ; < / b u t t o n >
2026-03-16 19:19:22 +01:00
< / t d >
< td class = "station-name-cell" > $ { safeName } < / t d >
< td > $ { safeBr } < / t d >
< td > $ { safeCC } < / t d >
< td class = "notes-cell" onclick = "editNotes(${station.id}, this.textContent.trim())" title = "" style = "cursor:pointer; color:#666; max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" > < / t d >
< td >
< button class = "btn btn-sm"
onclick = "playStation('${safeUrl}', '${safeName}', ${station.id})" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
& # 9654 ; Abspielen
2026-03-16 19:19:22 +01:00
< / b u t t o n >
< / t d >
< td >
< button class = "btn btn-sm btn-danger"
onclick = "removeStation(${station.id})" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
Entfernen
2026-03-16 19:19:22 +01:00
< / b u t t o n >
< / t d >
` ;
tbody . appendChild ( tr ) ;
}
// ---------------------------------------------------------------------------
// Remove station
// ---------------------------------------------------------------------------
2026-09-29 20:33:34 +02:00
// The only toggleFav: a second definition further down (expecting the button
// as an argument) used to shadow this one, so the server-rendered rows — which
// pass no button — toggled the favourite on the server without the star ever
// changing, and a second click silently took it back.
async function toggleFav ( pk , btnEl ) {
2026-03-16 19:19:22 +01:00
try {
const res = await fetch ( ` /radio/favorite/ ${ pk } / ` , {
method : 'POST' ,
headers : { 'X-CSRFToken' : getCsrfToken ( ) } ,
} ) ;
if ( ! res . ok ) return ;
const data = await res . json ( ) ;
// Flip the star button state
2026-09-29 20:33:34 +02:00
const btn = btnEl || document . querySelector ( ` #saved-row- ${ pk } .fav-btn ` ) ;
if ( btn ) btn . classList . toggle ( 'active' , ! ! data . is _favorite ) ;
2026-03-16 19:19:22 +01:00
// Re-sort rows in the DOM: favorites first, then alphabetically
const tbody = $ ( 'saved-tbody' ) ;
if ( ! tbody ) return ;
const rows = Array . from ( tbody . querySelectorAll ( 'tr[data-id]' ) ) ;
rows . sort ( ( a , b ) => {
const aFav = a . querySelector ( '.fav-btn' ) ? . classList . contains ( 'active' ) ? 0 : 1 ;
const bFav = b . querySelector ( '.fav-btn' ) ? . classList . contains ( 'active' ) ? 0 : 1 ;
if ( aFav !== bFav ) return aFav - bFav ;
return a . dataset . name . localeCompare ( b . dataset . name ) ;
} ) ;
rows . forEach ( row => tbody . appendChild ( row ) ) ;
} catch ( err ) {
console . error ( 'toggleFav error' , err ) ;
}
}
async function removeStation ( pk ) {
try {
const res = await fetch ( ` /radio/remove/ ${ pk } / ` , {
method : 'POST' ,
headers : { 'X-CSRFToken' : getCsrfToken ( ) } ,
} ) ;
if ( res . ok ) {
const row = $ ( ` saved-row- ${ pk } ` ) ;
if ( row ) row . remove ( ) ;
const tbody = $ ( 'saved-tbody' ) ;
if ( tbody && tbody . querySelectorAll ( 'tr' ) . length === 0 ) {
const tr = document . createElement ( 'tr' ) ;
tr . id = 'saved-empty-row' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
tr . innerHTML = '<td colspan="7" class="empty-msg">Noch keine gespeicherten Sender.</td>' ;
2026-03-16 19:19:22 +01:00
tbody . appendChild ( tr ) ;
}
}
} catch ( err ) {
console . error ( 'removeStation error:' , err ) ;
}
}
// ---------------------------------------------------------------------------
// Focus Timer
// ---------------------------------------------------------------------------
const TIMER _WORK = 25 * 60 ;
const TIMER _BREAK = 5 * 60 ;
let timerSeconds = TIMER _WORK ;
let timerRunning = false ;
let timerIsBreak = false ;
let timerInterval = null ;
function timerTick ( ) {
timerSeconds -- ;
renderTimer ( ) ;
if ( timerSeconds <= 0 ) {
clearInterval ( timerInterval ) ;
timerInterval = null ;
timerRunning = false ;
if ( ! timerIsBreak ) {
// work session ended → start break
timerIsBreak = true ;
recordFocusSession ( ) ;
timerSeconds = TIMER _BREAK ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
showTimerNotification ( 'Pause! 5 Minuten.' ) ;
2026-03-16 19:19:22 +01:00
// auto-pause playback during break
if ( audio . src && ! audio . paused ) audio . pause ( ) ;
} else {
// break ended → reset to work
timerIsBreak = false ;
timerSeconds = TIMER _WORK ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
showTimerNotification ( 'Pause vorbei. Weiter geht’ s.' ) ;
2026-03-16 19:19:22 +01:00
}
renderTimer ( ) ;
}
}
function toggleTimer ( ) {
if ( timerRunning ) {
clearInterval ( timerInterval ) ;
timerInterval = null ;
timerRunning = false ;
} else {
if ( 'Notification' in window && Notification . permission === 'default' ) {
Notification . requestPermission ( ) ;
}
timerRunning = true ;
timerInterval = setInterval ( timerTick , 1000 ) ;
}
renderTimer ( ) ;
}
function resetTimer ( ) {
clearInterval ( timerInterval ) ;
timerInterval = null ;
timerRunning = false ;
timerIsBreak = false ;
timerSeconds = TIMER _WORK ;
renderTimer ( ) ;
}
function renderTimer ( ) {
const m = String ( Math . floor ( timerSeconds / 60 ) ) . padStart ( 2 , '0' ) ;
const s = String ( timerSeconds % 60 ) . padStart ( 2 , '0' ) ;
const display = $ ( 'timer-display' ) ;
const btn = $ ( 'timer-toggle-btn' ) ;
const label = $ ( 'timer-phase-label' ) ;
if ( display ) display . textContent = ` ${ m } : ${ s } ` ;
if ( btn ) btn . textContent = timerRunning ? '⏸' : '▶' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( label ) label . textContent = timerIsBreak ? 'Pause' : 'Fokus' ;
2026-03-16 19:19:22 +01:00
// colour the display red when break
if ( display ) display . style . color = timerIsBreak ? '#e63946' : '' ;
}
function showTimerNotification ( msg ) {
if ( 'Notification' in window && Notification . permission === 'granted' ) {
new Notification ( 'diora' , { body : msg } ) ;
}
// also flash in the timer label
const label = $ ( 'timer-phase-label' ) ;
if ( label ) { label . textContent = msg ; setTimeout ( ( ) => renderTimer ( ) , 3000 ) ; }
}
// ---------------------------------------------------------------------------
// Focus session recording
// ---------------------------------------------------------------------------
async function recordFocusSession ( ) {
try {
await fetch ( '/radio/focus/record/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( {
station _name : currentStation ? currentStation . name : '' ,
duration _minutes : 25 ,
} ) ,
} ) ;
loadFocusStats ( ) ;
} catch ( e ) { }
}
async function loadFocusStats ( ) {
try {
const res = await fetch ( '/radio/focus/stats/' ) ;
const data = await res . json ( ) ;
const widget = document . getElementById ( 'focus-today-widget' ) ;
if ( widget ) {
if ( data . today _sessions > 0 ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
widget . textContent = ` Heute: ${ data . today _sessions } ${ data . today _sessions !== 1 ? 'Sitzungen' : 'Sitzung' } · ${ data . today _minutes } min ` ;
2026-03-16 19:19:22 +01:00
widget . style . display = '' ;
} else {
widget . style . display = 'none' ;
}
}
// populate focus tab
const tbody = document . getElementById ( 'focus-tbody' ) ;
if ( ! tbody ) return ;
tbody . innerHTML = '' ;
if ( ! data . sessions . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
tbody . innerHTML = '<tr><td colspan="3" class="empty-msg">Noch keine Fokus-Sitzungen. Starte den Timer!</td></tr>' ;
2026-03-16 19:19:22 +01:00
return ;
}
data . sessions . forEach ( s => {
const tr = document . createElement ( 'tr' ) ;
const dt = new Date ( s . completed _at ) . toLocaleString ( [ ] , { dateStyle : 'short' , timeStyle : 'short' } ) ;
tr . innerHTML = ` <td> ${ dt } </td><td> ${ escapeHtml ( s . station _name || '—' ) } </td><td> ${ s . duration _minutes } min</td> ` ;
tbody . appendChild ( tr ) ;
} ) ;
} catch ( e ) { }
}
// ---------------------------------------------------------------------------
// Do Not Disturb / focus mode
// ---------------------------------------------------------------------------
let dndActive = false ;
function toggleDNDLight ( ) {
document . body . classList . toggle ( 'dnd-dark' ) ;
const btn = $ ( 'dnd-light-btn' ) ;
if ( btn ) btn . style . opacity = document . body . classList . contains ( 'dnd-dark' ) ? '0.4' : '1' ;
}
function toggleDND ( ) {
dndActive = ! dndActive ;
if ( ! dndActive ) document . body . classList . remove ( 'dnd-dark' ) ;
document . body . classList . toggle ( 'dnd-mode' , dndActive ) ;
const btn = $ ( 'dnd-btn' ) ;
if ( btn ) btn . classList . toggle ( 'active' , dndActive ) ;
if ( dndActive ) {
const el = document . documentElement ;
if ( el . requestFullscreen ) el . requestFullscreen ( ) ;
else if ( el . webkitRequestFullscreen ) el . webkitRequestFullscreen ( ) ;
} else {
if ( document . fullscreenElement && document . exitFullscreen ) document . exitFullscreen ( ) ;
else if ( document . webkitFullscreenElement && document . webkitExitFullscreen ) document . webkitExitFullscreen ( ) ;
}
}
// Exit DND on Escape (browser also exits fullscreen on Escape, so sync state)
document . addEventListener ( 'fullscreenchange' , ( ) => {
if ( ! document . fullscreenElement && dndActive ) {
dndActive = false ;
document . body . classList . remove ( 'dnd-mode' ) ;
const btn = $ ( 'dnd-btn' ) ;
if ( btn ) btn . classList . remove ( 'active' ) ;
}
} ) ;
document . addEventListener ( 'keydown' , e => {
if ( e . key === 'Escape' && dndActive ) toggleDND ( ) ;
} ) ;
// ---------------------------------------------------------------------------
// Mood / genre tag filter
// ---------------------------------------------------------------------------
const MOOD _TAGS = [
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
{ label : 'Fokus' , tag : 'ambient' } ,
2026-08-04 09:01:52 +02:00
{ label : 'Lo-fi' , tag : 'lofi' } ,
{ label : 'Jazz' , tag : 'jazz' } ,
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
{ label : 'Klassik' , tag : 'classical' } ,
2026-08-04 09:01:52 +02:00
{ label : 'Ambient' , tag : 'ambient' } ,
{ label : 'Metal' , tag : 'metal' } ,
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
{ label : 'Elektronisch' , tag : 'electronic' } ,
{ label : 'Wort' , tag : 'talk' } ,
2026-03-16 19:19:22 +01:00
] ;
function initMoodChips ( ) {
const container = $ ( 'mood-chips' ) ;
if ( ! container ) return ;
MOOD _TAGS . forEach ( ( { label , tag } ) => {
const btn = document . createElement ( 'button' ) ;
btn . className = 'mood-chip' ;
btn . textContent = label ;
btn . onclick = ( ) => {
const input = $ ( 'search-input' ) ;
if ( input ) input . value = tag ;
doSearch ( ) ;
} ;
container . appendChild ( btn ) ;
} ) ;
}
// ---------------------------------------------------------------------------
// Curated station lists
// ---------------------------------------------------------------------------
const CURATED _LISTS = [
{
id : 'focus' ,
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
label : 'Fokus' ,
2026-03-16 19:19:22 +01:00
stations : [
{ name : 'SomaFM Drone Zone' , url : 'https://ice6.somafm.com/dronezone-256-mp3' } ,
{ name : 'SomaFM Groove Salad' , url : 'https://ice5.somafm.com/groovesalad-128-aac' } ,
{ name : 'Nightride FM' , url : 'https://stream.nightride.fm/nightride.mp3' } ,
{ name : 'Nightride FM Chillsynth' , url : 'https://stream.nightride.fm/chillsynth.mp3' } ,
] ,
} ,
{
id : 'lofi' ,
2026-08-04 09:01:52 +02:00
label : 'Lo-fi / Chill' ,
2026-03-16 19:19:22 +01:00
stations : [
{ name : 'SomaFM Groove Salad Classic' , url : 'https://ice6.somafm.com/gsclassic-128-mp3' } ,
{ name : 'SomaFM Secret Agent' , url : 'https://ice4.somafm.com/secretagent-128-mp3' } ,
{ name : 'dublab DE' , url : 'https://dublabde.out.airtime.pro/dublabde_a' } ,
] ,
} ,
{
id : 'dark' ,
2026-08-04 09:01:52 +02:00
label : 'Dark / Industrial' ,
2026-03-16 19:19:22 +01:00
stations : [
{ name : 'SomaFM Doomed' , url : 'https://ice2.somafm.com/doomed-256-mp3' } ,
{ name : 'Nightride FM Darksynth' , url : 'https://stream.nightride.fm/darksynth.mp3' } ,
{ name : 'Radio Caprice Industrial' , url : 'http://79.120.39.202:9095/' } ,
] ,
} ,
{
id : 'classical' ,
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
label : 'Klassik' ,
2026-03-16 19:19:22 +01:00
stations : [
{ name : 'BR Klassik' , url : 'https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high' } ,
{ name : 'SWR Kultur' , url : 'https://f111.rndfnk.com/ard/swr/swr2/live/mp3/256/stream.mp3?aggregator=web' } ,
{ name : 'Deutschlandfunk Kultur' , url : 'https://st02.sslstream.dlf.de/dlf/02/high/aac/stream.aac?aggregator=web' } ,
] ,
} ,
] ;
function initCuratedLists ( ) {
const container = document . getElementById ( 'curated-lists' ) ;
if ( ! container ) return ;
2026-03-16 21:16:28 +01:00
if ( INITIAL _FEATURED && INITIAL _FEATURED . length ) {
const section = document . createElement ( 'div' ) ;
section . className = 'curated-section' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
section . innerHTML = ` <div class="curated-label">★ Empfohlen</div> ` ;
2026-03-16 21:16:28 +01:00
const ul = document . createElement ( 'ul' ) ;
ul . className = 'curated-stations' ;
INITIAL _FEATURED . forEach ( s => {
const li = document . createElement ( 'li' ) ;
li . innerHTML = ` <button class="btn btn-sm" onclick="playStation(' ${ escapeAttr ( s . url ) } ', ' ${ escapeAttr ( s . name ) } ', null)">▶</button>
< span class = "curated-name" > $ { escapeHtml ( s . name ) } < / s p a n >
$ { s . description ? ` <span class="muted" style="font-size:0.78rem"> ${ escapeHtml ( s . description ) } </span> ` : '' } ` ;
ul . appendChild ( li ) ;
} ) ;
section . appendChild ( ul ) ;
container . appendChild ( section ) ;
}
2026-03-16 19:19:22 +01:00
CURATED _LISTS . forEach ( list => {
const section = document . createElement ( 'div' ) ;
section . className = 'curated-section' ;
section . innerHTML = ` <div class="curated-label"> ${ list . label } </div> ` ;
const ul = document . createElement ( 'ul' ) ;
ul . className = 'curated-stations' ;
list . stations . forEach ( s => {
const li = document . createElement ( 'li' ) ;
li . innerHTML = ` <button class="btn btn-sm" onclick="playStation(' ${ escapeAttr ( s . url ) } ', ' ${ escapeAttr ( s . name ) } ', null)">▶</button>
< span class = "curated-name" > $ { escapeHtml ( s . name ) } < / s p a n > ` ;
ul . appendChild ( li ) ;
} ) ;
section . appendChild ( ul ) ;
container . appendChild ( section ) ;
} ) ;
}
// ---------------------------------------------------------------------------
2026-03-16 20:57:07 +01:00
// Donation hint
// ---------------------------------------------------------------------------
const DONATION _HINT _THRESHOLD = 10 ;
const DONATION _HINT _COOLDOWN _MS = 7 * 24 * 60 * 60 * 1000 ; // 7 days
function maybeShowDonationHint ( stationUrl , stationName ) {
const station = INITIAL _SAVED . find ( s => s . url === stationUrl ) ;
if ( ! station || station . play _count < DONATION _HINT _THRESHOLD ) return ;
const key = ` diora_donation_hint_ ${ stationUrl } ` ;
const last = parseInt ( localStorage . getItem ( key ) || '0' , 10 ) ;
if ( Date . now ( ) - last < DONATION _HINT _COOLDOWN _MS ) return ;
2026-08-04 09:01:52 +02:00
// Past the threshold just means "this station is a regular" — from then on
// every single play would otherwise qualify, so only actually show the hint
// on roughly 1 in 10 of those plays instead of every time.
if ( Math . random ( ) >= 0.1 ) return ;
2026-03-16 20:57:07 +01:00
const existing = document . getElementById ( 'donation-hint' ) ;
if ( existing ) existing . remove ( ) ;
const el = document . createElement ( 'div' ) ;
el . id = 'donation-hint' ;
el . innerHTML = `
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< span > Du hörst < strong > $ { escapeHtml ( stationName ) } < / s t r o n g > o f t – v i e l l e i c h t m a g s t d u d e n S e n d e r u n t e r s t ü t z e n ♥ < / s p a n >
< button onclick = "dismissDonationHint('${escapeAttr(stationUrl)}')" title = "Ausblenden" > ✕ < / b u t t o n >
2026-03-16 20:57:07 +01:00
` ;
document . body . appendChild ( el ) ;
setTimeout ( ( ) => dismissDonationHint ( stationUrl ) , 12000 ) ;
}
function dismissDonationHint ( stationUrl ) {
localStorage . setItem ( ` diora_donation_hint_ ${ stationUrl } ` , Date . now ( ) ) ;
const el = document . getElementById ( 'donation-hint' ) ;
if ( el ) { el . classList . add ( 'hiding' ) ; setTimeout ( ( ) => el . remove ( ) , 400 ) ; }
}
// ---------------------------------------------------------------------------
2026-03-16 19:19:22 +01:00
// Station notes
// ---------------------------------------------------------------------------
2026-08-04 09:01:52 +02:00
async function editNotes ( pk , current ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const note = await customPrompt ( 'Notiz zum Sender:' , current || '' ) ;
2026-03-16 19:19:22 +01:00
if ( note === null ) return ; // cancelled
fetch ( ` /radio/notes/ ${ pk } / ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { notes : note } ) ,
} ) . then ( r => {
if ( r . ok ) {
const cell = document . querySelector ( ` #saved-row- ${ pk } .notes-cell ` ) ;
if ( cell ) cell . textContent = note ;
}
} ) ;
}
// ---------------------------------------------------------------------------
// Tabs
// ---------------------------------------------------------------------------
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
const TOP _TABS = [ 'home' , 'radio' , 'focus' , 'podcasts' , 'books' , 'feeds' , 'ambient' ] ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const RADIO _SUB _TABS = [ 'search' , 'saved' , 'history' ] ;
2026-08-04 09:01:52 +02:00
// ---------------------------------------------------------------------------
// URL hash routing — makes tabs deep-linkable/shareable and lets the browser
// back/forward buttons move between them. Deliberately hash-based rather
// than real paths: /podcasts/ and /books/ etc. are already taken by the
// JSON API, so this avoids touching Django's urls.py at all.
// ---------------------------------------------------------------------------
let _routingFromHash = false ;
let _currentTopTab = 'radio' ;
function _syncHash ( ) {
if ( _routingFromHash ) return ;
let hash = _currentTopTab ;
if ( _currentTopTab === 'radio' ) {
hash += '/' + ( localStorage . getItem ( 'diora_active_radio_tab' ) || 'saved' ) ;
}
if ( location . hash . slice ( 1 ) !== hash ) history . pushState ( null , '' , '#' + hash ) ;
}
function _routeFromHash ( ) {
const [ tab , sub ] = ( location . hash || '' ) . replace ( /^#/ , '' ) . split ( '/' ) . filter ( Boolean ) ;
if ( ! TOP _TABS . includes ( tab ) ) return ;
_routingFromHash = true ;
try {
showTab ( tab ) ;
if ( tab === 'radio' && RADIO _SUB _TABS . includes ( sub ) ) showRadioTab ( sub ) ;
} finally {
_routingFromHash = false ;
}
}
window . addEventListener ( 'popstate' , _routeFromHash ) ;
window . addEventListener ( 'hashchange' , _routeFromHash ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
2026-05-30 00:12:22 +02:00
function showOfflineBanner ( offline ) {
2026-05-30 00:34:53 +02:00
document . body . classList . toggle ( 'app-offline' , offline ) ;
const uploadArea = $ ( 'book-upload-area' ) ;
if ( uploadArea ) uploadArea . style . display = offline ? 'none' : '' ;
2026-05-30 00:12:22 +02:00
}
2026-03-16 19:19:22 +01:00
function showTab ( name ) {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
TOP _TABS . forEach ( p => {
2026-03-16 19:19:22 +01:00
const panel = $ ( ` tab- ${ p } ` ) ;
if ( panel ) panel . style . display = ( p === name ) ? '' : 'none' ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
document . querySelectorAll ( '#tabs .tab-btn' ) . forEach ( ( btn , i ) => {
btn . classList . toggle ( 'active' , TOP _TABS [ i ] === name ) ;
2026-03-16 19:19:22 +01:00
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
localStorage . setItem ( 'diora_active_tab' , name ) ;
2026-08-04 09:01:52 +02:00
_currentTopTab = name ;
_syncHash ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( name === 'books' ) loadBookList ( ) ;
2026-10-03 23:08:57 +02:00
if ( name === 'ambient' ) _ambientLoad ( ) ;
Home: neue Startseite mit Weiterlesen, Weiterhören, Radio-Favoriten und Ambient; sw v87
- static/js/home.js rendert #tab-home aus dem, was die Seite schon hat:
zuletzt gelesenes Buch (und drei weitere), laufende oder nächste Folge mit
Restzeit und „N neue Folgen“, Favoriten-Sender (sonst die ersten
gespeicherten) und zuletzt gehörter Sender, Drift mit der zuletzt
gewählten Klangwelt und Sleep-Timer. Läuft im Browser, in diora desktop und
in der Android-App (dort mit static/js/ kopiert).
- app.js: Tab 'home' (TOP_TABS), window.__dioraAmbientStart() lädt Drift bei
Bedarf und startet es; die gewählte Klangwelt wird gemerkt und beim Laden
wiederhergestellt.
- podcasts-web.js: Startseite Home, wenn nichts anderes gewählt ist.
- native-podcast.js aus diora-android 7dcc0c6 (Home in der Startseiten-Wahl,
__dioraPodcastView).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:34:02 +02:00
if ( name === 'home' && window . _ _dioraHomeShow ) window . _ _dioraHomeShow ( ) ; // static/js/home.js
Feed-Reader: Tab „Feeds“ mit Nachrichten (Briefing, Stories) und Blogs
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:16:24 +02:00
if ( name === 'feeds' && window . _ _dioraFeedsShow ) window . _ _dioraFeedsShow ( ) ; // static/js/feeds.js
2026-03-16 19:19:22 +01:00
}
2026-10-03 23:08:57 +02:00
// ---------------------------------------------------------------------------
// Ambient: "Drift" (static/ambient/), a generative synthesizer in a frame of its
// own. It and the radio / podcast player take turns — one starting stops the
// other — by postMessage: {type: 'ambient', cmd: 'start'|'stop'} to the frame,
// {type: 'ambient', state: 'playing'|'stopped'} from it.
// ---------------------------------------------------------------------------
let _ambientPlaying = false ;
2026-10-04 00:18:06 +02:00
let _ambientLast = false ; // Ambient was started last: the player bar shows it (native-podcast.js)
2026-10-03 23:08:57 +02:00
function _ambientFrame ( ) {
return $ ( 'ambient-frame' ) ;
}
function _ambientLoad ( ) {
const f = _ambientFrame ( ) ;
if ( ! f ) return ;
2026-10-04 00:01:03 +02:00
if ( ! f . getAttribute ( 'src' ) ) {
2026-10-04 00:18:06 +02:00
// Drift takes the page's volume before its first sound; the bar follows its sound world.
f . addEventListener ( 'load' , ( ) => {
_ambientVolume ( ) ;
try {
Home: neue Startseite mit Weiterlesen, Weiterhören, Radio-Favoriten und Ambient; sw v87
- static/js/home.js rendert #tab-home aus dem, was die Seite schon hat:
zuletzt gelesenes Buch (und drei weitere), laufende oder nächste Folge mit
Restzeit und „N neue Folgen“, Favoriten-Sender (sonst die ersten
gespeicherten) und zuletzt gehörter Sender, Drift mit der zuletzt
gewählten Klangwelt und Sleep-Timer. Läuft im Browser, in diora desktop und
in der Android-App (dort mit static/js/ kopiert).
- app.js: Tab 'home' (TOP_TABS), window.__dioraAmbientStart() lädt Drift bei
Bedarf und startet es; die gewählte Klangwelt wird gemerkt und beim Laden
wiederhergestellt.
- podcasts-web.js: Startseite Home, wenn nichts anderes gewählt ist.
- native-podcast.js aus diora-android 7dcc0c6 (Home in der Startseiten-Wahl,
__dioraPodcastView).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:34:02 +02:00
const sel = f . contentDocument . getElementById ( 'mode' ) ;
// The sound world chosen last comes back (Drift itself starts with its default).
let last = '' ;
try { last = localStorage . getItem ( 'diora_ambient_mode' ) || '' ; } catch ( e ) { }
if ( last && last !== sel . value && [ ... sel . options ] . some ( o => o . value === last ) ) {
f . contentWindow . postMessage ( { type : 'ambient' , cmd : 'mode' , mode : last } , '*' ) ;
}
sel . addEventListener ( 'change' , ( ) => {
try {
localStorage . setItem ( 'diora_ambient_mode' , sel . value ) ;
localStorage . setItem ( 'diora_ambient_mode_label' , sel . selectedOptions [ 0 ] ? sel . selectedOptions [ 0 ] . text : '' ) ;
} catch ( e ) { }
if ( window . _ _dioraAmbientChanged ) window . _ _dioraAmbientChanged ( ) ;
} ) ;
2026-10-04 00:18:06 +02:00
} catch ( e ) { }
} ) ;
2026-10-04 00:01:03 +02:00
f . src = f . dataset . src ;
}
2026-10-03 23:08:57 +02:00
requestAnimationFrame ( _ambientSize ) ;
}
// The frame fills the room between the tabs and the player bar at the bottom.
function _ambientSize ( ) {
const f = _ambientFrame ( ) ;
if ( ! f || _currentTopTab !== 'ambient' ) return ;
const root = getComputedStyle ( document . documentElement ) ;
const bar = parseFloat ( root . getPropertyValue ( '--nui-dock-h' ) ) || parseFloat ( root . getPropertyValue ( '--dpw-bar-h' ) ) || 0 ;
const top = f . getBoundingClientRect ( ) . top + window . scrollY ;
f . style . height = Math . max ( 320 , Math . floor ( window . innerHeight - top - bar ) ) + 'px' ;
}
window . addEventListener ( 'resize' , ( ) => requestAnimationFrame ( _ambientSize ) ) ;
2026-10-04 00:07:07 +02:00
// The bar at the bottom grows or shrinks (the player appears, a second line …): its height
// comes as --nui-dock-h / --dpw-bar-h on <html>, set inline — measure again then.
new MutationObserver ( ( ) => requestAnimationFrame ( _ambientSize ) )
. observe ( document . documentElement , { attributes : true , attributeFilter : [ 'style' ] } ) ;
2026-10-03 23:08:57 +02:00
2026-10-03 23:22:00 +02:00
// A command for Drift ('start' | 'stop'): the app's notification, the sleep timer.
2026-10-04 00:01:03 +02:00
window . _ _dioraAmbientCmd = function ( cmd , value ) {
2026-10-03 23:22:00 +02:00
const f = _ambientFrame ( ) ;
2026-10-04 00:01:03 +02:00
if ( f && f . contentWindow && f . getAttribute ( 'src' ) ) f . contentWindow . postMessage ( { type : 'ambient' , cmd , value } , '*' ) ;
2026-10-03 23:22:00 +02:00
} ;
Home: neue Startseite mit Weiterlesen, Weiterhören, Radio-Favoriten und Ambient; sw v87
- static/js/home.js rendert #tab-home aus dem, was die Seite schon hat:
zuletzt gelesenes Buch (und drei weitere), laufende oder nächste Folge mit
Restzeit und „N neue Folgen“, Favoriten-Sender (sonst die ersten
gespeicherten) und zuletzt gehörter Sender, Drift mit der zuletzt
gewählten Klangwelt und Sleep-Timer. Läuft im Browser, in diora desktop und
in der Android-App (dort mit static/js/ kopiert).
- app.js: Tab 'home' (TOP_TABS), window.__dioraAmbientStart() lädt Drift bei
Bedarf und startet es; die gewählte Klangwelt wird gemerkt und beim Laden
wiederhergestellt.
- podcasts-web.js: Startseite Home, wenn nichts anderes gewählt ist.
- native-podcast.js aus diora-android 7dcc0c6 (Home in der Startseiten-Wahl,
__dioraPodcastView).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:34:02 +02:00
// Start Drift from elsewhere (Home): load the frame if needed, then start it — with the
// sound world chosen last, which the frame's load handler above sets.
window . _ _dioraAmbientStart = function ( ) {
const f = _ambientFrame ( ) ;
if ( ! f ) return ;
const go = ( ) => window . _ _dioraAmbientCmd ( 'start' ) ;
let ready = false ;
try { ready = ! ! f . getAttribute ( 'src' ) && f . contentDocument . readyState === 'complete' && ! ! f . contentDocument . getElementById ( 'mode' ) ; } catch ( e ) { }
if ( ready ) { go ( ) ; return ; }
f . addEventListener ( 'load' , ( ) => setTimeout ( go , 0 ) , { once : true } ) ;
_ambientLoad ( ) ;
} ;
2026-10-04 00:18:06 +02:00
// The radio or a podcast starts: Ambient stops, and the player bar is theirs again.
2026-10-03 23:08:57 +02:00
window . _ _dioraAmbientStop = function ( ) {
2026-10-03 23:22:00 +02:00
if ( _ambientPlaying ) window . _ _dioraAmbientCmd ( 'stop' ) ;
2026-10-04 00:18:06 +02:00
if ( _ambientLast ) {
_ambientLast = false ;
if ( window . _ _dioraAmbientChanged ) window . _ _dioraAmbientChanged ( ) ;
}
2026-10-03 23:08:57 +02:00
} ;
2026-10-03 23:22:00 +02:00
window . _ _dioraAmbientPlaying = ( ) => _ambientPlaying ;
2026-10-03 23:08:57 +02:00
2026-10-04 00:18:06 +02:00
// For the player bar: playing, started last, the sound world's name (the frame is ours).
window . _ _dioraAmbientInfo = function ( ) {
const f = _ambientFrame ( ) ;
let mode = '' ;
try {
const sel = f && f . contentDocument && f . contentDocument . getElementById ( 'mode' ) ;
if ( sel && sel . selectedOptions [ 0 ] ) mode = sel . selectedOptions [ 0 ] . text ;
} catch ( e ) { }
return { playing : _ambientPlaying , last : _ambientLast , mode } ;
} ;
2026-10-04 00:01:03 +02:00
// The page's volume (0– 255, the bar's slider) is Drift's too; its own slider hides when
// embedded. Not in the Android app: there the volume keys set it, as for everything.
function _ambientVolume ( val ) {
if ( window . DioraNative && DioraNative . ambientState ) return ;
if ( val == null ) {
const s = $ ( 'volume' ) ;
val = s ? + s . value : + ( localStorage . getItem ( 'diora_volume' ) || 102 ) ;
}
window . _ _dioraAmbientCmd && window . _ _dioraAmbientCmd ( 'volume' , val / 255 ) ;
}
2026-10-03 23:08:57 +02:00
window . addEventListener ( 'message' , e => {
const f = _ambientFrame ( ) ;
const d = e . data ;
if ( ! f || e . source !== f . contentWindow || ! d || d . type !== 'ambient' || ! d . state ) return ;
_ambientPlaying = d . state === 'playing' ;
2026-10-04 00:18:06 +02:00
if ( _ambientPlaying ) _ambientLast = true ;
if ( window . _ _dioraAmbientChanged ) window . _ _dioraAmbientChanged ( ) ;
2026-10-03 23:22:00 +02:00
// The app keeps it going with the screen off (AmbientService); the browser's sleep timer counts it.
if ( window . DioraNative && DioraNative . ambientState ) DioraNative . ambientState ( _ambientPlaying ) ;
if ( window . _ _dioraOnAmbient ) window . _ _dioraOnAmbient ( _ambientPlaying ) ;
2026-10-03 23:08:57 +02:00
if ( ! _ambientPlaying ) return ;
// Ambient starts: the radio and a podcast here stop.
if ( typeof isPlaying !== 'undefined' && isPlaying ) stopPlayback ( false ) ;
if ( window . _ _dioraPausePlayer ) window . _ _dioraPausePlayer ( ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function showRadioTab ( name ) {
RADIO _SUB _TABS . forEach ( p => {
const panel = $ ( ` tab- ${ p } ` ) ;
if ( panel ) panel . style . display = ( p === name ) ? '' : 'none' ;
} ) ;
2026-03-16 19:19:22 +01:00
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
document . querySelectorAll ( '#radio-sub-tabs .tab-btn' ) . forEach ( ( btn , i ) => {
btn . classList . toggle ( 'active' , RADIO _SUB _TABS [ i ] === name ) ;
2026-03-16 19:19:22 +01:00
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
localStorage . setItem ( 'diora_active_radio_tab' , name ) ;
2026-08-04 09:01:52 +02:00
_syncHash ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( name === 'saved' ) loadRecommendations ( ) ;
2026-03-16 19:19:22 +01:00
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function openSidebar ( title , htmlContent ) {
$ ( 'sidebar-title' ) . textContent = title ;
$ ( 'sidebar-body' ) . innerHTML = sanitizeSidebarHtml ( htmlContent ) ;
$ ( 'sidebar-overlay' ) . style . display = '' ;
$ ( 'sidebar' ) . classList . add ( 'open' ) ;
}
function closeSidebar ( ) {
$ ( 'sidebar' ) . classList . remove ( 'open' ) ;
$ ( 'sidebar-overlay' ) . style . display = 'none' ;
}
document . addEventListener ( 'keydown' , e => {
const overlay = $ ( 'reader-overlay' ) ;
const readerOpen = overlay && overlay . style . display !== 'none' ;
if ( e . key === 'Escape' ) {
if ( readerOpen ) {
closeReader ( ) ;
} else {
closeSidebar ( ) ;
}
}
if ( readerOpen ) {
if ( ( e . ctrlKey && e . key === 'f' ) || e . key === 'F3' ) {
e . preventDefault ( ) ;
toggleReaderSearch ( ) ;
}
if ( readerSearchOpen ) {
if ( e . key === 'ArrowDown' ) { e . preventDefault ( ) ; readerSearchNext ( ) ; }
if ( e . key === 'ArrowUp' ) { e . preventDefault ( ) ; readerSearchPrev ( ) ; }
}
if ( readerSettings . pdfPaginated && currentPdfDoc ) {
if ( e . key === 'ArrowRight' ) { e . preventDefault ( ) ; pdfGoToPage ( pdfCurrentPage + 1 ) ; }
if ( e . key === 'ArrowLeft' ) { e . preventDefault ( ) ; pdfGoToPage ( pdfCurrentPage - 1 ) ; }
}
2026-04-05 18:20:06 +02:00
// Vim-style scroll — ignore when typing in an input
const tag = document . activeElement ? . tagName ;
if ( tag !== 'INPUT' && tag !== 'TEXTAREA' ) {
const contentEl = $ ( 'reader-content' ) ;
if ( contentEl ) {
const large = Math . round ( contentEl . clientHeight * 0.85 ) ;
2026-08-14 23:31:01 +02:00
const continuousKeys = { 'j' : 1 , 'k' : - 1 } ;
2026-04-05 18:32:32 +02:00
if ( continuousKeys [ e . key ] !== undefined && ! e . repeat ) {
e . preventDefault ( ) ;
2026-08-14 23:50:23 +02:00
_readerScrollDir = continuousKeys [ e . key ] ;
if ( ! _readerScrollInterval ) {
_readerScrollLastTs = performance . now ( ) ;
_readerScrollInterval = setInterval ( _readerScrollTick , READER _SCROLL _TICK _MS ) ;
2026-04-05 18:32:32 +02:00
}
}
2026-04-05 18:20:06 +02:00
if ( e . key === 'd' ) { e . preventDefault ( ) ; contentEl . scrollBy ( { top : large / 2 , behavior : 'smooth' } ) ; }
if ( e . key === 'u' ) { e . preventDefault ( ) ; contentEl . scrollBy ( { top : - large / 2 , behavior : 'smooth' } ) ; }
if ( e . key === 'f' ) { e . preventDefault ( ) ; contentEl . scrollBy ( { top : large , behavior : 'smooth' } ) ; }
if ( e . key === 'b' ) { e . preventDefault ( ) ; contentEl . scrollBy ( { top : - large , behavior : 'smooth' } ) ; }
if ( e . key === 'g' ) { e . preventDefault ( ) ; contentEl . scrollTop = 0 ; }
if ( e . key === 'G' ) { e . preventDefault ( ) ; contentEl . scrollTop = contentEl . scrollHeight ; }
if ( currentPdfDoc && ! readerSettings . pdfPaginated ) {
if ( e . key === 'n' ) { e . preventDefault ( ) ; pdfGoToPage ( pdfCurrentPage + 1 ) ; }
if ( e . key === 'p' ) { e . preventDefault ( ) ; pdfGoToPage ( pdfCurrentPage - 1 ) ; }
}
}
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
} ) ;
2026-04-05 18:32:32 +02:00
document . addEventListener ( 'keyup' , e => {
if ( e . key === 'j' || e . key === 'k' ) {
2026-08-14 23:50:23 +02:00
clearInterval ( _readerScrollInterval ) ;
_readerScrollInterval = null ;
2026-08-14 23:31:01 +02:00
_readerScrollDir = 0 ;
2026-04-05 18:32:32 +02:00
}
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function sanitizeSidebarHtml ( html ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! html ) return '<p class="muted">Keine Shownotes vorhanden.</p>' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const div = document . createElement ( 'div' ) ;
div . innerHTML = html ;
div . querySelectorAll ( 'script, iframe, object, embed, style' ) . forEach ( el => el . remove ( ) ) ;
div . querySelectorAll ( '*' ) . forEach ( el => {
Array . from ( el . attributes ) . forEach ( attr => {
if ( attr . name . startsWith ( 'on' ) ) el . removeAttribute ( attr . name ) ;
} ) ;
if ( el . tagName === 'A' ) {
el . setAttribute ( 'target' , '_blank' ) ;
el . setAttribute ( 'rel' , 'noopener noreferrer' ) ;
}
} ) ;
return div . innerHTML ;
}
// ---------------------------------------------------------------------------
// Service Worker
// ---------------------------------------------------------------------------
if ( 'serviceWorker' in navigator ) {
window . addEventListener ( 'load' , ( ) => {
2026-08-14 23:31:01 +02:00
// Registered from '/sw.js' (root), not '/static/js/sw.js': a service
// worker's default scope is its own script's directory, so registering
// it from under /static/js/ would only ever let it control requests
// under that path — page navigations to '/', '/books/' etc. would never
// be intercepted, breaking offline entirely.
navigator . serviceWorker . register ( '/sw.js' , { scope : '/' } ) . catch ( err => {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
console . warn ( 'Service worker registration failed:' , err ) ;
} ) ;
2026-08-14 23:31:01 +02:00
// Drop any leftover registration from the old, wrongly-scoped '/static/js/sw.js'
// path so it doesn't keep running or fight the new root-scoped worker.
navigator . serviceWorker . getRegistrations ( ) . then ( regs => {
regs . forEach ( r => { if ( r . scope . endsWith ( '/static/js/' ) ) r . unregister ( ) ; } ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} ) ;
2026-05-30 00:19:00 +02:00
// When a new SW activates it sends SW_ACTIVATED — reload to get fresh assets,
2026-10-02 15:37:38 +02:00
// but never in the middle of something: not while a book is open, radio or a
// podcast plays, or a text field has the focus — then as soon as that's over.
let reloadPending = false ;
const busy = ( ) => {
if ( currentBookId || isPlaying ) return true ;
const host = window . DioraPodcastHost ;
if ( host && host . playerState ) {
try { if ( JSON . parse ( host . playerState ( ) ) . playing ) return true ; } catch ( _ ) { }
}
const el = document . activeElement ;
return ! ! el && ( el . isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/ . test ( el . tagName ) ) ;
} ;
const reloadWhenIdle = ( ) => {
if ( busy ( ) ) setTimeout ( reloadWhenIdle , 5000 ) ;
else window . location . reload ( ) ;
} ;
2026-05-30 00:19:00 +02:00
navigator . serviceWorker . addEventListener ( 'message' , e => {
2026-10-02 15:37:38 +02:00
if ( e . data ? . type === 'SW_ACTIVATED' && ! reloadPending ) {
reloadPending = true ;
reloadWhenIdle ( ) ;
2026-05-30 00:19:00 +02:00
}
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
// ---------------------------------------------------------------------------
// M3U import
// ---------------------------------------------------------------------------
async function importM3U ( input ) {
const file = input . files [ 0 ] ;
if ( ! file ) return ;
const status = document . getElementById ( 'import-status' ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status . textContent = 'Importiere …' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const form = new FormData ( ) ;
form . append ( 'file' , file ) ;
form . append ( 'csrfmiddlewaretoken' , getCsrfToken ( ) ) ;
try {
const res = await fetch ( '/radio/import/' , { method : 'POST' , body : form } ) ;
const data = await res . json ( ) ;
if ( data . ok ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status . textContent = ` ✓ ${ data . added } hinzugefügt, ${ data . skipped } schon gespeichert ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( data . added > 0 ) location . reload ( ) ;
} else {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status . textContent = ` Fehler: ${ _germanError ( data . error ) } ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
} catch ( e ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status . textContent = 'Hochladen fehlgeschlagen' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
input . value = '' ;
}
// ---------------------------------------------------------------------------
// Contrast scheme
// ---------------------------------------------------------------------------
// Accent palette — ordered by preference. Algorithm picks the one with the
// highest WCAG contrast ratio against the detected background luminance.
const ACCENT _PALETTE = [
{ base : '#e63946' , hover : '#ff4d58' } , // red
{ base : '#ff9500' , hover : '#ffaa33' } , // orange
{ base : '#f1c40f' , hover : '#f9d439' } , // yellow
{ base : '#2ecc71' , hover : '#4ee88a' } , // green
{ base : '#00b4d8' , hover : '#33c7e5' } , // cyan
{ base : '#4361ee' , hover : '#6d84f4' } , // blue
{ base : '#c77dff' , hover : '#d89fff' } , // purple
{ base : '#ff6b9d' , hover : '#ff8fb5' } , // pink
{ base : '#ffffff' , hover : '#cccccc' } , // white (last resort)
] ;
function _linearise ( c ) {
c /= 255 ;
return c <= 0.03928 ? c / 12.92 : Math . pow ( ( c + 0.055 ) / 1.055 , 2.4 ) ;
}
function _luminance ( r , g , b ) {
return 0.2126 * _linearise ( r ) + 0.7152 * _linearise ( g ) + 0.0722 * _linearise ( b ) ;
}
function _contrast ( l1 , l2 ) {
return ( Math . max ( l1 , l2 ) + 0.05 ) / ( Math . min ( l1 , l2 ) + 0.05 ) ;
}
function _hexRgb ( hex ) {
return [ parseInt ( hex . slice ( 1 , 3 ) , 16 ) , parseInt ( hex . slice ( 3 , 5 ) , 16 ) , parseInt ( hex . slice ( 5 , 7 ) , 16 ) ] ;
}
function analyzeBackground ( url ) {
return new Promise ( resolve => {
const img = new Image ( ) ;
img . crossOrigin = 'anonymous' ;
img . onload = ( ) => {
const canvas = document . createElement ( 'canvas' ) ;
canvas . width = 64 ; canvas . height = 64 ;
const ctx = canvas . getContext ( '2d' ) ;
ctx . drawImage ( img , 0 , 0 , 64 , 64 ) ;
const data = ctx . getImageData ( 0 , 0 , 64 , 64 ) . data ;
let tR = 0 , tG = 0 , tB = 0 , tBt601 = 0 ;
const n = data . length / 4 ;
for ( let i = 0 ; i < data . length ; i += 4 ) {
tR += data [ i ] ; tG += data [ i + 1 ] ; tB += data [ i + 2 ] ;
tBt601 += 0.299 * data [ i ] + 0.587 * data [ i + 1 ] + 0.114 * data [ i + 2 ] ;
}
resolve ( {
bright : ( tBt601 / n ) > 127 ,
bgLuminance : _luminance ( tR / n , tG / n , tB / n ) ,
} ) ;
} ;
img . onerror = ( ) => resolve ( { bright : false , bgLuminance : 0 } ) ;
img . src = url ;
} ) ;
}
function pickBestAccent ( bgLuminance ) {
let best = ACCENT _PALETTE [ 0 ] , bestRatio = 0 ;
for ( const entry of ACCENT _PALETTE ) {
const [ r , g , b ] = _hexRgb ( entry . base ) ;
const ratio = _contrast ( bgLuminance , _luminance ( r , g , b ) ) ;
if ( ratio > bestRatio ) { bestRatio = ratio ; best = entry ; }
}
return best ;
}
function applyAccent ( entry ) {
const root = document . documentElement ;
root . style . setProperty ( '--accent' , entry . base ) ;
root . style . setProperty ( '--accent-hover' , entry . hover ) ;
}
2026-10-14 18:02:33 +02:00
// Light and dark: the switch is in base.html (every page has it). The reader's
// "Wie die App" follows it.
window . addEventListener ( 'diora-theme' , ( ) => {
if ( currentBookId ) applyReaderSettings ( ! ! currentPdfDoc ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function setScheme ( bright ) {
document . body . classList . toggle ( 'bright-bg' , bright ) ;
const btn = document . getElementById ( 'contrast-toggle' ) ;
if ( btn ) btn . style . opacity = bright ? '1' : '0.5' ;
}
function toggleContrast ( ) {
setScheme ( ! document . body . classList . contains ( 'bright-bg' ) ) ;
}
// ---------------------------------------------------------------------------
// E2E Encryption utilities (Web Crypto API)
// ---------------------------------------------------------------------------
let _encKey = null ;
function bytesToBase64 ( buf ) {
const bytes = new Uint8Array ( buf ) ;
let str = '' ;
for ( const b of bytes ) str += String . fromCharCode ( b ) ;
return btoa ( str ) ;
}
function base64ToBytes ( b64 ) {
const str = atob ( b64 ) ;
const buf = new Uint8Array ( str . length ) ;
for ( let i = 0 ; i < str . length ; i ++ ) buf [ i ] = str . charCodeAt ( i ) ;
return buf ;
}
function bytesToHex ( buf ) {
return Array . from ( new Uint8Array ( buf ) ) . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
}
function hexToBytes ( hex ) {
const arr = new Uint8Array ( hex . length / 2 ) ;
for ( let i = 0 ; i < arr . length ; i ++ ) arr [ i ] = parseInt ( hex . slice ( i * 2 , i * 2 + 2 ) , 16 ) ;
return arr ;
}
async function getOrCreateEncKey ( ) {
if ( _encKey ) return _encKey ;
2026-03-19 22:16:22 +01:00
const storageKey = ` diora_enc_key_ ${ window . USER _ID || 0 } ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const stored = localStorage . getItem ( storageKey ) ;
if ( stored ) {
try {
const raw = base64ToBytes ( stored ) ;
_encKey = await crypto . subtle . importKey ( 'raw' , raw , { name : 'AES-GCM' } , false , [ 'encrypt' , 'decrypt' ] ) ;
return _encKey ;
2026-03-19 22:16:22 +01:00
} catch ( e ) { /* fall through, generate new */ }
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-03-19 22:16:22 +01:00
// No key found — generate one and store it
_encKey = await crypto . subtle . generateKey ( { name : 'AES-GCM' , length : 256 } , true , [ 'encrypt' , 'decrypt' ] ) ;
const raw = await crypto . subtle . exportKey ( 'raw' , _encKey ) ;
localStorage . setItem ( storageKey , bytesToBase64 ( new Uint8Array ( raw ) ) ) ;
return _encKey ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
async function encryptBytes ( key , plainBytes ) {
const iv = crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
const ct = await crypto . subtle . encrypt ( { name : 'AES-GCM' , iv } , key , plainBytes ) ;
return { iv : bytesToHex ( iv ) , ciphertext : bytesToBase64 ( ct ) } ;
}
async function decryptBytes ( key , ivHex , ctB64 ) {
const iv = hexToBytes ( ivHex ) ;
const ct = base64ToBytes ( ctB64 ) ;
return crypto . subtle . decrypt ( { name : 'AES-GCM' , iv } , key , ct ) ;
}
// ---------------------------------------------------------------------------
// Encrypted wallpaper
// ---------------------------------------------------------------------------
async function uploadBackground ( file ) {
if ( ! file ) return ;
const allowedTypes = [ 'image/jpeg' , 'image/png' , 'image/webp' ] ;
if ( ! allowedTypes . includes ( file . type ) ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
await customAlert ( 'Nur JPEG-, PNG- oder WebP-Bilder sind erlaubt.' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
return ;
}
2026-04-04 21:05:51 +02:00
if ( file . size > DIORA _CONFIG . bgMaxBytes ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
await customAlert ( ` Das Bild darf höchstens ${ DIORA _CONFIG . bgMaxBytes / 1024 / 1024 } MB groß sein. ` ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
return ;
}
const key = await getOrCreateEncKey ( ) ;
const buf = await file . arrayBuffer ( ) ;
const { iv , ciphertext } = await encryptBytes ( key , buf ) ;
const res = await fetch ( '/accounts/background/upload/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { iv , ciphertext , mime _type : file . type , file _size : file . size } ) ,
} ) ;
const data = await res . json ( ) ;
if ( ! data . ok ) throw new Error ( data . error || 'upload failed' ) ;
}
async function applyEncryptedBackground ( ) {
if ( typeof ENCRYPTED _BG === 'undefined' || ! ENCRYPTED _BG . ciphertext ) return ;
try {
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , ENCRYPTED _BG . iv , ENCRYPTED _BG . ciphertext ) ;
const blob = new Blob ( [ plain ] , { type : ENCRYPTED _BG . mime || 'image/jpeg' } ) ;
const url = URL . createObjectURL ( blob ) ;
document . body . style . backgroundImage = ` url(' ${ url } ') ` ;
2026-10-14 18:02:33 +02:00
const contrastBtn = document . getElementById ( 'contrast-toggle' ) ;
if ( contrastBtn ) contrastBtn . hidden = false ; // only with a picture behind the page
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
document . body . style . backgroundSize = 'cover' ;
document . body . style . backgroundPosition = 'center' ;
document . body . style . backgroundAttachment = 'fixed' ;
// Analyze brightness for accent/scheme
analyzeBackground ( url ) . then ( ( { bright , bgLuminance } ) => {
setScheme ( bright ) ;
applyAccent ( pickBestAccent ( bgLuminance ) ) ;
} ) ;
} catch ( e ) {
console . warn ( 'Could not decrypt background:' , e ) ;
}
}
// ---------------------------------------------------------------------------
// EPUB parser (requires JSZip)
// ---------------------------------------------------------------------------
function resolveEpubPath ( base , relative ) {
if ( ! relative ) return '' ;
if ( relative . startsWith ( '/' ) ) return relative . slice ( 1 ) ;
const hashIdx = relative . indexOf ( '#' ) ;
const frag = hashIdx >= 0 ? relative . slice ( hashIdx ) : '' ;
const rel = hashIdx >= 0 ? relative . slice ( 0 , hashIdx ) : relative ;
const parts = ( base + rel ) . split ( '/' ) ;
const resolved = [ ] ;
for ( const p of parts ) {
if ( p === '..' ) resolved . pop ( ) ;
else if ( p !== '.' ) resolved . push ( p ) ;
}
return resolved . join ( '/' ) + frag ;
}
async function parseEpub ( arrayBuffer ) {
const zip = await JSZip . loadAsync ( arrayBuffer ) ;
// 1. Find OPF via container.xml
const containerXml = await zip . file ( 'META-INF/container.xml' ) . async ( 'text' ) ;
const containerDoc = new DOMParser ( ) . parseFromString ( containerXml , 'application/xml' ) ;
const rootfileEl = containerDoc . querySelector ( 'rootfile' ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! rootfileEl ) throw new Error ( 'Ungültige EPUB-Datei (container.xml ohne rootfile)' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const opfPath = rootfileEl . getAttribute ( 'full-path' ) ;
const opfDir = opfPath . includes ( '/' ) ? opfPath . substring ( 0 , opfPath . lastIndexOf ( '/' ) + 1 ) : '' ;
// 2. Parse OPF
const opfText = await zip . file ( opfPath ) . async ( 'text' ) ;
const opfDoc = new DOMParser ( ) . parseFromString ( opfText , 'application/xml' ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const title = opfDoc . querySelector ( 'metadata > title, metadata > *|title' ) ? . textContent ? . trim ( ) || 'Unbekannter Titel' ;
const author = opfDoc . querySelector ( 'metadata > creator, metadata > *|creator' ) ? . textContent ? . trim ( ) || 'Unbekannter Autor' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// 3. Build manifest: id → {href, mediaType, properties}
const manifest = { } ;
opfDoc . querySelectorAll ( 'manifest > item' ) . forEach ( item => {
manifest [ item . getAttribute ( 'id' ) ] = {
href : opfDir + item . getAttribute ( 'href' ) ,
mediaType : item . getAttribute ( 'media-type' ) || '' ,
properties : item . getAttribute ( 'properties' ) || '' ,
} ;
} ) ;
2026-04-29 11:40:58 +02:00
// 4. Build image map: abs zip path → blob URL (parallel)
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const imageMap = { } ;
2026-04-29 11:40:58 +02:00
await Promise . all (
Object . values ( manifest )
. filter ( ( { mediaType } ) => mediaType . startsWith ( 'image/' ) )
. map ( async ( { href , mediaType } ) => {
try {
const buf = await zip . file ( href ) . async ( 'arraybuffer' ) ;
imageMap [ href ] = URL . createObjectURL ( new Blob ( [ buf ] , { type : mediaType } ) ) ;
} catch ( e ) { /* missing asset */ }
} )
) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// 5. Parse TOC
const toc = await _parseEpubToc ( zip , opfDoc , manifest ) ;
2026-04-29 11:40:58 +02:00
// 6. Get spine items; chapters are returned raw for progressive DOM injection
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const spineItems = Array . from ( opfDoc . querySelectorAll ( 'spine > itemref' ) )
. map ( ref => manifest [ ref . getAttribute ( 'idref' ) ] ? . href )
. filter ( Boolean ) ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
// All chapters unpacked at once: one after the other, every chapter waits for
// JSZip's own ticks (a book like Das Kapital has 148 of them).
const texts = await Promise . all ( spineItems . map ( href => {
const f = zip . file ( href ) ;
return f ? f . async ( 'text' ) . catch ( ( ) => null ) : null ;
} ) ) ;
2026-04-29 11:40:58 +02:00
const chapters = [ ] ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
let slice = performance . now ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
for ( let i = 0 ; i < spineItems . length ; i ++ ) {
const href = spineItems [ i ] ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
if ( texts [ i ] == null ) continue ; // missing
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
try {
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
let chapterText = texts [ i ] ;
texts [ i ] = null ;
2026-04-29 11:40:58 +02:00
// Strip style/script blocks early — publisher EPUBs often embed large base64 fonts in CSS.
// This shrinks the string before regex and DOMParser work on it.
chapterText = chapterText . replace ( /<style\b[^>]*>[\s\S]*?<\/style>/gi , '' ) ;
chapterText = chapterText . replace ( /<script\b[^>]*>[\s\S]*?<\/script>/gi , '' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const chapterDir = href . includes ( '/' ) ? href . substring ( 0 , href . lastIndexOf ( '/' ) + 1 ) : '' ;
const withBlobs = _injectImageBlobs ( chapterText , chapterDir , imageMap ) ;
const sanitized = sanitizeEpubHtml ( withBlobs ) ;
2026-04-29 11:40:58 +02:00
chapters . push ( ` <div id="epub-chapter- ${ i } " data-epub-src=" ${ href } "> ${ sanitized } </div> ` ) ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
// Let the page breathe (spinner, taps) every ~50 ms of work, not after a fixed count.
if ( performance . now ( ) - slice > 50 ) {
await new Promise ( r => setTimeout ( r , 0 ) ) ;
slice = performance . now ( ) ;
}
} catch ( e ) { /* skip unreadable */ }
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-04-29 11:40:58 +02:00
return { title , author , chapters , toc , imageMap } ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
async function _parseEpubToc ( zip , opfDoc , manifest ) {
// Try EPUB3 nav document
const navItem = Object . values ( manifest ) . find ( m => m . properties . includes ( 'nav' ) ) ;
if ( navItem ) {
try {
const navText = await zip . file ( navItem . href ) . async ( 'text' ) ;
const navDoc = new DOMParser ( ) . parseFromString ( navText , 'application/xhtml+xml' ) ;
const tocNav = navDoc . querySelector ( 'nav[epub\\:type="toc"]' ) || navDoc . querySelector ( 'nav' ) ;
if ( tocNav ) {
const ol = tocNav . querySelector ( 'ol' ) ;
if ( ol ) return _parseTocOl ( ol , navItem . href , 0 ) ;
}
} catch ( e ) { }
}
// Fall back to EPUB2 NCX
const ncxItem = Object . values ( manifest ) . find ( m => m . mediaType === 'application/x-dtbncx+xml' ) ;
if ( ncxItem ) {
try {
const ncxText = await zip . file ( ncxItem . href ) . async ( 'text' ) ;
const ncxDoc = new DOMParser ( ) . parseFromString ( ncxText , 'application/xml' ) ;
const ncxDir = ncxItem . href . includes ( '/' ) ? ncxItem . href . substring ( 0 , ncxItem . href . lastIndexOf ( '/' ) + 1 ) : '' ;
return _parseNcxNavMap ( ncxDoc . querySelector ( 'navMap' ) , ncxDir , 0 ) ;
} catch ( e ) { }
}
return [ ] ;
}
function _parseTocOl ( ol , navHref , depth ) {
if ( ! ol || depth > 5 ) return [ ] ;
const navDir = navHref . includes ( '/' ) ? navHref . substring ( 0 , navHref . lastIndexOf ( '/' ) + 1 ) : '' ;
const items = [ ] ;
for ( const li of Array . from ( ol . children ) ) {
const a = li . querySelector ( ':scope > a' ) || li . querySelector ( ':scope > span' ) ;
if ( a ) {
const rawHref = a . getAttribute ( 'href' ) || '' ;
items . push ( {
label : a . textContent . trim ( ) ,
href : rawHref ? resolveEpubPath ( navDir , rawHref ) : '' ,
depth ,
} ) ;
}
const childOl = li . querySelector ( ':scope > ol' ) ;
if ( childOl ) items . push ( ... _parseTocOl ( childOl , navHref , depth + 1 ) ) ;
}
return items ;
}
function _parseNcxNavMap ( navMap , ncxDir , depth ) {
if ( ! navMap || depth > 5 ) return [ ] ;
const items = [ ] ;
for ( const navPoint of Array . from ( navMap . children ) ) {
if ( navPoint . tagName !== 'navPoint' ) continue ;
const label = navPoint . querySelector ( 'navLabel > text' ) ? . textContent ? . trim ( ) || '' ;
const src = navPoint . querySelector ( 'content' ) ? . getAttribute ( 'src' ) || '' ;
items . push ( { label , href : src ? resolveEpubPath ( ncxDir , src ) : '' , depth } ) ;
items . push ( ... _parseNcxNavMap ( navPoint , ncxDir , depth + 1 ) ) ;
}
return items ;
}
function _resolveImageBlob ( imageMap , absPath ) {
if ( imageMap [ absPath ] ) return imageMap [ absPath ] ;
// Fallback: match by decoded filename only
const name = decodeURIComponent ( absPath . split ( '/' ) . pop ( ) ) . toLowerCase ( ) ;
for ( const [ k , v ] of Object . entries ( imageMap ) ) {
if ( decodeURIComponent ( k . split ( '/' ) . pop ( ) ) . toLowerCase ( ) === name ) return v ;
}
return null ;
}
// Replace image src/href in raw chapter HTML text with blob URLs before DOMParser sees it.
// This avoids relying on innerHTML serialisation preserving attributes we set on DOM nodes.
function _injectImageBlobs ( html , chapterDir , imageMap ) {
function subst ( src ) {
if ( ! src || src . startsWith ( 'blob:' ) || src . startsWith ( 'data:' ) || src . startsWith ( 'http' ) ) return src ;
return _resolveImageBlob ( imageMap , resolveEpubPath ( chapterDir , src ) ) || '' ;
}
// <img src="...">
html = html . replace ( /(<img\b[^>]*?)\bsrc="([^"]*)"/gi ,
( _ , pre , src ) => { const b = subst ( src ) ; return b ? ` ${ pre } src=" ${ b } " ` : pre ; } ) ;
// SVG <image xlink:href="...">
html = html . replace ( /(<image\b[^>]*?)\bxlink:href="([^"]*)"/gi ,
( _ , pre , src ) => { const b = subst ( src ) ; return b ? ` ${ pre } xlink:href=" ${ b } " ` : pre ; } ) ;
// SVG <image href="...">
html = html . replace ( /(<image\b[^>]*?)\bhref="([^"]*)"/gi ,
( _ , pre , src ) => { const b = subst ( src ) ; return b ? ` ${ pre } href=" ${ b } " ` : pre ; } ) ;
return html ;
}
2026-08-04 18:55:48 +02:00
// EPUBs mark footnote/endnote reference links very inconsistently across
// editions/tools — some use class="footnote" (same-file '#frag' href), some
// use the EPUB3 epub:type="noteref" attribute, and some (e.g. older
// Calibre/zeno.org conversions) use neither and just wrap a plain link to
// another chapter file's fragment in a <sup> — in either direction
// (<sup><a>…</a></sup> or <a><sup>…</sup></a>). Recognize all of these
// rather than only the single hardcoded class name.
function _looksLikeFootnoteLink ( el , href ) {
if ( ! href . includes ( '#' ) ) return false ;
const cls = ( el . getAttribute ( 'class' ) || '' ) . toLowerCase ( ) ;
if ( /\bnote|\bfootnote|\bfn\b/ . test ( cls ) ) return true ;
const epubType = ( el . getAttribute ( 'epub:type' ) || '' ) . toLowerCase ( ) ;
if ( epubType . includes ( 'noteref' ) ) return true ;
return ! ! ( el . closest ( 'sup' ) || el . querySelector ( 'sup' ) ) ;
}
2026-10-08 18:51:05 +02:00
// Chapters are XHTML, but parsed as HTML (forgiving with broken books). HTML
// knows no self-closing <a id="x"/>: the anchor stays open and swallows the
// text after it — and being a formatting element it is re-opened in every
// following paragraph (Suhrkamp/transpect books put one in each heading and
// note). Close the inline ones; block elements are left alone so the block
// indexes that highlights are anchored to stay the same.
const _SELF _CLOSING _INLINE = /<(a|span|b|i|em|strong|sup|sub|small|u|s|code|font|abbr|cite|q|mark|var)(\s(?:[^<>"']|"[^"]*"|'[^']*')*)?\/>/gi ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function sanitizeEpubHtml ( html ) {
2026-10-08 18:51:05 +02:00
html = html . replace ( _SELF _CLOSING _INLINE , '<$1$2></$1>' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const doc = new DOMParser ( ) . parseFromString ( html , 'text/html' ) ;
doc . querySelectorAll ( 'script, iframe, object, embed, style, head, meta, link' ) . forEach ( el => el . remove ( ) ) ;
doc . querySelectorAll ( '*' ) . forEach ( el => {
Array . from ( el . attributes ) . forEach ( attr => {
2026-05-29 21:21:43 +02:00
if ( attr . name . startsWith ( 'on' ) || attr . name === 'title' ) el . removeAttribute ( attr . name ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} ) ;
const tag = el . tagName . toLowerCase ( ) ;
if ( tag === 'img' ) {
const src = el . getAttribute ( 'src' ) || '' ;
// Remove any non-blob, non-data src that slipped through (broken relative paths)
if ( src && ! src . startsWith ( 'blob:' ) && ! src . startsWith ( 'data:' ) ) el . removeAttribute ( 'src' ) ;
}
2026-10-08 18:51:05 +02:00
// The printed edition's page numbers ("[12]") as small marks, not glued to the text.
const etype = el . getAttribute ( 'epub:type' ) || '' ;
if ( /\bpagebreak\b/ . test ( etype ) || el . getAttribute ( 'role' ) === 'doc-pagebreak' ) {
el . classList . add ( 'epub-pagenum' ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( el . tagName === 'A' ) {
const href = el . getAttribute ( 'href' ) || '' ;
if ( href . startsWith ( 'http://' ) || href . startsWith ( 'https://' ) ) {
el . setAttribute ( 'target' , '_blank' ) ;
el . setAttribute ( 'rel' , 'noopener noreferrer' ) ;
2026-08-04 18:55:48 +02:00
} else if ( _looksLikeFootnoteLink ( el , href ) ) {
// The whole book is one concatenated DOM, so a fragment id resolves
// via getElementById regardless of which chapter file it originally
// pointed into — strip any "../Text/other.html" path prefix and keep
// just the id after the last '#'.
el . dataset . footnoteRef = href . slice ( href . indexOf ( '#' ) + 1 ) ;
2026-05-29 21:04:47 +02:00
el . removeAttribute ( 'href' ) ;
el . style . cursor = 'pointer' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} else {
el . removeAttribute ( 'href' ) ;
el . style . cursor = 'default' ;
}
}
} ) ;
return doc . body ? doc . body . innerHTML : doc . documentElement . innerHTML ;
}
// ---------------------------------------------------------------------------
// Books
// ---------------------------------------------------------------------------
let currentBookId = null ;
let currentBookToc = [ ] ;
let currentImageMap = { } ;
let readerScrollSaveTimer = null ;
2026-04-01 15:41:30 +02:00
let _resizeObserver = null ;
let _currentPositionAnchor = '' ;
2026-08-04 13:51:05 +02:00
let _currentBookCharCount = 0 ;
2026-10-07 08:50:07 +02:00
// Rough characters per printed book page, used to estimate a real page count for EPUBs.
const CHARS _PER _BOOK _PAGE = 1800 ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const bookMetaCache = { } ; // id → {title, author, type}
2026-08-04 13:51:05 +02:00
// Reading-position percentages (the progress readout, annotation "% im Buch"
// coordinates): a whole percent is fine for a normal book, but on a long one
// (~600-700 physical pages and up) it barely moves during a whole scrolling
// session, which reads as broken. Past that length, show one decimal —
// formatted with the browser locale's own decimal separator.
const LONG _BOOK _CHAR _THRESHOLD = 1000000 ;
function _formatBookPercent ( fraction ) {
const pct = Math . max ( 0 , Math . min ( 100 , fraction * 100 ) ) ;
const decimals = _currentBookCharCount > LONG _BOOK _CHAR _THRESHOLD ? 1 : 0 ;
return pct . toLocaleString ( undefined , { minimumFractionDigits : decimals , maximumFractionDigits : decimals } ) ;
}
// Accepts both '45.3' and '45,3' (locale-typed input)
function _parseLocalePercent ( str ) {
const v = parseFloat ( String ( str ) . trim ( ) . replace ( ',' , '.' ) ) ;
return isNaN ( v ) ? 0 : v ;
}
2026-08-04 09:01:52 +02:00
// ---------------------------------------------------------------------------
// Reader "scrolled too far" safety net — offers a toast to jump back after a
// sudden, large scroll (accidental fling, fat-fingered keybind, etc.). Only
// watches organic scrolling; explicit destination jumps (TOC, bookmarks, the
// progress input, search) call _suppressScrollJumpDetect() first so they
// don't trigger it.
// ---------------------------------------------------------------------------
let _scrollRestTop = null ; // scrollTop last "settled" at, before the current burst
let _scrollRestDebounce = null ;
let _scrollJumpSuppressed = false ; // true while an explicit/programmatic jump is in flight
let _scrollJumpToastEl = null ;
// Marks the current (and any immediately-following, possibly animated) scroll
// as deliberate, so _checkScrollJump ignores it. Suppression lifts itself
// once scrolling actually settles (see _armScrollJumpSettle) — not after a
// fixed delay — so it stays active for however long a `smooth` scroll
// animation to a far-away target actually takes, and calling this again
// (e.g. from repeated resize-triggered corrections while a book settles)
// simply pushes that settle point further out instead of racing it.
function _suppressScrollJumpDetect ( ) {
_scrollJumpSuppressed = true ;
_armScrollJumpSettle ( ) ;
}
function _armScrollJumpSettle ( ) {
clearTimeout ( _scrollRestDebounce ) ;
_scrollRestDebounce = setTimeout ( ( ) => {
const contentEl = $ ( 'reader-content' ) ;
if ( contentEl ) _scrollRestTop = contentEl . scrollTop ;
_scrollJumpSuppressed = false ;
} , 500 ) ;
}
function _dismissScrollJumpToast ( ) {
if ( _scrollJumpToastEl ) {
_scrollJumpToastEl . remove ( ) ;
_scrollJumpToastEl = null ;
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
if ( _readPos && _readPos . hold === 'jump' ) _readPos . hold = false ;
2026-08-04 09:01:52 +02:00
}
function _showScrollJumpToast ( backTop ) {
_dismissScrollJumpToast ( ) ;
const toast = document . createElement ( 'div' ) ;
toast . className = 'reader-toast reader-toast-action' ;
toast . innerHTML = ` <span>Weit gescrollt</span><button type="button" class="reader-toast-btn">Zurückspringen</button> ` ;
document . body . appendChild ( toast ) ;
_scrollJumpToastEl = toast ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Until it's gone, the place before the jump stays the reading position.
if ( _readPos && ! _readPos . hold ) _readPos . hold = 'jump' ;
2026-08-04 09:01:52 +02:00
toast . querySelector ( '.reader-toast-btn' ) . addEventListener ( 'click' , ( ) => {
const contentEl = $ ( 'reader-content' ) ;
if ( contentEl ) {
_suppressScrollJumpDetect ( ) ;
contentEl . scrollTo ( { top : backTop , behavior : 'smooth' } ) ;
}
_dismissScrollJumpToast ( ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Back where it was: that's no new reading.
_readerInteractedAt = 0 ;
2026-08-04 09:01:52 +02:00
} ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
setTimeout ( ( ) => {
if ( _scrollJumpToastEl !== toast ) return ;
_dismissScrollJumpToast ( ) ;
// Stayed there: the far place is where the reading goes on.
_noteReaderInteraction ( ) ;
if ( _readPos ) _readPos . key = null ;
} , 6000 ) ;
2026-08-04 09:01:52 +02:00
}
function _checkScrollJump ( contentEl ) {
const top = contentEl . scrollTop ;
if ( _scrollRestTop === null ) _scrollRestTop = top ; // first sample this session
if ( ! _scrollJumpSuppressed && ! _scrollJumpToastEl ) {
const screen = contentEl . clientHeight || 1 ;
if ( Math . abs ( top - _scrollRestTop ) > screen * 3.5 ) {
_showScrollJumpToast ( _scrollRestTop ) ;
}
}
// Only adopt the new position as the "settled" baseline — and lift any
// suppression — once scrolling has paused for a bit. That's what lets a
// fast, still-in-progress fling (or a long smooth-scroll animation) be
// compared against/protected until it actually finishes, not mid-flight.
_armScrollJumpSettle ( ) ;
}
2026-04-01 15:41:30 +02:00
const EPUB _BLOCK _SELECTOR = 'p, h1, h2, h3, h4, h5, h6, li, blockquote, dt, dd, figcaption, div:not(:has(*))' ;
2026-10-08 19:01:40 +02:00
// The open book's text blocks (EPUB_BLOCK_SELECTOR), with the chapter each one
// belongs to — found once and kept until the book's content changes (a
// highlight wrapping text can turn a leaf div into a non-leaf one). Selecting
// them is the expensive part on an e-reader: Chrome 61 has no :has(), so
// compat61.js filters every div in JavaScript.
let _blockCache = null ; // {content, blocks, chapterOf: Int32Array, chapters: Element[]}
function _epubBlocks ( contentEl ) {
if ( _blockCache && _blockCache . content === contentEl ) return _blockCache ;
2026-04-01 15:41:30 +02:00
const blocks = Array . from ( contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ) ;
2026-10-08 19:01:40 +02:00
const chapters = Array . from ( contentEl . querySelectorAll ( '[data-epub-src]' ) ) ;
const chapterOf = new Int32Array ( blocks . length ) ;
for ( let i = 0 , ch = - 1 ; i < blocks . length ; i ++ ) {
while ( ch + 1 < chapters . length && chapters [ ch + 1 ] . compareDocumentPosition ( blocks [ i ] ) & Node . DOCUMENT _POSITION _FOLLOWING ) ch ++ ;
chapterOf [ i ] = ch ;
}
const observer = new MutationObserver ( ( ) => {
if ( _blockCache && _blockCache . content === contentEl ) _blockCache = null ;
observer . disconnect ( ) ;
} ) ;
observer . observe ( contentEl , { childList : true , subtree : true } ) ;
_blockCache = { content : contentEl , blocks , chapterOf , chapters } ;
return _blockCache ;
}
// The anchor of the reading position: "block:inner" — the last block (with a
// height) that starts at or above the top of the view, the first of several
// starting at the same height; else the first block below. Blocks are in
// document order and so top to bottom: a binary search over the chapters on
// screen (the e-reader mode shows one), not every block from the book's start.
function getPositionAnchor ( contentEl ) {
const { blocks , chapterOf , chapters } = _epubBlocks ( contentEl ) ;
2026-04-01 15:41:30 +02:00
if ( ! blocks . length ) return '' ;
const containerTop = contentEl . getBoundingClientRect ( ) . top ;
const containerBottom = containerTop + contentEl . clientHeight ;
2026-10-08 19:01:40 +02:00
// Index ranges of the blocks in chapters that are laid out (all, when reading by scrolling).
const shown = new Set ( ) ;
chapters . forEach ( ( ch , i ) => { if ( ch . getClientRects ( ) . length ) shown . add ( i ) ; } ) ;
const order = [ ] ;
2026-04-01 15:41:30 +02:00
for ( let i = 0 ; i < blocks . length ; i ++ ) {
2026-10-08 19:01:40 +02:00
if ( chapterOf [ i ] < 0 || shown . has ( chapterOf [ i ] ) ) {
let j = i ;
while ( j + 1 < blocks . length && chapterOf [ j + 1 ] === chapterOf [ i ] ) j ++ ;
order . push ( [ i , j ] ) ;
i = j ;
2026-04-01 15:41:30 +02:00
}
}
2026-10-08 19:01:40 +02:00
const count = order . reduce ( ( n , r ) => n + r [ 1 ] - r [ 0 ] + 1 , 0 ) ;
const at = k => { // k-th block on screen -> block index
for ( const [ a , b ] of order ) { if ( k <= b - a ) return a + k ; k -= b - a + 1 ; }
return - 1 ;
} ;
const rectOf = k => blocks [ at ( k ) ] . getBoundingClientRect ( ) ;
// A block without height says nothing about the position: use the next one with a height.
const measured = k => {
for ( let n = k ; n < count ; n ++ ) { const r = rectOf ( n ) ; if ( r . height >= 1 ) return { k : n , top : r . top } ; }
return null ;
} ;
// Binary search for the last block with a height whose top <= containerTop.
let lo = 0 , hi = count - 1 , best = - 1 ;
while ( lo <= hi ) {
const mid = ( lo + hi ) >> 1 ;
const m = measured ( mid ) ;
if ( m && m . top <= containerTop ) { best = m . k ; lo = m . k + 1 ; }
else hi = mid - 1 ;
}
let bestIndex ;
if ( best >= 0 ) {
// Several blocks starting at the same height (a blockquote and its first paragraph): the first.
const top = rectOf ( best ) . top ;
let k = best ;
for ( let n = best - 1 ; n >= 0 ; n -- ) {
const r = rectOf ( n ) ;
if ( r . height < 1 ) continue ;
if ( r . top !== top ) break ;
k = n ;
}
bestIndex = at ( k ) ;
} else {
const first = measured ( 0 ) ;
bestIndex = first && first . top <= containerBottom ? at ( first . k ) : 0 ;
}
2026-04-01 15:41:30 +02:00
const rect = blocks [ bestIndex ] . getBoundingClientRect ( ) ;
const innerFraction = Math . max ( 0 , Math . min ( 1 , ( containerTop - rect . top ) / ( rect . height || 1 ) ) ) ;
return ` ${ bestIndex } : ${ innerFraction . toFixed ( 6 ) } ` ;
}
2026-09-27 19:57:10 +02:00
// Reading progress as a share of the book's text (characters), from a
// position anchor. Independent of layout — font size, screen width, which
// chapters are rendered (the Android app's e-reader mode lays out one chapter
// at a time) — so every client shows the same percentage for the same place.
// The pixel share (scrollTop / scrollHeight) used before differed between
// layouts and counted margins, headings and images as reading.
let _charIndex = null ; // {content, starts[], lens[], total} for the open book
function _bookCharIndex ( contentEl ) {
if ( _charIndex && _charIndex . content === contentEl ) return _charIndex ;
const offsets = new Map ( ) ;
let pos = 0 ;
const walker = document . createTreeWalker ( contentEl , NodeFilter . SHOW _TEXT ) ;
let node ;
while ( ( node = walker . nextNode ( ) ) ) {
offsets . set ( node , pos ) ;
pos += node . nodeValue . length ;
}
const blocks = contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ;
const starts = new Array ( blocks . length ) ;
const lens = new Array ( blocks . length ) ;
for ( let i = 0 ; i < blocks . length ; i ++ ) {
const first = document . createTreeWalker ( blocks [ i ] , NodeFilter . SHOW _TEXT ) . nextNode ( ) ;
starts [ i ] = first ? offsets . get ( first ) : ( i ? starts [ i - 1 ] + lens [ i - 1 ] : 0 ) ;
lens [ i ] = blocks [ i ] . textContent . length ;
}
_charIndex = { content : contentEl , starts , lens , total : pos || 1 } ;
return _charIndex ;
}
// Share of the text before the anchored place (0..1), or null.
function _fractionFromAnchor ( contentEl , anchor ) {
const [ block , inner ] = _anchorParts ( anchor ) ;
if ( block < 0 ) return null ;
const idx = _bookCharIndex ( contentEl ) ;
if ( block >= idx . starts . length ) return null ;
return Math . max ( 0 , Math . min ( 1 , ( idx . starts [ block ] + inner * idx . lens [ block ] ) / idx . total ) ) ;
}
// Anchor of the place where `fraction` of the text has been read.
function _anchorForFraction ( contentEl , fraction ) {
const idx = _bookCharIndex ( contentEl ) ;
if ( ! idx . starts . length ) return '' ;
const target = Math . max ( 0 , Math . min ( 1 , fraction ) ) * idx . total ;
let lo = 0 , hi = idx . starts . length - 1 ;
while ( lo < hi ) {
const mid = ( lo + hi + 1 ) >> 1 ;
if ( idx . starts [ mid ] <= target ) lo = mid ; else hi = mid - 1 ;
}
const inner = idx . lens [ lo ] ? Math . max ( 0 , Math . min ( 1 , ( target - idx . starts [ lo ] ) / idx . lens [ lo ] ) ) : 0 ;
return ` ${ lo } : ${ inner . toFixed ( 6 ) } ` ;
}
2026-04-01 15:41:30 +02:00
function restoreFromAnchor ( contentEl , anchor ) {
if ( ! anchor ) return false ;
const parts = anchor . split ( ':' ) ;
if ( parts . length !== 2 ) return false ;
const idx = parseInt ( parts [ 0 ] , 10 ) ;
const innerFraction = parseFloat ( parts [ 1 ] ) ;
if ( isNaN ( idx ) || isNaN ( innerFraction ) ) return false ;
const blocks = Array . from ( contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ) ;
if ( idx >= blocks . length ) return false ;
const el = blocks [ idx ] ;
contentEl . scrollTop = el . offsetTop + Math . round ( innerFraction * el . offsetHeight ) ;
return true ;
}
2026-07-10 20:52:27 +02:00
// Split 'blockIndex:innerFraction' into [block, inner]; [-1, 0] if invalid/empty.
function _anchorParts ( anchor ) {
if ( typeof anchor !== 'string' ) return [ - 1 , 0 ] ;
const c = anchor . indexOf ( ':' ) ;
if ( c < 0 ) return [ - 1 , 0 ] ;
const b = parseInt ( anchor . slice ( 0 , c ) , 10 ) ;
const f = parseFloat ( anchor . slice ( c + 1 ) ) ;
return [ Number . isFinite ( b ) ? b : - 1 , Number . isFinite ( f ) ? f : 0 ] ;
}
// >0 if position A is further into the book than B, <0 if behind, 0 if equal.
// Compares by anchor block index (precise); falls back to scroll_fraction only
// when an anchor is missing (e.g. PDFs), matching the server's _progress_is_further.
function _cmpProgress ( anchorA , fracA , anchorB , fracB ) {
const [ ba , ia ] = _anchorParts ( anchorA ) ;
const [ bb , ib ] = _anchorParts ( anchorB ) ;
if ( ba >= 0 && bb >= 0 ) return ba !== bb ? ba - bb : ia - ib ;
return ( fracA || 0 ) - ( fracB || 0 ) ;
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// ---------------------------------------------------------------------------
// Reading position: the place last read wins, on any device (docs/progress-sync.md).
//
// A position counts as read when the reader got there by reading (scrolling,
// turning pages, choosing a chapter); its time is that moment, not the time it
// is saved. So re-saving an unchanged place, on a timer or on closing, carries
// the old time and can never beat a newer place read on another device. A book
// just opened at the server's place has read nothing here yet: nothing is sent.
// Own writes wait in a small outbox (localStorage) until the server has them.
// ---------------------------------------------------------------------------
let _serverOffset = 0 ; // the server's clock minus this device's (ms)
let _readPos = null ; // the open book: {bookId, ready, key, readAt (device ms), moved, hold}
let _readerInteractedAt = 0 ;
function _progressDevice ( ) {
return window . DIORA _DEVICE _NAME || 'Browser' ;
}
function _noteServerNow ( ms ) {
if ( typeof ms === 'number' && ms > 1e12 ) _serverOffset = ms - Date . now ( ) ;
}
function _noteServerNowHeader ( res ) {
try { _noteServerNow ( Number ( res . headers . get ( 'X-Server-Now' ) ) ) ; } catch ( e ) { }
}
// The reader did something (scroll wheel, touch, key, mouse): the next change
// of position is reading, not a layout settling.
function _noteReaderInteraction ( ) {
_readerInteractedAt = Date . now ( ) ;
if ( _readPos && _readPos . hold === 'until-moved' ) _readPos . hold = false ;
}
window . _ _dioraReaderInteraction = _noteReaderInteraction ;
// While a jump is only a look (a footnote's place, "Zurück zum Text"), the
// place before it stays the reading position. true: until the next
// interaction; false: released now.
window . _ _dioraHoldProgress = function ( on ) {
if ( _readPos ) _readPos . hold = on ? 'until-moved' : false ;
} ;
function _positionKey ( anchor , fraction ) {
return anchor || 'f' + ( fraction || 0 ) . toFixed ( 4 ) ;
}
// Called with every computed position: a change that follows an interaction is reading.
function _noteReadingPosition ( anchor , fraction ) {
if ( ! _readPos || ! _readPos . ready || _readPos . hold ) return ;
const key = _positionKey ( anchor , fraction ) ;
if ( key === _readPos . key ) return ;
_readPos . key = key ;
if ( Date . now ( ) - _readerInteractedAt < 10000 ) {
_readPos . readAt = Date . now ( ) ;
_readPos . moved = true ;
}
}
function _progressOutboxKey ( ) {
return ` diora_progress_outbox_ ${ window . USER _ID || 0 } ` ;
}
function _progressOutbox ( ) {
try { return JSON . parse ( localStorage . getItem ( _progressOutboxKey ( ) ) || '{}' ) || { } ; } catch ( e ) { return { } ; }
}
function _progressOutboxPut ( entry ) {
try {
const box = _progressOutbox ( ) ;
const old = box [ entry . book ] ;
if ( old && ! entry . force && old . read _at > entry . read _at ) return ;
box [ entry . book ] = entry ;
localStorage . setItem ( _progressOutboxKey ( ) , JSON . stringify ( box ) ) ;
} catch ( e ) { }
}
function _progressOutboxDone ( bookId , readAt ) {
try {
const box = _progressOutbox ( ) ;
if ( box [ bookId ] && box [ bookId ] . read _at === readAt ) {
delete box [ bookId ] ;
localStorage . setItem ( _progressOutboxKey ( ) , JSON . stringify ( box ) ) ;
}
} catch ( e ) { }
}
function _progressBody ( e ) {
return JSON . stringify ( {
scroll _fraction : e . fraction , position _anchor : e . anchor , read _at : e . read _at ,
sent _at : Date . now ( ) , device : e . device , ... ( e . force && { force : true } ) ,
} ) ;
}
// Send one queued position; true once the server has it (stored or not: it decides).
async function _sendProgress ( e ) {
try {
const res = await fetch ( ` /books/ ${ e . book } /progress/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : _progressBody ( e ) ,
} ) ;
if ( ! ( res . ok || ( res . status >= 400 && res . status < 500 ) ) ) return false ;
try { _noteServerNow ( ( await res . json ( ) ) . server _now ) ; } catch ( err ) { }
_progressOutboxDone ( e . book , e . read _at ) ;
return true ;
} catch ( err ) {
return false ;
}
}
let _progressFlushing = null ;
function _flushProgressOutbox ( ) {
if ( ! _progressFlushing ) {
_progressFlushing = ( async ( ) => {
for ( const e of Object . values ( _progressOutbox ( ) ) ) await _sendProgress ( e ) ;
} ) ( ) . finally ( ( ) => { _progressFlushing = null ; } ) ;
}
return _progressFlushing ;
}
// A book's state from the server list, made newer by an own write the server doesn't have yet.
function _withPendingProgress ( b ) {
const e = _progressOutbox ( ) [ b . id ] ;
if ( ! e ) return b ;
const at = e . read _at + _serverOffset ;
if ( b . read _at != null && at < b . read _at && ! e . force ) return b ;
return { ... b , scroll _fraction : e . fraction , position _anchor : e . anchor , read _at : at ,
device : e . device , last _read : new Date ( at ) . toISOString ( ) } ;
}
function _bookRecency ( b ) {
if ( typeof b . read _at === 'number' ) return b . read _at ;
const t = b . last _read ? Date . parse ( b . last _read ) : NaN ;
return isNaN ( t ) ? 0 : t ;
}
function _readerToast ( text , button , onClick , ms ) {
const toast = document . createElement ( 'div' ) ;
toast . className = 'reader-toast reader-toast-action' ;
toast . innerHTML = '<span></span><button type="button" class="reader-toast-btn"></button>' ;
toast . querySelector ( 'span' ) . textContent = text ;
toast . querySelector ( 'button' ) . textContent = button ;
document . body . appendChild ( toast ) ;
const done = ( ) => toast . remove ( ) ;
toast . querySelector ( 'button' ) . addEventListener ( 'click' , ( ) => { done ( ) ; onClick ( ) ; } ) ;
setTimeout ( done , ms || 10000 ) ;
return toast ;
}
function _jumpToPosition ( anchor , fraction ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
_suppressScrollJumpDetect ( ) ;
if ( currentPdfDoc && readerSettings . pdfPaginated && pdfTotalPages > 1 ) {
pdfCurrentPage = Math . max ( 1 , Math . round ( fraction * ( pdfTotalPages - 1 ) ) + 1 ) ;
enterPdfPaginatedMode ( ) ;
} else if ( currentPdfDoc ) {
contentEl . scrollTop = fraction * ( contentEl . scrollHeight - contentEl . clientHeight ) ;
} else if ( ! restoreFromAnchor ( contentEl , anchor ) ) {
restoreFromAnchor ( contentEl , _anchorForFraction ( contentEl , fraction ) ) ;
}
}
// Another device read this book after the place shown here: offer to go there.
let _newerOfferShown = null ;
async function _checkNewerPositionElsewhere ( ) {
const pos = _readPos ;
if ( ! pos || ! pos . ready || ! currentBookId || pos . bookId !== currentBookId ) return ;
let b ;
try {
const res = await fetch ( '/books/' , { cache : 'no-store' } ) ;
_noteServerNowHeader ( res ) ;
b = ( await res . json ( ) ) . find ( x => x . id === pos . bookId ) ;
} catch ( e ) { return ; }
if ( ! b || b . read _at == null || _readPos !== pos ) return ;
if ( b . read _at <= pos . readAt + _serverOffset + 3000 ) return ;
if ( _positionKey ( b . position _anchor || '' , b . scroll _fraction ) === pos . key ) return ;
const offerKey = ` ${ pos . bookId } : ${ b . read _at } ` ;
if ( _newerOfferShown === offerKey ) return ;
_newerOfferShown = offerKey ;
const where = b . device ? ` Auf ${ b . device } ` : 'Auf einem anderen Gerät' ;
_readerToast ( ` ${ where } weitergelesen · ${ _formatBookPercent ( b . scroll _fraction || 0 ) } % ` , 'Dorthin' , ( ) => {
if ( _readPos !== pos ) return ;
_jumpToPosition ( b . position _anchor || '' , b . scroll _fraction || 0 ) ;
// That place and its time are the server's: nothing new to tell it.
pos . key = null ;
pos . readAt = b . read _at - _serverOffset ;
pos . moved = false ;
setTimeout ( ( ) => {
const contentEl = $ ( 'reader-content' ) ;
if ( _readPos === pos && contentEl ) {
pos . key = currentPdfDoc ? _positionKey ( '' , b . scroll _fraction || 0 ) : _positionKey ( getPositionAnchor ( contentEl ) , 0 ) ;
}
} , 300 ) ;
} , 15000 ) ;
}
window . _ _dioraCheckNewerPosition = _checkNewerPositionElsewhere ;
2026-04-01 16:10:03 +02:00
// ---------------------------------------------------------------------------
// Book cache — IndexedDB, evict after 4 weeks of inactivity
// ---------------------------------------------------------------------------
2026-05-29 21:14:40 +02:00
const _BOOK _CACHE _STORE = 'books' ;
const _BOOK _META _STORE = 'book_meta' ;
const _BOOK _ANNOTATIONS _STORE = 'book_annotations' ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
const _BOOK _PARSED _STORE = 'parsed' ; // prepared chapters, see _getParsedBook
2026-05-29 21:14:40 +02:00
const _BOOK _CACHE _EVICT _MS = 4 * 7 * 24 * 60 * 60 * 1000 ;
2026-04-01 16:10:03 +02:00
function _openBookCacheDb ( ) {
return new Promise ( ( resolve , reject ) => {
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
const req = indexedDB . open ( 'diora_book_cache' , 3 ) ;
2026-05-29 21:14:40 +02:00
req . onupgradeneeded = e => {
const db = e . target . result ;
if ( ! db . objectStoreNames . contains ( _BOOK _CACHE _STORE ) )
db . createObjectStore ( _BOOK _CACHE _STORE , { keyPath : 'id' } ) ;
if ( ! db . objectStoreNames . contains ( _BOOK _META _STORE ) )
db . createObjectStore ( _BOOK _META _STORE , { keyPath : 'id' } ) ;
if ( ! db . objectStoreNames . contains ( _BOOK _ANNOTATIONS _STORE ) )
db . createObjectStore ( _BOOK _ANNOTATIONS _STORE , { keyPath : 'id' } ) ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
if ( ! db . objectStoreNames . contains ( _BOOK _PARSED _STORE ) )
db . createObjectStore ( _BOOK _PARSED _STORE , { keyPath : 'id' } ) ;
2026-05-29 21:14:40 +02:00
} ;
2026-04-01 16:10:03 +02:00
req . onsuccess = e => resolve ( e . target . result ) ;
req . onerror = ( ) => reject ( ) ;
} ) ;
}
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
// ---------------------------------------------------------------------------
// Prepared books: an EPUB's chapters after unpacking and sanitizing, so opening
// it again skips both (for a book like Das Kapital that's most of the time an
// e-reader needs to open it). Encrypted with the book key like everything else
// on the device. Valid for one version of the book file (its IV) and of the
// code that prepares chapters (a hash of its source: changing the sanitizer
// makes every book be prepared anew). Images stay in the book file: the
// chapters keep "diora-img:<path>" in place of the session's blob URLs.
// ---------------------------------------------------------------------------
let _parseCodeSig = null ;
function _parsePreparationSig ( ) {
if ( _parseCodeSig ) return _parseCodeSig ;
const src = [ parseEpub , sanitizeEpubHtml , _injectImageBlobs , _resolveImageBlob , _parseEpubToc ]
. map ( f => String ( f ) ) . join ( '\n' ) ;
let h = 5381 ;
for ( let i = 0 ; i < src . length ; i ++ ) h = ( ( h << 5 ) + h + src . charCodeAt ( i ) ) | 0 ;
return ( _parseCodeSig = ( h >>> 0 ) . toString ( 36 ) + '.' + src . length ) ;
}
async function _getParsedBook ( bookId , dataIv , key ) {
try {
const db = await _openBookCacheDb ( ) ;
const row = await new Promise ( resolve => {
const req = db . transaction ( _BOOK _PARSED _STORE ) . objectStore ( _BOOK _PARSED _STORE ) . get ( bookId ) ;
req . onsuccess = e => resolve ( e . target . result || null ) ;
req . onerror = ( ) => resolve ( null ) ;
} ) ;
if ( ! row || row . sig !== dataIv + ':' + _parsePreparationSig ( ) ) return null ;
const plain = await crypto . subtle . decrypt ( { name : 'AES-GCM' , iv : row . iv } , key , row . ct ) ;
return JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
} catch ( e ) { return null ; }
}
async function _setParsedBook ( bookId , dataIv , key , result ) {
try {
// Blob URLs live as long as this page; the stored chapters name the image instead.
const pathOf = { } ;
for ( const [ path , url ] of Object . entries ( result . imageMap || { } ) ) pathOf [ url ] = path ;
const blobRe = /blob:[^"'\s)]+/g ;
const chapters = result . chapters . map ( c => c . replace ( blobRe , u => pathOf [ u ] ? 'diora-img:' + pathOf [ u ] : u ) ) ;
const doc = { title : result . title , author : result . author , toc : result . toc , chapters ,
images : Object . keys ( result . imageMap || { } ) } ;
// Raw bytes, not base64: IndexedDB keeps an ArrayBuffer as it is (megabytes for a big book).
const iv = crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
const ct = await crypto . subtle . encrypt ( { name : 'AES-GCM' , iv } , key , new TextEncoder ( ) . encode ( JSON . stringify ( doc ) ) ) ;
const db = await _openBookCacheDb ( ) ;
await new Promise ( resolve => {
const tx = db . transaction ( _BOOK _PARSED _STORE , 'readwrite' ) ;
const store = tx . objectStore ( _BOOK _PARSED _STORE ) ;
store . put ( { id : bookId , sig : dataIv + ':' + _parsePreparationSig ( ) , iv , ct , cached _at : Date . now ( ) } ) ;
// Prepared books not opened for a while go (they come back on the next open).
const all = store . getAll ( ) ;
all . onsuccess = ( ) => all . result . forEach ( r => {
if ( r . id !== bookId && Date . now ( ) - ( r . cached _at || 0 ) > _BOOK _CACHE _EVICT _MS ) store . delete ( r . id ) ;
} ) ;
tx . oncomplete = resolve ;
tx . onerror = resolve ;
} ) ;
} catch ( e ) { }
}
/** A prepared book back to what parseEpub returns: fresh blob URLs for its images. */
async function _restoreParsedBook ( doc , epubBytes ) {
const imageMap = { } ;
if ( doc . images && doc . images . length ) {
const zip = await JSZip . loadAsync ( epubBytes ) ;
await Promise . all ( doc . images . map ( async path => {
const f = zip . file ( path ) ;
if ( ! f ) return ;
try {
const buf = await f . async ( 'arraybuffer' ) ;
const ext = path . split ( '.' ) . pop ( ) . toLowerCase ( ) ;
const type = { jpg : 'image/jpeg' , jpeg : 'image/jpeg' , png : 'image/png' , gif : 'image/gif' ,
svg : 'image/svg+xml' , webp : 'image/webp' } [ ext ] || '' ;
imageMap [ path ] = URL . createObjectURL ( new Blob ( [ buf ] , type ? { type } : { } ) ) ;
} catch ( e ) { }
} ) ) ;
}
const placeholder = /diora-img:([^"'\s)]+)/g ;
const chapters = doc . chapters . map ( c => c . replace ( placeholder , ( m , path ) => imageMap [ path ] || '' ) ) ;
return { title : doc . title , author : doc . author , toc : doc . toc , chapters , imageMap } ;
}
2026-04-01 16:10:03 +02:00
async function _getCachedBook ( bookId ) {
try {
const db = await _openBookCacheDb ( ) ;
return await new Promise ( ( resolve ) => {
const req = db . transaction ( _BOOK _CACHE _STORE ) . objectStore ( _BOOK _CACHE _STORE ) . get ( bookId ) ;
req . onsuccess = e => resolve ( e . target . result || null ) ;
req . onerror = ( ) => resolve ( null ) ;
} ) ;
} catch { return null ; }
}
async function _setCachedBook ( bookId , data _ct , data _iv ) {
try {
const db = await _openBookCacheDb ( ) ;
await new Promise ( ( resolve ) => {
const tx = db . transaction ( _BOOK _CACHE _STORE , 'readwrite' ) ;
tx . objectStore ( _BOOK _CACHE _STORE ) . put ( { id : bookId , data _ct , data _iv , cached _at : Date . now ( ) } ) ;
tx . oncomplete = resolve ;
tx . onerror = resolve ;
} ) ;
2026-05-29 23:54:54 +02:00
// Request persistent storage so the browser doesn't evict cached books
// under storage pressure. Chrome grants this silently for installed PWAs;
// Firefox may show a permission prompt.
if ( navigator . storage ? . persist ) navigator . storage . persist ( ) ;
2026-04-01 16:10:03 +02:00
} catch { }
}
2026-05-25 17:03:59 +02:00
async function _evictCachedBook ( bookId ) {
try {
const db = await _openBookCacheDb ( ) ;
await new Promise ( ( resolve ) => {
const tx = db . transaction ( _BOOK _CACHE _STORE , 'readwrite' ) ;
tx . objectStore ( _BOOK _CACHE _STORE ) . delete ( bookId ) ;
tx . oncomplete = resolve ;
tx . onerror = resolve ;
} ) ;
} catch { }
}
2026-04-01 16:10:03 +02:00
async function _evictBookCache ( bookList ) {
try {
const db = await _openBookCacheDb ( ) ;
const now = Date . now ( ) ;
const metaById = Object . fromEntries ( bookList . map ( b => [ b . id , b ] ) ) ;
await new Promise ( ( resolve ) => {
const tx = db . transaction ( _BOOK _CACHE _STORE , 'readwrite' ) ;
const store = tx . objectStore ( _BOOK _CACHE _STORE ) ;
store . openCursor ( ) . onsuccess = e => {
const cursor = e . target . result ;
if ( ! cursor ) { resolve ( ) ; return ; }
const meta = metaById [ cursor . value . id ] ;
const lastRead = meta ? . last _read ? new Date ( meta . last _read ) . getTime ( ) : 0 ;
if ( ! meta || ( now - lastRead ) > _BOOK _CACHE _EVICT _MS ) cursor . delete ( ) ;
cursor . continue ( ) ;
} ;
tx . onerror = resolve ;
} ) ;
} catch { }
}
2026-05-29 21:14:40 +02:00
async function _saveBookMeta ( books ) {
try {
const db = await _openBookCacheDb ( ) ;
const ids = new Set ( books . map ( b => String ( b . id ) ) ) ;
await new Promise ( resolve => {
const tx = db . transaction ( _BOOK _META _STORE , 'readwrite' ) ;
const store = tx . objectStore ( _BOOK _META _STORE ) ;
2026-05-31 15:41:35 +02:00
for ( const b of books ) store . put ( b ) ;
2026-05-29 21:14:40 +02:00
store . openCursor ( ) . onsuccess = e => {
const cursor = e . target . result ;
if ( ! cursor ) return ;
if ( ! ids . has ( String ( cursor . value . id ) ) ) cursor . delete ( ) ;
cursor . continue ( ) ;
} ;
tx . oncomplete = resolve ;
tx . onerror = resolve ;
} ) ;
} catch { }
}
async function _loadCachedBookMeta ( ) {
try {
const db = await _openBookCacheDb ( ) ;
return await new Promise ( resolve => {
const req = db . transaction ( _BOOK _META _STORE ) . objectStore ( _BOOK _META _STORE ) . getAll ( ) ;
req . onsuccess = e => resolve ( e . target . result || [ ] ) ;
req . onerror = ( ) => resolve ( [ ] ) ;
} ) ;
} catch { return [ ] ; }
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
async function _updateCachedBookProgress ( bookId , fraction , anchor , force = false , readAt = null ) {
const at = ( readAt == null ? Date . now ( ) : readAt ) + _serverOffset ;
2026-05-29 21:14:40 +02:00
try {
const db = await _openBookCacheDb ( ) ;
await new Promise ( resolve => {
const tx = db . transaction ( _BOOK _META _STORE , 'readwrite' ) ;
const store = tx . objectStore ( _BOOK _META _STORE ) ;
const req = store . get ( bookId ) ;
req . onsuccess = e => {
const entry = e . target . result ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// The place read last, as on the server (docs/progress-sync.md).
if ( entry && ( force || entry . read _at == null || at >= entry . read _at ) ) {
2026-05-29 21:14:40 +02:00
entry . scroll _fraction = fraction ;
entry . position _anchor = anchor ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
entry . read _at = at ;
entry . device = _progressDevice ( ) ;
entry . last _read = new Date ( at ) . toISOString ( ) ;
if ( _cmpProgress ( anchor , fraction , entry . furthest _anchor || '' , entry . furthest _fraction || 0 ) > 0 ) {
entry . furthest _anchor = anchor ;
entry . furthest _fraction = fraction ;
}
2026-05-29 21:14:40 +02:00
store . put ( entry ) ;
}
} ;
tx . oncomplete = resolve ;
tx . onerror = resolve ;
} ) ;
} catch { }
}
async function _saveAnnotationsCache ( bookId , type , ct , iv ) {
try {
const db = await _openBookCacheDb ( ) ;
await new Promise ( resolve => {
const tx = db . transaction ( _BOOK _ANNOTATIONS _STORE , 'readwrite' ) ;
tx . objectStore ( _BOOK _ANNOTATIONS _STORE ) . put ( { id : ` ${ bookId } _ ${ type } ` , ct , iv } ) ;
tx . oncomplete = resolve ;
tx . onerror = resolve ;
} ) ;
} catch { }
}
async function _loadAnnotationsCache ( bookId , type ) {
try {
const db = await _openBookCacheDb ( ) ;
return await new Promise ( resolve => {
const req = db . transaction ( _BOOK _ANNOTATIONS _STORE ) . objectStore ( _BOOK _ANNOTATIONS _STORE ) . get ( ` ${ bookId } _ ${ type } ` ) ;
req . onsuccess = e => resolve ( e . target . result || null ) ;
req . onerror = ( ) => resolve ( null ) ;
} ) ;
} catch { return null ; }
}
2026-05-31 15:41:35 +02:00
const _PENDING _SYNC _KEY = 'diora_pending_sync' ;
2026-05-30 07:22:32 +02:00
function _markPendingSync ( bookId , type ) {
try {
const pending = JSON . parse ( localStorage . getItem ( _PENDING _SYNC _KEY ) || '[]' ) ;
const key = ` ${ bookId } : ${ type } ` ;
if ( ! pending . includes ( key ) ) pending . push ( key ) ;
localStorage . setItem ( _PENDING _SYNC _KEY , JSON . stringify ( pending ) ) ;
} catch { }
}
async function _syncPendingAnnotations ( ) {
try {
const pending = JSON . parse ( localStorage . getItem ( _PENDING _SYNC _KEY ) || '[]' ) ;
if ( ! pending . length ) return ;
const remaining = [ ] ;
for ( const key of pending ) {
const sep = key . indexOf ( ':' ) ;
const bookId = key . slice ( 0 , sep ) ;
const type = key . slice ( sep + 1 ) ;
const cached = await _loadAnnotationsCache ( bookId , type ) ;
if ( ! cached ? . ct ) continue ;
const url = type === 'bookmarks'
? ` /books/ ${ bookId } /bookmarks/ `
: ` /books/ ${ bookId } /highlights/ ` ;
try {
await fetch ( url , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { ct : cached . ct , iv : cached . iv } ) ,
} ) ;
} catch { remaining . push ( key ) ; }
}
localStorage . setItem ( _PENDING _SYNC _KEY , JSON . stringify ( remaining ) ) ;
} catch { }
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Reader settings
let readerSettings = { fontSize : 16 , lineHeight : 1.8 , maxWidth : 65 , theme : 'dark' ,
2026-09-28 20:25:59 +02:00
fontFamily : 'serif' , noBold : false , fontWeight : null ,
2026-04-05 13:18:25 +02:00
pdfZoom : 100 , pdfInverted : false , pdfPaginated : false , pdfSpread : false } ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
let readerSettingsPanelOpen = false ;
let currentPdfDoc = null ;
let currentPdfBuffer = null ;
// Bookmarks
let currentBookmarks = [ ] ;
let bookmarksDirty = false ;
// Highlights
let currentHighlights = [ ] ;
let highlightsDirty = false ;
let currentHighlightPopover = null ;
// Search
let searchMatches = [ ] ;
let searchMatchIndex = - 1 ;
let searchOriginalContent = null ;
let readerSearchOpen = false ;
// PDF paginated
let pdfCurrentPage = 1 ;
let pdfTotalPages = 0 ;
let _pdfPageTextBoxCache = { } ;
2026-03-20 20:22:11 +01:00
let _pdfRenderGen = 0 ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
let _touchStartX = 0 ;
2026-08-14 23:50:23 +02:00
let _readerScrollInterval = null ;
2026-08-14 23:31:01 +02:00
let _readerScrollDir = 0 ;
let _readerScrollLastTs = 0 ;
2026-08-14 23:50:23 +02:00
const READER _SCROLL _SPEED = 1200 ; // constant px/s, no ramp-up
const READER _SCROLL _TICK _MS = 20 ; // fixed cadence, independent of the
// display's refresh rate — rAF runs at
// that rate (120/144/165Hz on plenty of
// laptops), which was driving 2-3x more
// scrollBy + scroll-listener work per
// second than intended and read as lag.
function _readerScrollTick ( ) {
const now = performance . now ( ) ;
const dt = now - _readerScrollLastTs ;
_readerScrollLastTs = now ;
$ ( 'reader-content' ) ? . scrollBy ( { top : Math . round ( _readerScrollDir * READER _SCROLL _SPEED * dt / 1000 ) } ) ;
2026-08-14 23:31:01 +02:00
}
2026-04-05 13:18:25 +02:00
let _pinchStartDist = 0 ;
let _pinchStartZoom = 100 ;
let _isPinching = false ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( typeof pdfjsLib !== 'undefined' ) {
pdfjsLib . GlobalWorkerOptions . workerSrc = '/static/js/pdf.worker.min.js' ;
}
async function loadBookList ( ) {
if ( ! IS _AUTHENTICATED ) return ;
const listEl = $ ( 'book-list' ) ;
if ( ! listEl ) return ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
listEl . innerHTML = '<p class="muted">Bücher werden geladen …</p>' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
try {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
listEl . innerHTML = '<p class="muted">Bücherliste wird vom Server geholt …</p>' ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
_flushProgressOutbox ( ) ; // own positions the server doesn't have yet
2026-03-20 09:45:39 +01:00
const res = await fetch ( '/books/' , { cache : 'no-store' } ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
_noteServerNowHeader ( res ) ;
2026-03-20 06:43:36 +01:00
if ( ! res . ok ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
listEl . innerHTML = ` <p class="muted">Serverfehler ${ res . status } beim Laden der Bücher.</p> ` ;
2026-03-20 06:43:36 +01:00
return ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const books = await res . json ( ) ;
2026-04-01 16:10:03 +02:00
_evictBookCache ( books ) ; // fire-and-forget
2026-03-20 06:43:36 +01:00
if ( ! Array . isArray ( books ) ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
listEl . innerHTML = ` <p class="muted">Unerwartete Antwort vom Server.</p> ` ;
2026-03-20 06:43:36 +01:00
return ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( ! books . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
listEl . innerHTML = '<p class="muted">Noch keine Bücher. Zieh oben eine .epub- oder .pdf-Datei hinein.</p>' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
return ;
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
listEl . innerHTML = ` <p class="muted"> ${ books . length === 1 ? '1 Buch' : books . length + ' Bücher' } auf dem Server. Entschlüssele …</p> ` ;
2026-03-20 12:28:00 +01:00
2026-03-20 09:45:39 +01:00
let key ;
try {
key = await getOrCreateEncKey ( ) ;
} catch ( e ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
listEl . innerHTML = ` <p class="muted">Verschlüsselung nicht verfügbar: ${ e . message } . Die Seite muss über HTTPS laufen.</p> ` ;
2026-03-20 09:45:39 +01:00
return ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const decrypted = [ ] ;
for ( const b of books ) {
try {
const metaBuf = await decryptBytes ( key , b . meta _iv , b . meta _ct ) ;
const meta = JSON . parse ( new TextDecoder ( ) . decode ( metaBuf ) ) ;
2026-10-07 08:50:07 +02:00
bookMetaCache [ b . id ] = { title : meta . title || '?' , author : meta . author || '' , type : meta . type || 'epub' , filename : meta . filename || '' , folder : meta . folder || '' , isbn : meta . isbn || '' , shelfTag : meta . shelfTag || '' , pages : meta . pages || 0 } ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
decrypted . push ( { id : b . id , title : meta . title || '?' , author : meta . author || '' , type : meta . type || 'epub' , folder : meta . folder || '' , isbn : meta . isbn || '' , shelfTag : meta . shelfTag || '' , pages : meta . pages || 0 , scroll _fraction : b . scroll _fraction , position _anchor : b . position _anchor || '' , read _at : b . read _at == null ? null : b . read _at , device : b . device || '' , furthest _fraction : b . furthest _fraction || 0 , furthest _anchor : b . furthest _anchor || '' , uploaded _at : b . uploaded _at , last _read : b . last _read || null , keyOk : true , is _read : ! ! b . is _read , has _highlights : ! ! b . has _highlights } ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} catch ( e ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
bookMetaCache [ b . id ] = { title : ` Buch # ${ b . id } ` , author : '' , type : 'epub' , filename : '' , folder : '' , isbn : '' , shelfTag : '' } ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
decrypted . push ( { id : b . id , title : ` Buch # ${ b . id } ` , author : '' , type : 'epub' , folder : '' , isbn : '' , shelfTag : '' , scroll _fraction : b . scroll _fraction , position _anchor : b . position _anchor || '' , read _at : b . read _at == null ? null : b . read _at , device : b . device || '' , furthest _fraction : b . furthest _fraction || 0 , furthest _anchor : b . furthest _anchor || '' , uploaded _at : b . uploaded _at , last _read : b . last _read || null , keyOk : false , is _read : ! ! b . is _read , has _highlights : ! ! b . has _highlights } ) ;
2026-05-30 07:22:32 +02:00
}
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// An own position the server doesn't have yet (sent above) shows if it's the newer one.
for ( let i = 0 ; i < decrypted . length ; i ++ ) decrypted [ i ] = _withPendingProgress ( decrypted [ i ] ) ;
2026-05-30 07:22:32 +02:00
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
decrypted . sort ( ( a , b ) => ( _bookRecency ( b ) - _bookRecency ( a ) ) || b . uploaded _at . localeCompare ( a . uploaded _at ) ) ;
2026-05-29 21:14:40 +02:00
_saveBookMeta ( decrypted ) ; // persist for offline
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
renderBookList ( decrypted ) ;
} catch ( e ) {
2026-05-29 21:14:40 +02:00
// Network error — try offline fallback
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
const cachedBooks = ( await _loadCachedBookMeta ( ) ) . map ( _withPendingProgress ) ;
2026-05-29 21:14:40 +02:00
if ( cachedBooks . length ) {
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
cachedBooks . sort ( ( a , b ) => ( _bookRecency ( b ) - _bookRecency ( a ) ) || ( b . uploaded _at || '' ) . localeCompare ( a . uploaded _at || '' ) ) ;
2026-10-07 08:50:07 +02:00
for ( const b of cachedBooks ) bookMetaCache [ b . id ] = { title : b . title , author : b . author , type : b . type || 'epub' , filename : b . filename || '' , folder : b . folder || '' , isbn : b . isbn || '' , shelfTag : b . shelfTag || '' , pages : b . pages || 0 } ;
2026-05-29 21:14:40 +02:00
const uploadArea = $ ( 'book-upload-area' ) ;
if ( uploadArea ) uploadArea . style . display = 'none' ;
renderBookList ( cachedBooks ) ;
const notice = document . createElement ( 'p' ) ;
notice . className = 'muted offline-notice' ;
notice . textContent = 'Offline — gespeicherte Bücher. Fortschritt wird beim nächsten Öffnen synchronisiert.' ;
listEl . prepend ( notice ) ;
} else {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( listEl ) listEl . innerHTML = ` <p class="muted">Fehler beim Laden der Bücher: ${ e . message } </p> ` ;
2026-05-29 21:14:40 +02:00
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
2026-05-25 17:03:59 +02:00
const _brokenBooks = new Set ( ) ;
function markBookBroken ( bookId ) {
_brokenBooks . add ( bookId ) ;
const item = document . querySelector ( ` .book-item[data-book-id=" ${ bookId } "] ` ) ;
if ( ! item ) return ;
if ( ! item . querySelector ( '.book-broken-btn' ) ) {
const btn = document . createElement ( 'button' ) ;
btn . className = 'btn btn-sm btn-danger book-broken-btn' ;
btn . title = 'Buch konnte nicht geöffnet werden — Datei erneut hochladen' ;
btn . textContent = '!' ;
btn . onclick = ( ) => repairBook ( bookId ) ;
item . querySelector ( '.book-item-actions' ) . prepend ( btn ) ;
}
}
2026-08-17 11:27:16 +02:00
function _extractIsbnFromOpf ( opfDoc ) {
const identifiers = opfDoc . querySelectorAll ( 'metadata > identifier, metadata > *|identifier' ) ;
for ( const el of identifiers ) {
const digits = ( el . textContent || '' ) . replace ( /[^0-9Xx]/g , '' ) ;
if ( digits . length === 10 || digits . length === 13 ) return digits ;
}
return '' ;
}
2026-05-25 17:03:59 +02:00
function repairBook ( bookId ) {
const input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . accept = '.epub,.pdf' ;
input . onchange = async ( ) => {
const file = input . files [ 0 ] ;
if ( ! file ) return ;
const item = document . querySelector ( ` .book-item[data-book-id=" ${ bookId } "] ` ) ;
const btn = item ? . querySelector ( '.book-broken-btn' ) ;
if ( btn ) btn . textContent = '…' ;
try {
const key = await getOrCreateEncKey ( ) ;
const buf = await file . arrayBuffer ( ) ;
2026-05-25 17:06:52 +02:00
const isPdf = /\.pdf$/i . test ( file . name ) ;
const type = isPdf ? 'pdf' : 'epub' ;
2026-08-17 11:27:16 +02:00
let title = file . name . replace ( /\.(epub|pdf)$/i , '' ) , author = '' , isbn = '' ;
2026-05-25 17:06:52 +02:00
try {
if ( isPdf ) {
const pdfDoc = await pdfjsLib . getDocument ( { data : new Uint8Array ( buf . slice ( 0 ) ) } ) . promise ;
const meta = await pdfDoc . getMetadata ( ) ;
title = meta . info ? . Title ? . trim ( ) || title ;
author = meta . info ? . Author ? . trim ( ) || '' ;
} else {
const zip = await JSZip . loadAsync ( buf . slice ( 0 ) ) ;
const containerXml = await zip . file ( 'META-INF/container.xml' ) . async ( 'text' ) ;
const opfPath = new DOMParser ( )
. parseFromString ( containerXml , 'application/xml' )
. querySelector ( 'rootfile' ) ? . getAttribute ( 'full-path' ) ;
if ( opfPath ) {
const opfDoc = new DOMParser ( ) . parseFromString (
await zip . file ( opfPath ) . async ( 'text' ) , 'application/xml' ) ;
title = opfDoc . querySelector ( 'metadata > title, metadata > *|title' ) ? . textContent ? . trim ( ) || title ;
author = opfDoc . querySelector ( 'metadata > creator, metadata > *|creator' ) ? . textContent ? . trim ( ) || '' ;
2026-08-17 11:27:16 +02:00
isbn = _extractIsbnFromOpf ( opfDoc ) ;
2026-05-25 17:06:52 +02:00
}
}
} catch ( e ) { /* keep filename as title */ }
2026-08-17 11:27:16 +02:00
// Preserve folder/shelfTag assigned before this repair — replacing the book's
// data shouldn't silently wipe organization the user already set up.
const prevMeta = bookMetaCache [ bookId ] || { } ;
const metaJson = new TextEncoder ( ) . encode ( JSON . stringify ( {
title , author , filename : file . name , type ,
isbn : isbn || prevMeta . isbn || '' ,
folder : prevMeta . folder || '' ,
shelfTag : prevMeta . shelfTag || '' ,
} ) ) ;
2026-05-25 17:06:52 +02:00
const [ metaEnc , dataEnc ] = await Promise . all ( [
encryptBytes ( key , metaJson ) ,
encryptBytes ( key , buf ) ,
] ) ;
2026-05-25 17:03:59 +02:00
const res = await fetch ( ` /books/ ${ bookId } /replace-data/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
2026-05-25 17:06:52 +02:00
body : JSON . stringify ( {
data _ct : dataEnc . ciphertext , data _iv : dataEnc . iv ,
meta _ct : metaEnc . ciphertext , meta _iv : metaEnc . iv ,
} ) ,
2026-05-25 17:03:59 +02:00
} ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! ( await res . json ( ) ) . ok ) throw new Error ( 'Serverfehler' ) ;
2026-05-25 17:03:59 +02:00
await _evictCachedBook ( bookId ) ;
_brokenBooks . delete ( bookId ) ;
if ( btn ) btn . remove ( ) ;
openBook ( bookId ) ;
} catch ( e ) {
if ( btn ) btn . textContent = '!' ;
2026-08-04 09:01:52 +02:00
await customAlert ( 'Reparatur fehlgeschlagen: ' + e . message ) ;
2026-05-25 17:03:59 +02:00
}
} ;
input . click ( ) ;
}
2026-08-04 18:10:33 +02:00
let _lastBookListData = [ ] ;
const _bookShowReadKey = 'diora_book_show_read' ;
let _bookShowRead = localStorage . getItem ( _bookShowReadKey ) === '1' ;
function _onBookShowReadToggle ( checked ) {
_bookShowRead = checked ;
localStorage . setItem ( _bookShowReadKey , checked ? '1' : '0' ) ;
renderBookList ( _lastBookListData ) ;
}
async function toggleBookRead ( bookId , currentlyRead ) {
const verb = currentlyRead ? 'als ungelesen markieren' : 'als gelesen markieren' ;
if ( ! await customConfirm ( ` Dieses Buch ${ verb } ? ` ) ) return ;
try {
const res = await fetch ( ` /books/ ${ bookId } /read/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { is _read : ! currentlyRead } ) ,
} ) ;
const data = await res . json ( ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! data . ok ) throw new Error ( 'Serverfehler' ) ;
2026-08-04 18:10:33 +02:00
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( book ) book . is _read = data . is _read ;
renderBookList ( _lastBookListData ) ;
} catch ( e ) {
await customAlert ( 'Konnte Lesestatus nicht ändern: ' + e . message ) ;
}
}
2026-08-17 10:58:53 +02:00
// Folder assigned to a book lives inside its encrypted meta blob (like title/author), so the
// server never sees plaintext folder names — no schema change needed, and it rides along for
// free in /api/sync/. One level only: folders don't nest.
let _currentBookFolder = null ; // null = root view (folder tiles + unfiled books)
function _recentlyReadBooks ( books ) {
return books
. filter ( b => b . last _read )
. sort ( ( a , b ) => b . last _read . localeCompare ( a . last _read ) )
. slice ( 0 , 7 ) ;
}
function _renderBookItemHtml ( b ) {
const pct = Math . round ( ( b . scroll _fraction || 0 ) * 100 ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const keyWarning = b . keyOk === false ? '<span title="Falscher Schlüssel – importiere den richtigen Schlüssel, um dieses Buch zu öffnen" style="color:var(--accent,#e63946);margin-left:4px;">⚠ falscher Schlüssel</span>' : '' ;
2026-08-17 10:58:53 +02:00
const broken = _brokenBooks . has ( b . id ) ;
return ` <div class="book-item" data-book-id=" ${ b . id } ">
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< div class = "book-item-info" >
2026-08-04 18:10:33 +02:00
< strong class = "book-title" > $ { escapeHtml ( b . title ) } $ { keyWarning } $ { b . is _read ? ' <span class="muted book-read-badge">✓ gelesen</span>' : '' } < / s t r o n g >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< span class = "muted book-author" > $ { escapeHtml ( b . author ) } < / s p a n >
2026-08-17 11:27:16 +02:00
< span class = "book-item-meta-row" >
2026-10-07 08:50:07 +02:00
$ { pct > 0 ? ` <span class="muted book-progress"> ${ pct } % gelesen ${ b . pages ? ` (≈ ${ Math . max ( 1 , Math . round ( b . pages * pct / 100 ) ) } von ${ b . pages } Seiten) ` : '' } </span> ` : ( b . pages ? ` <span class="muted book-progress">ca. ${ b . pages } Seiten</span> ` : '' ) }
2026-08-17 11:27:16 +02:00
$ { b . shelfTag ? ` <span class="book-shelf-badge" title="Regal-Kategorie"> ${ escapeHtml ( b . shelfTag ) } </span> ` : '' }
< / s p a n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< / d i v >
< div class = "book-item-actions" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn btn-sm" onclick = "openBook(${b.id})" $ { b . keyOk === false ? ' disabled title="Erst den richtigen Schlüssel importieren"' : '' } > Öffnen < / b u t t o n >
2026-08-17 11:27:16 +02:00
< div class = "book-item-menu" >
2026-08-17 15:37:09 +02:00
< button class = "btn btn-sm book-menu-toggle" title = "Weitere Optionen" onclick = "toggleBookMenu(this)" > ⋮ < / b u t t o n >
< div class = "book-item-menu-list" >
2026-08-17 11:27:16 +02:00
$ { broken ? ` <button class="book-menu-item book-menu-item--danger book-broken-btn" onclick="repairBook( ${ b . id } )">🔧 Reparieren (Datei erneut hochladen)</button> ` : '' }
$ { b . has _highlights ? ` <button class="book-menu-item" onclick="downloadBookAnnotationsFromList( ${ b . id } , this)">⭳ Markierungen & Notizen herunterladen</button> ` : '' }
< button class = "book-menu-item" onclick = "assignBookFolder(${b.id})" > 📁 Ordner zuweisen < / b u t t o n >
< button class = "book-menu-item" onclick = "lookupBookMetadata(${b.id})" > 🏷 ️ Metadaten abrufen ( Regal ) < / b u t t o n >
< button class = "book-menu-item" onclick = "toggleBookRead(${b.id}, ${!!b.is_read})" > $ { b . is _read ? '↺ Als ungelesen markieren' : '✓ Als gelesen markieren' } < / b u t t o n >
< button class = "book-menu-item book-menu-item--danger" onclick = "deleteBook(${b.id})" > 🗑 Löschen < / b u t t o n >
< / d i v >
< / d i v >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< / d i v >
< / d i v > ` ;
2026-08-17 10:58:53 +02:00
}
2026-08-17 15:37:09 +02:00
function toggleBookMenu ( toggleBtn ) {
// Looked up relative to the clicked button (not by id) because the same book — and
// thus the same rendered menu — can appear twice at once: once in the "Zuletzt
// gelesen" section and again further down in the folder/unfiled list.
const menu = toggleBtn . closest ( '.book-item-menu' ) ? . querySelector ( '.book-item-menu-list' ) ;
2026-08-17 11:27:16 +02:00
if ( ! menu ) return ;
const willOpen = ! menu . classList . contains ( 'open' ) ;
document . querySelectorAll ( '.book-item-menu-list.open' ) . forEach ( m => m . classList . remove ( 'open' ) ) ;
if ( willOpen ) menu . classList . add ( 'open' ) ;
}
// Close any open book menu on an outside click, or right after an item inside it is
// clicked (the toggle button manages its own open/close state above, so it's excluded).
document . addEventListener ( 'click' , ( e ) => {
if ( e . target . closest ( '.book-menu-toggle' ) ) return ;
document . querySelectorAll ( '.book-item-menu-list.open' ) . forEach ( m => m . classList . remove ( 'open' ) ) ;
} ) ;
2026-08-17 10:58:53 +02:00
function _openBookFolder ( name ) {
_currentBookFolder = name ;
renderBookList ( _lastBookListData ) ;
}
function renderBookList ( books ) {
const listEl = $ ( 'book-list' ) ;
if ( ! listEl ) return ;
_lastBookListData = books ;
if ( ! books . length ) {
listEl . innerHTML = '' ;
return ;
}
let html = '' ;
2026-08-17 15:37:09 +02:00
// Always-visible "recently read" section, independent of folder navigation and the
// read/unread filter below — it's a shortcut back into whatever was open last. Same
// vertical list-item look as the rest of the list (not a card shelf).
2026-08-17 10:58:53 +02:00
const recent = _recentlyReadBooks ( books ) ;
if ( recent . length ) {
2026-08-17 15:37:09 +02:00
html += '<div class="book-recent-section">'
2026-08-17 10:58:53 +02:00
+ '<h3 class="book-recent-title">Zuletzt gelesen</h3>'
2026-08-17 15:37:09 +02:00
+ recent . map ( _renderBookItemHtml ) . join ( '' )
+ '</div>' ;
2026-08-17 10:58:53 +02:00
}
const visible = _bookShowRead ? books : books . filter ( b => ! b . is _read ) ;
if ( ! visible . length ) {
html += books . length
? '<p class="muted">Keine ungelesenen Bücher. „Gelesene Bücher anzeigen“ aktivieren, um alle zu sehen.</p>'
: '' ;
listEl . innerHTML = html ;
return ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-08-17 10:58:53 +02:00
const grouped = new Map ( ) ;
for ( const b of visible ) {
const key = b . folder || '' ;
if ( ! grouped . has ( key ) ) grouped . set ( key , [ ] ) ;
grouped . get ( key ) . push ( b ) ;
}
// Folder emptied out (last book moved/deleted, or read-filter hid it) — fall back to root.
if ( _currentBookFolder !== null && ! grouped . has ( _currentBookFolder ) ) {
_currentBookFolder = null ;
}
if ( _currentBookFolder === null ) {
const folderNames = [ ... grouped . keys ( ) ] . filter ( k => k !== '' ) . sort ( ( a , b ) => a . localeCompare ( b , 'de' ) ) ;
if ( folderNames . length ) {
html += '<div class="book-folder-grid">'
+ folderNames . map ( name => ` <button class="book-folder-tile" data-folder=" ${ escapeHtml ( name ) } ">
< span class = "book-folder-tile-icon" > 📁 < / s p a n >
< span class = "book-folder-tile-name" > $ { escapeHtml ( name ) } < / s p a n >
< span class = "book-folder-tile-count" > $ { grouped . get ( name ) . length } < / s p a n >
< / b u t t o n > ` ) . j o i n ( ' ' )
+ '</div>' ;
}
html += ( grouped . get ( '' ) || [ ] ) . map ( _renderBookItemHtml ) . join ( '' ) ;
} else {
html += ` <div class="book-folder-header">
< button class = "btn btn-sm" onclick = "_openBookFolder(null)" > ← Alle Ordner < / b u t t o n >
< strong > $ { escapeHtml ( _currentBookFolder ) } < / s t r o n g >
< / d i v > ` ;
html += grouped . get ( _currentBookFolder ) . map ( _renderBookItemHtml ) . join ( '' ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
listEl . innerHTML = html ;
2026-08-17 10:58:53 +02:00
listEl . querySelectorAll ( '.book-folder-tile' ) . forEach ( tile => {
tile . addEventListener ( 'click' , ( ) => _openBookFolder ( tile . dataset . folder ) ) ;
} ) ;
}
2026-08-17 11:27:16 +02:00
// Shared by assignBookFolder/lookupBookMetadata: re-encrypts the meta blob with the given
// field overrides applied on top of whatever is already cached, and pushes it to the server.
async function _updateBookMeta ( bookId , overrides ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return false ;
const cached = bookMetaCache [ bookId ] || { } ;
const merged = {
title : cached . title || book . title ,
author : cached . author || book . author ,
filename : cached . filename || '' ,
type : cached . type || book . type || 'epub' ,
isbn : cached . isbn || book . isbn || '' ,
folder : cached . folder || book . folder || '' ,
shelfTag : cached . shelfTag || book . shelfTag || '' ,
2026-10-07 08:50:07 +02:00
pages : cached . pages || book . pages || 0 ,
2026-08-17 11:27:16 +02:00
... overrides ,
} ;
const key = await getOrCreateEncKey ( ) ;
const metaJson = new TextEncoder ( ) . encode ( JSON . stringify ( merged ) ) ;
const metaEnc = await encryptBytes ( key , metaJson ) ;
const res = await fetch ( ` /books/ ${ bookId } /meta/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { meta _ct : metaEnc . ciphertext , meta _iv : metaEnc . iv } ) ,
} ) ;
const data = await res . json ( ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! data . ok ) throw new Error ( 'Serverfehler' ) ;
2026-08-17 11:27:16 +02:00
Object . assign ( book , overrides ) ;
bookMetaCache [ bookId ] = merged ;
_saveBookMeta ( _lastBookListData ) ;
return true ;
}
2026-08-17 10:58:53 +02:00
async function assignBookFolder ( bookId ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return ;
const folders = [ ... new Set ( _lastBookListData . map ( b => b . folder ) . filter ( Boolean ) ) ] . sort ( ( a , b ) => a . localeCompare ( b , 'de' ) ) ;
const hint = folders . length ? ` Vorhandene Ordner: ${ folders . join ( ', ' ) } . ` : '' ;
const result = await customPrompt ( ` Ordner für „ ${ book . title } “ (leer = kein Ordner). ${ hint } ` , book . folder || '' ) ;
if ( result === null ) return ;
const folder = result . trim ( ) ;
if ( folder === ( book . folder || '' ) ) return ;
try {
2026-08-17 11:27:16 +02:00
await _updateBookMeta ( bookId , { folder } ) ;
2026-08-17 10:58:53 +02:00
if ( folder ) _currentBookFolder = folder ;
renderBookList ( _lastBookListData ) ;
} catch ( e ) {
await customAlert ( 'Konnte Ordner nicht ändern: ' + e . message ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-08-17 12:12:17 +02:00
// Fallback for books uploaded before ISBN extraction existed (or repaired/re-encrypted
// without it): pulls the already-accessible encrypted book bytes, decrypts and unzips just
// far enough to read the OPF identifiers — same source openBook() itself parses — without
// doing a full reader render. EPUB only; PDFs never carried a reliable ISBN via pdf.js.
async function _extractIsbnFromBookFile ( bookId ) {
try {
const key = await getOrCreateEncKey ( ) ;
let data _ct , data _iv ;
const cached = await _getCachedBook ( bookId ) ;
if ( cached ) {
( { data _ct , data _iv } = cached ) ;
} else {
const res = await fetch ( ` /books/ ${ bookId } /data/ ` ) ;
( { data _ct , data _iv } = await res . json ( ) ) ;
}
const plain = await decryptBytes ( key , data _iv , data _ct ) ;
const zip = await JSZip . loadAsync ( plain ) ;
const containerXml = await zip . file ( 'META-INF/container.xml' ) . async ( 'text' ) ;
const opfPath = new DOMParser ( )
. parseFromString ( containerXml , 'application/xml' )
. querySelector ( 'rootfile' ) ? . getAttribute ( 'full-path' ) ;
if ( ! opfPath ) return '' ;
const opfDoc = new DOMParser ( ) . parseFromString ( await zip . file ( opfPath ) . async ( 'text' ) , 'application/xml' ) ;
return _extractIsbnFromOpf ( opfDoc ) ;
} catch ( e ) {
return '' ;
}
}
2026-08-17 12:33:54 +02:00
async function _fetchShelfLookup ( isbn , title , author ) {
const params = new URLSearchParams ( ) ;
if ( isbn ) params . set ( 'isbn' , isbn ) ;
if ( title ) params . set ( 'title' , title ) ;
if ( author ) params . set ( 'author' , author ) ;
const res = await fetch ( ` /books/metadata-lookup/? ${ params . toString ( ) } ` ) ;
return res . json ( ) ;
}
2026-08-17 11:27:16 +02:00
// Manually triggered (never automatic) — looks up which library shelf/DDC category this
// book falls under via the server-side DNB→Open Library proxy, purely as an organizational
// hint (shown as a badge). See books/views.py:lookup_book_metadata for why this one call is
2026-08-17 12:33:54 +02:00
// allowed to briefly touch the server with a plaintext ISBN/title/author.
//
// Fallback chain when there's no ISBN (older upload, or a book that never had one — e.g.
// every PDF, since pdf.js doesn't expose one): (1) extract ISBN from the file itself, for
// EPUBs, (2) server-side title/author text search (less precise — could match the wrong
// edition/translation), (3) if that also comes up empty, ask the user to type in an ISBN
// by hand (from the book's cover/an online store) and retry once.
2026-08-17 11:27:16 +02:00
async function lookupBookMetadata ( bookId ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return ;
2026-08-17 12:12:17 +02:00
const cached = bookMetaCache [ bookId ] || { } ;
let isbn = cached . isbn || book . isbn || '' ;
const isPdf = ( cached . type || book . type ) === 'pdf' ;
2026-08-17 12:33:54 +02:00
const title = cached . title || book . title || '' ;
const author = cached . author || book . author || '' ;
2026-08-17 12:12:17 +02:00
2026-08-17 12:33:54 +02:00
if ( ! isbn && ! isPdf ) {
2026-08-17 12:12:17 +02:00
// Older upload without a stored ISBN — extract it from the book file itself (may take
// a moment for large books, since it downloads/decrypts the full file if not cached).
isbn = await _extractIsbnFromBookFile ( bookId ) ;
2026-08-17 11:27:16 +02:00
}
2026-08-17 12:12:17 +02:00
2026-08-17 11:27:16 +02:00
try {
2026-08-17 12:33:54 +02:00
let data = await _fetchShelfLookup ( isbn , title , author ) ;
if ( ! data . label ) {
const manual = await customPrompt (
'Keine Regal-Kategorie per ISBN/Titel/Autor gefunden. ISBN manuell eingeben (z. B. vom Buchrücken oder Online-Shop), oder leer lassen zum Abbrechen:' , ''
) ;
const manualIsbn = ( manual || '' ) . replace ( /[^0-9Xx]/g , '' ) ;
if ( manualIsbn ) {
isbn = manualIsbn ;
data = await _fetchShelfLookup ( isbn , title , author ) ;
}
}
// Persist whatever we ended up with either way, so a repeat lookup never needs to
// re-extract/re-search from scratch.
const overrides = { } ;
if ( isbn ) overrides . isbn = isbn ;
if ( data . label ) overrides . shelfTag = data . label ;
if ( Object . keys ( overrides ) . length ) {
await _updateBookMeta ( bookId , overrides ) ;
renderBookList ( _lastBookListData ) ;
}
2026-08-17 11:27:16 +02:00
if ( ! data . label ) {
2026-08-17 12:33:54 +02:00
await customAlert ( 'Keine Regal-Kategorie gefunden (weder DNB noch Open Library, per ISBN oder Titel/Autor).' ) ;
2026-08-17 11:27:16 +02:00
}
} catch ( e ) {
await customAlert ( 'Metadaten-Abruf fehlgeschlagen: ' + e . message ) ;
}
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function bookFileSelected ( input ) {
const file = input . files [ 0 ] ;
if ( ! file ) return ;
uploadEbook ( file ) ;
input . value = '' ;
}
function initBookDropZone ( ) {
2026-03-19 20:55:55 +01:00
// Prevent Firefox from opening dragged files when dropped outside the zone
document . addEventListener ( 'dragover' , e => e . preventDefault ( ) ) ;
document . addEventListener ( 'drop' , e => {
2026-03-19 21:29:51 +01:00
const zone = $ ( 'book-drop-zone' ) ;
2026-03-19 20:55:55 +01:00
if ( ! zone || ! zone . contains ( e . target ) ) e . preventDefault ( ) ;
} ) ;
2026-03-19 21:29:51 +01:00
const zone = $ ( 'book-drop-zone' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( ! zone ) return ;
zone . addEventListener ( 'dragover' , e => {
e . preventDefault ( ) ;
zone . classList . add ( 'drag-over' ) ;
} ) ;
zone . addEventListener ( 'dragleave' , ( ) => zone . classList . remove ( 'drag-over' ) ) ;
zone . addEventListener ( 'drop' , e => {
e . preventDefault ( ) ;
zone . classList . remove ( 'drag-over' ) ;
const file = e . dataTransfer . files [ 0 ] ;
if ( file ) uploadEbook ( file ) ;
} ) ;
}
2026-03-19 21:29:51 +01:00
async function deriveAndStoreKey ( ) {
const pwInput = document . getElementById ( 'enc-key-password' ) ;
const statusEl = $ ( 'enc-key-status' ) ;
const pw = pwInput ? pwInput . value : '' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! pw ) { if ( statusEl ) statusEl . textContent = 'Bitte gib dein Passwort ein.' ; return ; }
2026-03-19 21:29:51 +01:00
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Schlüssel wird abgeleitet …' ;
2026-03-19 21:29:51 +01:00
try {
const enc = new TextEncoder ( ) ;
const username = document . querySelector ( 'meta[name="username"]' ) ? . content || '' ;
const mat = await crypto . subtle . importKey ( 'raw' , enc . encode ( pw ) , 'PBKDF2' , false , [ 'deriveKey' ] ) ;
const key = await crypto . subtle . deriveKey (
{ name : 'PBKDF2' , salt : enc . encode ( 'diora:' + username ) , iterations : 200000 , hash : 'SHA-256' } ,
mat , { name : 'AES-GCM' , length : 256 } , true , [ 'encrypt' , 'decrypt' ]
) ;
const raw = await crypto . subtle . exportKey ( 'raw' , key ) ;
2026-03-19 22:16:22 +01:00
const storageKey = ` diora_enc_key_ ${ window . USER _ID || 0 } ` ;
2026-03-19 21:29:51 +01:00
localStorage . setItem ( storageKey , bytesToBase64 ( new Uint8Array ( raw ) ) ) ;
_encKey = null ; // reset cached key
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = '✓ Entsperrt' ;
2026-03-19 21:29:51 +01:00
const prompt = $ ( 'enc-key-prompt' ) ;
const uploadArea = $ ( 'book-upload-area' ) ;
if ( prompt ) prompt . style . display = 'none' ;
if ( uploadArea ) uploadArea . style . display = '' ;
loadBookList ( ) ;
} catch ( err ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Fehler: ' + err . message ;
2026-03-19 21:29:51 +01:00
}
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
async function uploadEbook ( file ) {
const statusEl = $ ( 'book-upload-status' ) ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
if ( file . size > DIORA _CONFIG . ebookMaxBytes ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = ` Datei zu groß (höchstens ${ DIORA _CONFIG . ebookMaxBytes / 1024 / 1024 } MB). ` ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
return false ;
}
return _importEbookBuffer ( await file . arrayBuffer ( ) , file . name , statusEl ) ;
}
// Shared tail of every import path (local file, cloud): parse metadata, encrypt
// with the user's key and POST ciphertext. Plaintext never leaves the browser.
async function _importEbookBuffer ( buf , filename , statusEl ) {
const isPdf = /\.pdf$/i . test ( filename ) ;
const isEpub = /\.epub$/i . test ( filename ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( ! isPdf && ! isEpub ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Nur .epub- und .pdf-Dateien werden unterstützt.' ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
return false ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
if ( buf . byteLength > DIORA _CONFIG . ebookMaxBytes ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = ` Datei zu groß (höchstens ${ DIORA _CONFIG . ebookMaxBytes / 1024 / 1024 } MB). ` ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
return false ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Verschlüssele …' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
try {
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
let title = filename . replace ( /\.(epub|pdf)$/i , '' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
let author = '' ;
2026-08-17 11:27:16 +02:00
let isbn = '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const type = isPdf ? 'pdf' : 'epub' ;
if ( isPdf ) {
try {
const pdfDoc = await pdfjsLib . getDocument ( { data : new Uint8Array ( buf . slice ( 0 ) ) } ) . promise ;
const meta = await pdfDoc . getMetadata ( ) ;
title = meta . info ? . Title ? . trim ( ) || title ;
author = meta . info ? . Author ? . trim ( ) || '' ;
} catch ( e ) { /* use filename as title */ }
} else {
try {
const zip = await JSZip . loadAsync ( buf . slice ( 0 ) ) ;
const containerXml = await zip . file ( 'META-INF/container.xml' ) . async ( 'text' ) ;
const containerDoc = new DOMParser ( ) . parseFromString ( containerXml , 'application/xml' ) ;
const opfPath = containerDoc . querySelector ( 'rootfile' ) ? . getAttribute ( 'full-path' ) ;
if ( opfPath ) {
const opfText = await zip . file ( opfPath ) . async ( 'text' ) ;
const opfDoc = new DOMParser ( ) . parseFromString ( opfText , 'application/xml' ) ;
title = opfDoc . querySelector ( 'metadata > title, metadata > *|title' ) ? . textContent ? . trim ( ) || title ;
author = opfDoc . querySelector ( 'metadata > creator, metadata > *|creator' ) ? . textContent ? . trim ( ) || '' ;
2026-08-17 11:27:16 +02:00
isbn = _extractIsbnFromOpf ( opfDoc ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
} catch ( e ) { /* use filename as title */ }
}
const key = await getOrCreateEncKey ( ) ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
const metaJson = new TextEncoder ( ) . encode ( JSON . stringify ( { title , author , filename , type , isbn , folder : '' , shelfTag : '' } ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const [ metaEnc , dataEnc ] = await Promise . all ( [
encryptBytes ( key , metaJson ) ,
encryptBytes ( key , buf ) ,
] ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Lade hoch …' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const res = await fetch ( '/books/upload/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( {
meta _ct : metaEnc . ciphertext ,
meta _iv : metaEnc . iv ,
data _ct : dataEnc . ciphertext ,
data _iv : dataEnc . iv ,
} ) ,
} ) ;
const data = await res . json ( ) ;
if ( data . ok ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = ` ✓ „ ${ title } “ hochgeladen ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
loadBookList ( ) ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
return true ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Fehler: ' + ( data . error ? _germanError ( data . error ) : 'Hochladen fehlgeschlagen' ) ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
return false ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} catch ( e ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Hochladen fehlgeschlagen: ' + e . message ;
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
return false ;
}
}
// ---------------------------------------------------------------------------
// Cloud import — browse a user-configured WebDAV server and pull a book in.
//
// The download is proxied by the server because the remote host is a different
// origin and sends no CORS headers. It still lands here as raw bytes and goes
// through _importEbookBuffer like any local file, so what gets stored is
// ciphertext the server cannot read.
// ---------------------------------------------------------------------------
let _cloudSources = [ ] ;
let _cloudSourceId = null ;
let _cloudPath = '' ;
async function toggleCloudImport ( ) {
const panel = $ ( 'cloud-browser' ) ;
if ( ! panel ) return ;
if ( panel . style . display !== 'none' ) { closeCloudImport ( ) ; return ; }
try {
const res = await fetch ( '/books/cloud/sources/' ) ;
_cloudSources = ( await res . json ( ) ) . sources || [ ] ;
} catch ( e ) {
await customAlert ( 'Cloud-Verbindungen konnten nicht geladen werden: ' + e . message ) ;
return ;
}
if ( ! _cloudSources . length ) {
await customAlert (
'Noch keine Cloud-Verbindung eingerichtet. Unter Einstellungen → '
+ '„Cloud-Verbindungen für Bücher“ kannst du eine hinzufügen.'
) ;
return ;
}
const select = $ ( 'cloud-source-select' ) ;
if ( select ) {
select . innerHTML = '' ;
for ( const source of _cloudSources ) {
const option = document . createElement ( 'option' ) ;
option . value = source . id ;
option . textContent = source . label ;
select . appendChild ( option ) ;
}
select . style . display = _cloudSources . length > 1 ? '' : 'none' ;
}
panel . style . display = '' ;
_cloudSourceId = _cloudSources [ 0 ] . id ;
_cloudBrowse ( '' ) ;
}
function closeCloudImport ( ) {
const panel = $ ( 'cloud-browser' ) ;
if ( panel ) panel . style . display = 'none' ;
}
function _onCloudSourceChange ( value ) {
_cloudSourceId = parseInt ( value , 10 ) ;
_cloudBrowse ( '' ) ;
}
function _cloudParentPath ( path ) {
const parts = path . split ( '/' ) . filter ( Boolean ) ;
parts . pop ( ) ;
return parts . join ( '/' ) ;
}
function _formatBytes ( n ) {
if ( ! n ) return '' ;
if ( n < 1024 ) return n + ' B' ;
if ( n < 1024 * 1024 ) return Math . round ( n / 1024 ) + ' KB' ;
return ( n / 1024 / 1024 ) . toFixed ( 1 ) + ' MB' ;
}
function _cloudEntryRow ( label , onClick , sizeText ) {
const li = document . createElement ( 'li' ) ;
li . className = 'cloud-entry' ;
const button = document . createElement ( 'button' ) ;
button . type = 'button' ;
button . className = 'cloud-entry-name' ;
button . textContent = label ;
button . addEventListener ( 'click' , onClick ) ;
li . appendChild ( button ) ;
if ( sizeText ) {
const size = document . createElement ( 'span' ) ;
size . className = 'cloud-entry-size' ;
size . textContent = sizeText ;
li . appendChild ( size ) ;
}
return li ;
}
async function _cloudBrowse ( path ) {
const listEl = $ ( 'cloud-entries' ) ;
const pathEl = $ ( 'cloud-path' ) ;
const statusEl = $ ( 'cloud-status' ) ;
if ( ! listEl ) return ;
if ( statusEl ) statusEl . textContent = 'Lade…' ;
listEl . innerHTML = '' ;
let data ;
try {
const res = await fetch ( ` /books/cloud/ ${ _cloudSourceId } /browse/?path= ${ encodeURIComponent ( path ) } ` ) ;
data = await res . json ( ) ;
if ( ! res . ok ) throw new Error ( data . error || ` HTTP ${ res . status } ` ) ;
} catch ( e ) {
if ( statusEl ) statusEl . textContent = 'Fehler: ' + e . message ;
return ;
}
_cloudPath = data . path || '' ;
if ( pathEl ) pathEl . textContent = '/' + _cloudPath ;
if ( statusEl ) statusEl . textContent = '' ;
if ( _cloudPath ) {
listEl . appendChild ( _cloudEntryRow ( '⬑ ..' , ( ) => _cloudBrowse ( _cloudParentPath ( _cloudPath ) ) ) ) ;
}
const entries = ( data . entries || [ ] ) . filter ( e => e . is _dir || e . is _book ) ;
if ( ! entries . length && statusEl ) {
statusEl . textContent = 'Keine Bücher oder Ordner in diesem Verzeichnis.' ;
}
for ( const entry of entries ) {
listEl . appendChild ( entry . is _dir
? _cloudEntryRow ( '📁 ' + entry . name , ( ) => _cloudBrowse ( entry . path ) )
: _cloudEntryRow ( '📖 ' + entry . name , ( ) => _cloudImport ( entry ) , _formatBytes ( entry . size ) ) ) ;
}
}
async function _cloudImport ( entry ) {
const statusEl = $ ( 'cloud-status' ) ;
if ( statusEl ) statusEl . textContent = ` Lade „ ${ entry . name } “… ` ;
try {
const res = await fetch ( ` /books/cloud/ ${ _cloudSourceId } /fetch/?path= ${ encodeURIComponent ( entry . path ) } ` ) ;
if ( ! res . ok ) {
const err = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
throw new Error ( err . error || ` HTTP ${ res . status } ` ) ;
}
const buf = await res . arrayBuffer ( ) ;
if ( await _importEbookBuffer ( buf , entry . name , statusEl ) && statusEl ) {
statusEl . textContent = ` ✓ „ ${ entry . name } “ importiert ` ;
}
} catch ( e ) {
if ( statusEl ) statusEl . textContent = 'Import fehlgeschlagen: ' + e . message ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
async function _parsePdfOutline ( pdf , items , depth ) {
depth = depth || 0 ;
const result = [ ] ;
for ( const item of items ) {
let href = '' ;
if ( item . dest ) {
try {
const dest = typeof item . dest === 'string' ? await pdf . getDestination ( item . dest ) : item . dest ;
if ( dest ) {
const pageIndex = await pdf . getPageIndex ( dest [ 0 ] ) ;
href = ` #pdf-page- ${ pageIndex + 1 } ` ;
}
} catch ( e ) { }
}
result . push ( { label : item . title || '(untitled)' , href , depth } ) ;
if ( item . items && item . items . length ) {
result . push ( ... await _parsePdfOutline ( pdf , item . items , depth + 1 ) ) ;
}
}
return result ;
}
2026-04-05 18:50:01 +02:00
async function renderPdf ( arrayBuffer , contentEl , scaleOverride , pivotPage ) {
2026-03-20 20:22:11 +01:00
const myGen = ++ _pdfRenderGen ;
2026-03-20 20:13:45 +01:00
const pdf = currentPdfDoc || await pdfjsLib . getDocument ( { data : new Uint8Array ( arrayBuffer . slice ( 0 ) ) } ) . promise ;
2026-03-20 20:22:11 +01:00
if ( _pdfRenderGen !== myGen ) return null ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
currentPdfDoc = pdf ;
let pdfTitle = '' , pdfAuthor = '' ;
try {
const meta = await pdf . getMetadata ( ) ;
pdfTitle = meta . info ? . Title ? . trim ( ) || '' ;
pdfAuthor = meta . info ? . Author ? . trim ( ) || '' ;
} catch ( e ) { }
let toc = [ ] ;
try {
const outline = await pdf . getOutline ( ) ;
if ( outline && outline . length ) toc = await _parsePdfOutline ( pdf , outline ) ;
} catch ( e ) { }
contentEl . innerHTML = '' ;
2026-03-20 20:22:11 +01:00
const pdfVp = document . createElement ( 'div' ) ;
pdfVp . id = 'pdf-viewport' ;
contentEl . appendChild ( pdfVp ) ;
2026-04-05 13:18:25 +02:00
const containerWidth = readerSettings . pdfSpread
? contentEl . clientWidth - 32
: Math . min ( contentEl . clientWidth - 32 , 900 ) ;
2026-04-05 18:50:01 +02:00
const pageWidth = readerSettings . pdfSpread ? ( containerWidth - 8 ) / 2 : containerWidth ;
const dpr = window . devicePixelRatio || 1 ;
2026-04-05 13:18:25 +02:00
2026-04-05 18:50:01 +02:00
// Pass 1: build DOM structure and size all canvases (no pixel rendering yet)
const pageEntries = [ ] ;
2026-04-05 13:18:25 +02:00
let spreadContainer = null ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
for ( let pageNum = 1 ; pageNum <= pdf . numPages ; pageNum ++ ) {
2026-03-20 20:22:11 +01:00
if ( _pdfRenderGen !== myGen ) { contentEl . innerHTML = '' ; return null ; }
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const page = await pdf . getPage ( pageNum ) ;
const naturalVp = page . getViewport ( { scale : 1 } ) ;
const scale = scaleOverride != null ? scaleOverride
2026-04-05 16:14:24 +02:00
: Math . max ( 0.5 , pageWidth / naturalVp . width ) * ( readerSettings . pdfZoom / 100 ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const viewport = page . getViewport ( { scale } ) ;
const wrapper = document . createElement ( 'div' ) ;
wrapper . className = 'pdf-page-wrapper' ;
wrapper . id = ` pdf-page- ${ pageNum } ` ;
const inner = document . createElement ( 'div' ) ;
inner . className = 'pdf-page-inner' ;
const canvas = document . createElement ( 'canvas' ) ;
canvas . className = 'pdf-page' ;
2026-04-01 15:48:44 +02:00
canvas . width = Math . round ( viewport . width * dpr ) ;
canvas . height = Math . round ( viewport . height * dpr ) ;
canvas . style . width = viewport . width + 'px' ;
canvas . style . height = viewport . height + 'px' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
inner . appendChild ( canvas ) ;
wrapper . appendChild ( inner ) ;
2026-04-05 13:18:25 +02:00
if ( ! readerSettings . pdfSpread ) {
pdfVp . appendChild ( wrapper ) ;
} else if ( pageNum === 1 ) {
wrapper . classList . add ( 'pdf-spread-cover' ) ;
pdfVp . appendChild ( wrapper ) ;
spreadContainer = null ;
} else {
if ( pageNum % 2 === 0 ) {
spreadContainer = document . createElement ( 'div' ) ;
spreadContainer . className = 'pdf-spread-wrapper' ;
pdfVp . appendChild ( spreadContainer ) ;
}
if ( spreadContainer ) spreadContainer . appendChild ( wrapper ) ;
else pdfVp . appendChild ( wrapper ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
2026-04-05 18:50:01 +02:00
pageEntries . push ( { page , canvas , viewport } ) ;
}
pdfTotalPages = pdf . numPages ;
// Scroll to pivot page immediately — DOM and canvas sizes are set
const pivot = Math . max ( 1 , Math . min ( pdf . numPages , pivotPage || 1 ) ) ;
const pivotEl = document . getElementById ( ` pdf-page- ${ pivot } ` ) ;
if ( pivotEl && pivot > 1 ) pivotEl . scrollIntoView ( { block : 'start' } ) ;
// Pass 2: render pixels from pivot outward (pivot→end, then pivot-1→start)
const order = [ ] ;
for ( let i = pivot - 1 ; i < pdf . numPages ; i ++ ) order . push ( i ) ;
for ( let i = pivot - 2 ; i >= 0 ; i -- ) order . push ( i ) ;
for ( const idx of order ) {
if ( _pdfRenderGen !== myGen ) { contentEl . innerHTML = '' ; return null ; }
const { page , canvas , viewport } = pageEntries [ idx ] ;
2026-04-01 15:48:44 +02:00
const ctx = canvas . getContext ( '2d' ) ;
ctx . scale ( dpr , dpr ) ;
await page . render ( { canvasContext : ctx , viewport } ) . promise ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
return { title : pdfTitle , author : pdfAuthor , toc , numPages : pdf . numPages } ;
}
2026-08-31 22:29:13 +02:00
// ---------------------------------------------------------------------------
// Read-aloud (TTS) — server-side Piper synthesis, one sentence per request.
// EPUB only (no block model for PDFs). Highlights the sentence currently
// playing and scrolls it into view; playback advances sentence-by-sentence,
// then block-by-block, until stopped or the book ends.
// ---------------------------------------------------------------------------
const TTS _MAX _CHARS = 480 ; // stay under the server's 500-char cap with margin
let ttsActive = false ;
let ttsPaused = false ;
let ttsRunToken = 0 ;
let ttsAudio = null ;
let ttsCurrentMark = null ;
let ttsBarEl = null ;
2026-09-01 07:04:27 +02:00
let ttsLang = localStorage . getItem ( 'diora_tts_lang' ) || 'de' ;
2026-10-14 18:02:33 +02:00
let _ttsBlocks = null ; // the book's blocks while reading aloud
let _ttsCur = null ; // what's being read: {bi, si, sentence, block}
2026-09-01 07:04:27 +02:00
function setTtsLang ( lang ) {
ttsLang = lang ;
localStorage . setItem ( 'diora_tts_lang' , lang ) ;
}
2026-08-31 22:29:13 +02:00
function _ttsSplitSentences ( text ) {
const raw = text . split ( /(?<=[.!?])\s+/ ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
const pieces = raw . length ? raw : [ text ] ;
const out = [ ] ;
for ( const piece of pieces ) {
let rest = piece ;
while ( rest . length > TTS _MAX _CHARS ) {
let cut = rest . lastIndexOf ( ' ' , TTS _MAX _CHARS ) ;
if ( cut <= 0 ) cut = TTS _MAX _CHARS ;
out . push ( rest . slice ( 0 , cut ) . trim ( ) ) ;
rest = rest . slice ( cut ) . trim ( ) ;
}
if ( rest ) out . push ( rest ) ;
}
return out ;
}
async function _ttsFetchAudio ( sentence ) {
const resp = await fetch ( '/tts/synthesize/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-09-01 07:04:27 +02:00
body : JSON . stringify ( { text : sentence , lang : ttsLang } ) ,
2026-08-31 22:29:13 +02:00
} ) ;
if ( ! resp . ok ) throw new Error ( 'tts request failed' ) ;
return await resp . blob ( ) ;
}
function _ttsClearHighlight ( ) {
if ( ttsCurrentMark && ttsCurrentMark . parentNode ) {
const parent = ttsCurrentMark . parentNode ;
while ( ttsCurrentMark . firstChild ) parent . insertBefore ( ttsCurrentMark . firstChild , ttsCurrentMark ) ;
parent . removeChild ( ttsCurrentMark ) ;
parent . normalize ( ) ;
}
ttsCurrentMark = null ;
}
// Finds `sentence` as a substring of block.textContent starting at fromOffset
// (so repeated sentence text earlier in the block isn't matched again), turns
// it into a DOM Range via the same char-offset addressing highlights use, and
// wraps it in a <mark>. Returns the offset to resume searching from.
function _ttsHighlightSentence ( block , sentence , fromOffset ) {
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Listening is reading: where the voice is counts as the reading position.
_noteReaderInteraction ( ) ;
2026-08-31 22:29:13 +02:00
_ttsClearHighlight ( ) ;
const full = block . textContent ;
const idx = full . indexOf ( sentence , fromOffset ) ;
if ( idx === - 1 ) return fromOffset ;
const start = _nodeAtCharOffset ( block , idx ) ;
const end = _nodeAtCharOffset ( block , idx + sentence . length ) ;
if ( ! start || ! end ) return idx + sentence . length ;
try {
const range = document . createRange ( ) ;
range . setStart ( start . node , start . offset ) ;
range . setEnd ( end . node , end . offset ) ;
const mark = document . createElement ( 'mark' ) ;
mark . className = 'tts-current' ;
range . surroundContents ( mark ) ;
ttsCurrentMark = mark ;
_ttsScrollIntoView ( mark ) ;
} catch ( e ) { }
return idx + sentence . length ;
}
function _ttsScrollIntoView ( el ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
const top = el . getBoundingClientRect ( ) . top - contentEl . getBoundingClientRect ( ) . top ;
if ( top < 40 || top > contentEl . clientHeight - 80 ) {
_suppressScrollJumpDetect ( ) ;
contentEl . scrollBy ( { top : top - contentEl . clientHeight * 0.3 , behavior : 'smooth' } ) ;
}
}
function _ttsPlayBlob ( blob ) {
return new Promise ( ( resolve ) => {
const url = URL . createObjectURL ( blob ) ;
const audio = new Audio ( url ) ;
ttsAudio = audio ;
const done = ( ) => { URL . revokeObjectURL ( url ) ; resolve ( ) ; } ;
audio . addEventListener ( 'ended' , done ) ;
audio . addEventListener ( 'error' , done ) ;
if ( ! ttsPaused ) audio . play ( ) . catch ( done ) ;
} ) ;
}
2026-10-14 18:02:33 +02:00
// Walks blocks/sentences from startBlockIndex (and its sentence startSentence)
// onward, fetching one sentence ahead while the current one plays so there's
// no gap between them.
async function _ttsPlayLoop ( blocks , startBlockIndex , runToken , startSentence = 0 ) {
2026-08-31 22:29:13 +02:00
function * sentenceStream ( ) {
for ( let bi = startBlockIndex ; bi < blocks . length ; bi ++ ) {
const block = blocks [ bi ] ;
const text = ( block . textContent || '' ) . trim ( ) ;
if ( ! text ) continue ;
2026-10-14 18:02:33 +02:00
const sentences = _ttsSplitSentences ( text ) ;
for ( let si = bi === startBlockIndex ? startSentence : 0 ; si < sentences . length ; si ++ ) {
yield { block , sentence : sentences [ si ] , bi , si } ;
}
2026-08-31 22:29:13 +02:00
}
}
const iter = sentenceStream ( ) ;
let cur = iter . next ( ) ;
if ( cur . done ) { stopReadAloud ( ) ; return ; }
let curFetch = _ttsFetchAudio ( cur . value . sentence ) ;
let lastBlock = null ;
let blockSearchOffset = 0 ;
while ( ! cur . done ) {
if ( runToken !== ttsRunToken ) return ;
const { block , sentence } = cur . value ;
const next = iter . next ( ) ;
const nextFetch = next . done ? null : _ttsFetchAudio ( next . value . sentence ) . catch ( ( ) => null ) ;
let audioBlob ;
try {
audioBlob = await curFetch ;
} catch ( e ) {
stopReadAloud ( ) ;
return ;
}
if ( runToken !== ttsRunToken ) return ;
2026-10-14 18:02:33 +02:00
if ( block !== lastBlock ) {
lastBlock = block ;
// Started in the middle of a block (previous/next sentence): find it from there.
blockSearchOffset = _ttsOffsetOf ( block , cur . value . si ) ;
}
_ttsCur = { bi : cur . value . bi , si : cur . value . si , sentence , block } ;
2026-08-31 22:29:13 +02:00
blockSearchOffset = _ttsHighlightSentence ( block , sentence , blockSearchOffset ) ;
2026-10-14 18:02:33 +02:00
_ttsNotify ( ) ;
2026-08-31 22:29:13 +02:00
await _ttsPlayBlob ( audioBlob ) ;
if ( runToken !== ttsRunToken ) return ;
cur = next ;
curFetch = nextFetch ;
}
if ( runToken === ttsRunToken ) stopReadAloud ( ) ;
}
function _ttsTogglePause ( ) {
ttsPaused = ! ttsPaused ;
if ( ttsAudio ) {
if ( ttsPaused ) ttsAudio . pause ( ) ;
else ttsAudio . play ( ) . catch ( ( ) => { } ) ;
}
_ttsUpdateBar ( ) ;
2026-10-14 18:02:33 +02:00
_ttsNotify ( ) ;
}
// ---- read-aloud for a player bar (the shared dock in native-podcast.js) ----------
// window.__dioraTts: state(), toggle(), stop(), next(), prev(); a 'diora-tts'
// event on window after every change. A dock that shows it sets
// window.__dioraTtsDock = true, and the reader's own little bar stays away.
function _ttsState ( ) {
const meta = ( currentBookId && bookMetaCache [ currentBookId ] ) || { } ;
const cur = _ttsCur ;
const chapterEl = cur && cur . block . closest ( '[data-epub-src]' ) ;
const blocks = _ttsBlocks || [ ] ;
return {
active : ttsActive ,
paused : ttsPaused ,
bookId : currentBookId ,
title : meta . title || '' ,
author : meta . author || '' ,
chapter : chapterEl ? _tocTitleForChapterSrc ( chapterEl . dataset . epubSrc ) : '' ,
sentence : cur ? cur . sentence : '' ,
progress : cur && blocks . length ? cur . bi / blocks . length : 0 ,
} ;
}
function _ttsNotify ( ) {
try { window . dispatchEvent ( new CustomEvent ( 'diora-tts' , { detail : _ttsState ( ) } ) ) ; } catch ( e ) { }
2026-08-31 22:29:13 +02:00
}
2026-10-14 18:02:33 +02:00
// Read from another sentence on: the one before/after the current one.
function _ttsRestartAt ( bi , si ) {
const blocks = _ttsBlocks ;
if ( ! ttsActive || ! blocks ) return ;
ttsRunToken ++ ;
const runToken = ttsRunToken ;
if ( ttsAudio ) {
try { ttsAudio . pause ( ) ; ttsAudio . src = '' ; } catch ( e ) { }
ttsAudio = null ;
}
_ttsPlayLoop ( blocks , bi , runToken , si ) ;
}
// Where sentence si of a block starts in its text (for the highlight).
function _ttsOffsetOf ( block , si ) {
const full = block . textContent || '' ;
let off = 0 ;
_ttsSplitSentences ( full . trim ( ) ) . slice ( 0 , si ) . forEach ( s => {
const k = full . indexOf ( s , off ) ;
if ( k >= 0 ) off = k + s . length ;
} ) ;
return off ;
}
function _ttsSentenceCount ( block ) {
const text = ( block . textContent || '' ) . trim ( ) ;
return text ? _ttsSplitSentences ( text ) . length : 0 ;
}
function _ttsStep ( dir ) {
const cur = _ttsCur ;
const blocks = _ttsBlocks ;
if ( ! cur || ! blocks ) return ;
if ( dir > 0 ) {
if ( cur . si + 1 < _ttsSentenceCount ( cur . block ) ) return _ttsRestartAt ( cur . bi , cur . si + 1 ) ;
for ( let bi = cur . bi + 1 ; bi < blocks . length ; bi ++ ) if ( _ttsSentenceCount ( blocks [ bi ] ) ) return _ttsRestartAt ( bi , 0 ) ;
stopReadAloud ( ) ;
} else {
if ( cur . si > 0 ) return _ttsRestartAt ( cur . bi , cur . si - 1 ) ;
for ( let bi = cur . bi - 1 ; bi >= 0 ; bi -- ) {
const n = _ttsSentenceCount ( blocks [ bi ] ) ;
if ( n ) return _ttsRestartAt ( bi , n - 1 ) ;
}
_ttsRestartAt ( cur . bi , 0 ) ;
}
}
window . _ _dioraTts = {
state : _ttsState ,
toggle : ( ) => ( ttsActive ? _ttsTogglePause ( ) : startReadAloud ( ) ) ,
stop : ( ) => stopReadAloud ( ) ,
next : ( ) => _ttsStep ( 1 ) ,
prev : ( ) => _ttsStep ( - 1 ) ,
} ;
2026-08-31 22:29:13 +02:00
function _ttsUpdateBar ( ) {
if ( ! ttsBarEl ) return ;
const btn = ttsBarEl . querySelector ( '.tts-pause-btn' ) ;
if ( btn ) btn . textContent = ttsPaused ? '▶' : '⏸' ;
}
function _ttsShowBar ( ) {
_ttsRemoveBar ( ) ;
2026-10-14 18:02:33 +02:00
if ( window . _ _dioraTtsDock ) return ; // the player bar shows it
2026-08-31 22:29:13 +02:00
const bar = document . createElement ( 'div' ) ;
bar . className = 'tts-bar' ;
bar . innerHTML = `
< button type = "button" class = "tts-pause-btn" title = "Pause/Weiter" > ⏸ < / b u t t o n >
< button type = "button" class = "tts-stop-btn" title = "Vorlesen beenden" > ■ < / b u t t o n >
` ;
document . body . appendChild ( bar ) ;
ttsBarEl = bar ;
bar . querySelector ( '.tts-pause-btn' ) . addEventListener ( 'click' , _ttsTogglePause ) ;
bar . querySelector ( '.tts-stop-btn' ) . addEventListener ( 'click' , stopReadAloud ) ;
}
function _ttsRemoveBar ( ) {
if ( ttsBarEl ) { ttsBarEl . remove ( ) ; ttsBarEl = null ; }
}
function toggleReadAloud ( ) {
if ( ttsActive ) stopReadAloud ( ) ;
else startReadAloud ( ) ;
}
function startReadAloud ( ) {
if ( ttsActive ) return ;
if ( currentPdfDoc ) {
const toast = document . createElement ( 'div' ) ;
toast . className = 'reader-toast' ;
toast . textContent = 'Vorlesen ist aktuell nur für EPUB-Bücher verfügbar.' ;
document . body . appendChild ( toast ) ;
setTimeout ( ( ) => toast . remove ( ) , 2200 ) ;
return ;
}
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
const blocks = Array . from ( contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ) ;
if ( ! blocks . length ) return ;
const [ anchorBlock ] = _anchorParts ( getPositionAnchor ( contentEl ) ) ;
const startIndex = ( anchorBlock >= 0 && anchorBlock < blocks . length ) ? anchorBlock : 0 ;
ttsActive = true ;
ttsPaused = false ;
ttsRunToken ++ ;
const runToken = ttsRunToken ;
2026-10-14 18:02:33 +02:00
_ttsBlocks = blocks ;
_ttsCur = null ;
2026-08-31 22:29:13 +02:00
const btn = $ ( 'reader-tts-btn' ) ;
if ( btn ) { btn . classList . add ( 'active' ) ; btn . title = 'Vorlesen beenden' ; }
_ttsShowBar ( ) ;
2026-10-14 18:02:33 +02:00
_ttsNotify ( ) ;
2026-08-31 22:29:13 +02:00
_ttsPlayLoop ( blocks , startIndex , runToken ) ;
}
function stopReadAloud ( ) {
ttsRunToken ++ ;
ttsActive = false ;
ttsPaused = false ;
if ( ttsAudio ) {
try { ttsAudio . pause ( ) ; ttsAudio . src = '' ; } catch ( e ) { }
ttsAudio = null ;
}
_ttsClearHighlight ( ) ;
_ttsRemoveBar ( ) ;
2026-10-14 18:02:33 +02:00
_ttsBlocks = null ;
_ttsCur = null ;
2026-08-31 22:29:13 +02:00
const btn = $ ( 'reader-tts-btn' ) ;
if ( btn ) { btn . classList . remove ( 'active' ) ; btn . title = 'Vorlesen' ; }
2026-10-14 18:02:33 +02:00
_ttsNotify ( ) ;
2026-08-31 22:29:13 +02:00
}
2026-04-01 15:24:00 +02:00
// ---------------------------------------------------------------------------
2026-04-01 15:50:31 +02:00
// Immersive reader mode — tap centre of screen to toggle bars
2026-04-01 15:24:00 +02:00
// ---------------------------------------------------------------------------
2026-04-01 15:50:31 +02:00
let _immBarsVisible = true ;
function _immHandleTap ( e ) {
2026-05-29 21:04:47 +02:00
// Ignore taps on interactive elements (buttons, links, inputs, settings panel, footnote popover)
2026-08-31 22:29:13 +02:00
if ( e . target . closest ( 'button, a, input, select, label, #reader-settings-panel, .reader-header, .footnote-popover, #reader-margin, #highlight-popover, .note-bottom-sheet, .tts-bar' ) ) return ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
// In marker mode, taps have a dedicated meaning (highlight/create a note) —
// don't also toggle the immersive bars underneath.
if ( markerModeActive ) return ;
2026-08-04 10:17:01 +02:00
// Don't toggle bars while the user has just finished selecting text (e.g. to highlight it)
const sel = window . getSelection ( ) ;
if ( sel && ! sel . isCollapsed ) return ;
2026-05-29 21:04:47 +02:00
// If a footnote popover is open, dismiss it instead of toggling bars
if ( currentFootnotePopover && ! currentFootnotePopover . contains ( e . target ) ) {
dismissFootnotePopover ( ) ;
return ;
}
2026-04-01 15:50:31 +02:00
_immBarsVisible = ! _immBarsVisible ;
document . body . classList . toggle ( 'reader-immersive' , ! _immBarsVisible ) ;
2026-04-05 14:13:28 +02:00
// Close settings panel when bars disappear
if ( ! _immBarsVisible && readerSettingsPanelOpen ) {
const sp = document . getElementById ( 'reader-settings-panel' ) ;
if ( sp ) sp . remove ( ) ;
readerSettingsPanelOpen = false ;
}
2026-04-01 15:24:00 +02:00
}
function enterReaderImmersiveMode ( ) {
2026-04-01 15:50:31 +02:00
_immBarsVisible = true ;
document . body . classList . remove ( 'reader-immersive' ) ;
document . addEventListener ( 'click' , _immHandleTap ) ;
2026-04-01 15:24:00 +02:00
}
function exitReaderImmersiveMode ( ) {
2026-04-01 15:50:31 +02:00
_immBarsVisible = true ;
document . body . classList . remove ( 'reader-immersive' ) ;
document . removeEventListener ( 'click' , _immHandleTap ) ;
2026-04-01 15:24:00 +02:00
}
2026-03-21 18:04:11 +01:00
2026-09-28 21:19:05 +02:00
// Serializes openBook: it empties #reader-content, then appends the chapters
// across several awaits. A second call in between (a second tap while a slow
// e-reader is still decrypting) appended the whole book a second time — and
// progress saved from that doubled DOM had block indices beyond the real book,
// which then out-ranked every real position on the server.
let _openBookInFlight = null ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
async function openBook ( bookId ) {
2026-09-28 21:19:05 +02:00
if ( _openBookInFlight ) {
if ( _openBookInFlight . bookId === bookId ) return _openBookInFlight . promise ;
await _openBookInFlight . promise . catch ( ( ) => { } ) ;
}
const promise = _openBookOnce ( bookId ) ;
_openBookInFlight = { bookId , promise } ;
try {
return await promise ;
} finally {
if ( _openBookInFlight && _openBookInFlight . promise === promise ) _openBookInFlight = null ;
}
}
async function _openBookOnce ( bookId ) {
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Nothing is saved for this book until its place is restored.
_readPos = { bookId , ready : false , key : null , readAt : 0 , moved : false , hold : false } ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const overlay = $ ( 'reader-overlay' ) ;
const contentEl = $ ( 'reader-content' ) ;
const titleEl = $ ( 'reader-title' ) ;
if ( ! overlay || ! contentEl ) return ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
titleEl . textContent = 'Lade …' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
contentEl . innerHTML = '' ;
overlay . style . display = '' ;
try {
2026-04-05 13:53:38 +02:00
loadReaderSettings ( bookId ) ;
2026-09-27 19:57:10 +02:00
_charIndex = null ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const key = await getOrCreateEncKey ( ) ;
2026-04-01 16:10:03 +02:00
let data _ct , data _iv ;
const cached = await _getCachedBook ( bookId ) ;
if ( cached ) {
( { data _ct , data _iv } = cached ) ;
} else {
2026-05-30 00:12:22 +02:00
let res ;
try {
res = await fetch ( ` /books/ ${ bookId } /data/ ` ) ;
} catch {
// offline and book not cached yet
overlay . style . display = 'none' ;
const toast = document . createElement ( 'div' ) ;
toast . className = 'reader-toast' ;
toast . textContent = 'Buch nicht offline verfügbar — einmal online öffnen zum Cachen.' ;
document . body . appendChild ( toast ) ;
setTimeout ( ( ) => toast . remove ( ) , 3500 ) ;
return ;
}
2026-04-01 16:10:03 +02:00
( { data _ct , data _iv } = await res . json ( ) ) ;
_setCachedBook ( bookId , data _ct , data _iv ) ; // fire-and-forget
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const plain = await decryptBytes ( key , data _iv , data _ct ) ;
// Revoke any previous image blob URLs
for ( const url of Object . values ( currentImageMap ) ) URL . revokeObjectURL ( url ) ;
currentImageMap = { } ;
const cachedMeta = bookMetaCache [ bookId ] || { } ;
let title = cachedMeta . title || '' ;
let author = cachedMeta . author || '' ;
let toc = [ ] ;
let numPages = 0 ;
const isPdfBook = cachedMeta . type === 'pdf' ;
if ( isPdfBook ) {
currentPdfDoc = null ; // reset so renderPdf creates fresh doc
2026-04-05 18:29:46 +02:00
const loadingEl = document . createElement ( 'div' ) ;
loadingEl . className = 'pdf-loading-overlay' ;
loadingEl . innerHTML = '<span class="pdf-loading-spinner"></span>' ;
overlay . appendChild ( loadingEl ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const result = await renderPdf ( plain , contentEl ) ;
2026-04-05 18:29:46 +02:00
loadingEl . remove ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
title = result . title || title ;
author = result . author || author ;
toc = result . toc ;
numPages = result . numPages ;
currentPdfBuffer = plain ;
} else {
currentPdfBuffer = null ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
// Prepared before (same book file, same preparation code): only the images to unpack.
const prepared = await _getParsedBook ( bookId , data _iv , key ) ;
const result = prepared ? await _restoreParsedBook ( prepared , plain ) : await parseEpub ( plain ) ;
if ( ! prepared ) {
// Kept for the next open once the book is on screen (encrypting takes a moment).
setTimeout ( ( ) => _setParsedBook ( bookId , data _iv , key , result ) , 3000 ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
title = result . title || title ;
author = result . author || author ;
toc = result . toc ;
currentImageMap = result . imageMap ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
// Inject chapters progressively so the browser stays responsive on large books:
// a batch per ~50 ms of work (a fixed count waited a frame every 8 chapters).
const tmp = document . createElement ( 'div' ) ;
for ( let i = 0 ; i < result . chapters . length ; ) {
2026-04-29 11:40:58 +02:00
const frag = document . createDocumentFragment ( ) ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
const start = performance . now ( ) ;
do {
tmp . innerHTML = result . chapters [ i ++ ] ;
2026-04-29 11:40:58 +02:00
while ( tmp . firstChild ) frag . appendChild ( tmp . firstChild ) ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
} while ( i < result . chapters . length && performance . now ( ) - start < 50 ) ;
2026-04-29 11:40:58 +02:00
contentEl . appendChild ( frag ) ;
Bücher schneller öffnen: Kapitel parallel entpacken, vorbereitete Bücher merken
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:41:56 +02:00
if ( i < result . chapters . length ) await new Promise ( r => setTimeout ( r , 0 ) ) ;
2026-04-29 11:40:58 +02:00
}
2026-08-04 13:51:05 +02:00
// Used to decide whole-percent vs one-decimal display further down —
// see _formatBookPercent().
_currentBookCharCount = contentEl . textContent . length ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
currentBookToc = toc ;
titleEl . textContent = title + ( author ? ` — ${ author } ` : '' ) ;
currentBookId = bookId ;
2026-10-07 08:50:07 +02:00
// Remember the book's real (PDF) or estimated (EPUB) page count in its encrypted meta so
// the book list can show it; rewritten only when it changed.
const bookPages = isPdfBook ? numPages : Math . max ( 1 , Math . round ( _currentBookCharCount / CHARS _PER _BOOK _PAGE ) ) ;
if ( bookPages > 0 && cachedMeta . pages !== bookPages ) {
_updateBookMeta ( bookId , { pages : bookPages } ) . catch ( ( ) => { } ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Load bookmarks and highlights
await Promise . all ( [
loadBookmarks ( bookId ) ,
loadHighlights ( bookId ) ,
] ) ;
// Apply reader settings (theme, font size, etc.)
applyReaderSettings ( isPdfBook ) ;
2026-08-04 18:10:33 +02:00
// If this book already has notes/highlights, surface them right away
// instead of making the reader hide them behind a manual toggle.
2026-09-24 10:49:09 +00:00
// Not on phones: there the margin covers the text; it stays one tap away (✎).
if ( ! isPdfBook && currentHighlights . length && ! readerAnnotationsMarginOpen && ! _isMobileReader ( ) ) {
2026-08-04 18:10:33 +02:00
toggleAnnotationsMargin ( ) ;
}
2026-04-05 13:18:25 +02:00
// Touch: swipe (paginated) + pinch-to-zoom
contentEl . addEventListener ( 'touchstart' , _pdfTouchStart , { passive : true } ) ;
contentEl . addEventListener ( 'touchmove' , _pdfTouchMove , { passive : false } ) ;
contentEl . addEventListener ( 'touchend' , _pdfTouchEnd , { passive : true } ) ;
contentEl . addEventListener ( 'touchcancel' , _pdfTouchEnd , { passive : true } ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Set up progress input
const progressInput = $ ( 'reader-progress-input' ) ;
const progressSuffix = $ ( 'reader-progress-suffix' ) ;
const isPdf = isPdfBook ;
if ( progressInput ) {
progressInput . style . display = '' ;
if ( isPdf ) {
progressInput . value = 1 ;
if ( progressSuffix ) progressSuffix . textContent = ` / ${ numPages } ` ;
} else {
2026-08-04 13:51:05 +02:00
progressInput . value = _formatBookPercent ( 0 ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( progressSuffix ) progressSuffix . textContent = '%' ;
}
progressInput . addEventListener ( 'change' , function ( ) {
2026-08-04 09:01:52 +02:00
_suppressScrollJumpDetect ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( isPdf ) {
const page = Math . min ( numPages , Math . max ( 1 , parseInt ( this . value , 10 ) || 1 ) ) ;
this . value = page ;
const target = contentEl . querySelector ( ` #pdf-page- ${ page } ` ) ;
if ( target ) {
const top = target . getBoundingClientRect ( ) . top - contentEl . getBoundingClientRect ( ) . top ;
contentEl . scrollBy ( { top : top - 8 , behavior : 'smooth' } ) ;
}
} else {
2026-08-04 13:51:05 +02:00
const pct = Math . min ( 100 , Math . max ( 0 , _parseLocalePercent ( this . value ) ) ) ;
this . value = _formatBookPercent ( pct / 100 ) ;
2026-09-27 19:57:10 +02:00
restoreFromAnchor ( contentEl , _anchorForFraction ( contentEl , pct / 100 ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
} ) ;
progressInput . addEventListener ( 'click' , function ( ) { this . select ( ) ; } ) ;
}
2026-04-01 23:46:57 +02:00
// Restore scroll position — must happen BEFORE auto-save timer is started
2026-08-04 09:01:52 +02:00
_suppressScrollJumpDetect ( ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
let openedProgress = null ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
try {
2026-05-29 21:14:40 +02:00
let fraction = 0 , anchor = '' ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
let bookData = null ;
2026-05-29 21:14:40 +02:00
try {
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
const progressRes = await fetch ( '/books/' , { cache : 'no-store' } ) ;
_noteServerNowHeader ( progressRes ) ;
2026-05-29 21:14:40 +02:00
const allBooks = await progressRes . json ( ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
bookData = allBooks . find ( b => b . id === bookId ) || null ;
2026-05-29 21:14:40 +02:00
} catch {
// offline — use cached meta
const cached = await _loadCachedBookMeta ( ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
bookData = cached . find ( b => b . id === bookId ) || null ;
2026-05-29 21:14:40 +02:00
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// The newer of the server's place and an own one it doesn't have yet.
if ( bookData ) bookData = _withPendingProgress ( bookData ) ;
fraction = bookData ? ( bookData . scroll _fraction || 0 ) : 0 ;
anchor = bookData ? ( bookData . position _anchor || '' ) : '' ;
openedProgress = bookData ;
2026-04-01 15:41:30 +02:00
_currentPositionAnchor = anchor ;
2026-04-01 16:10:03 +02:00
if ( isPdf && readerSettings . pdfPaginated ) {
if ( fraction > 0 && pdfTotalPages > 1 )
pdfCurrentPage = Math . max ( 1 , Math . round ( fraction * ( pdfTotalPages - 1 ) ) + 1 ) ;
enterPdfPaginatedMode ( ) ;
2026-04-01 23:46:57 +02:00
} else if ( isPdf ) {
// PDF scroll mode
await new Promise ( r => requestAnimationFrame ( r ) ) ;
if ( fraction > 0 )
contentEl . scrollTop = fraction * ( contentEl . scrollHeight - contentEl . clientHeight ) ;
} else {
// EPUB — wait for images so scrollHeight is final
2026-04-01 15:41:30 +02:00
const imgs = Array . from ( contentEl . querySelectorAll ( 'img' ) ) ;
if ( imgs . length ) {
await Promise . all ( imgs . map ( img =>
img . complete ? Promise . resolve ( )
: new Promise ( r => { img . onload = r ; img . onerror = r ; } )
) ) ;
}
await new Promise ( r => requestAnimationFrame ( r ) ) ;
if ( ! restoreFromAnchor ( contentEl , anchor ) && fraction > 0 ) {
2026-09-28 21:19:05 +02:00
restoreFromAnchor ( contentEl , _anchorForFraction ( contentEl , fraction ) ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
} catch ( e ) { }
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// From here on, what the reader does is reading. The place shown has the
// time it was read at (elsewhere, or here earlier): nothing new to send yet.
if ( _readPos && _readPos . bookId === bookId && currentBookId === bookId ) {
const shownAnchor = isPdf ? '' : getPositionAnchor ( contentEl ) ;
const readAtServer = openedProgress && openedProgress . read _at != null ? openedProgress . read _at : null ;
_readPos . key = _positionKey ( shownAnchor , openedProgress ? ( openedProgress . scroll _fraction || 0 ) : 0 ) ;
_readPos . readAt = readAtServer == null ? 0 : readAtServer - _serverOffset ;
_readPos . ready = true ;
_offerFurthestPosition ( openedProgress ) ;
}
2026-08-04 09:01:52 +02:00
// Prime the "settled" baseline to wherever we just restored to, rather
// than waiting to lazily pick up whatever scroll event happens to fire
// first — that could otherwise be a later, unrelated layout correction.
_scrollRestTop = contentEl . scrollTop ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Update progress input on scroll
contentEl . addEventListener ( 'scroll' , ( ) => {
if ( ! progressInput ) return ;
if ( isPdf ) {
const wrappers = contentEl . querySelectorAll ( '.pdf-page-wrapper' ) ;
const cTop = contentEl . getBoundingClientRect ( ) . top ;
let currentPage = 1 ;
for ( const w of wrappers ) {
if ( w . getBoundingClientRect ( ) . bottom > cTop + 20 ) {
currentPage = parseInt ( w . id . replace ( 'pdf-page-' , '' ) , 10 ) || 1 ;
break ;
}
}
progressInput . value = currentPage ;
}
2026-09-27 19:57:10 +02:00
// EPUB: shown from the position anchor, see the anchor tracking below.
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} ) ;
// Auto-save progress every 10s and on scroll (debounced 2s)
2026-04-01 23:46:57 +02:00
// Started AFTER restore so an early visibilitychange can't overwrite with position 0
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
readerScrollSaveTimer = setInterval ( saveReaderProgress , 10000 ) ;
let _scrollDebounce = null ;
contentEl . addEventListener ( 'scroll' , ( ) => {
clearTimeout ( _scrollDebounce ) ;
_scrollDebounce = setTimeout ( saveReaderProgress , 2000 ) ;
} , { passive : true } ) ;
2026-08-04 09:01:52 +02:00
// Keep _currentPositionAnchor fresh independent of the slower save debounce
// above: the resize observer below (or an immersive-bars toggle right after
// scrolling) would otherwise restore to a stale, pre-scroll anchor.
if ( ! isPdf ) {
let _anchorTrackDebounce = null ;
contentEl . addEventListener ( 'scroll' , ( ) => {
clearTimeout ( _anchorTrackDebounce ) ;
_anchorTrackDebounce = setTimeout ( ( ) => {
_currentPositionAnchor = getPositionAnchor ( contentEl ) ;
2026-09-27 19:57:10 +02:00
const f = _fractionFromAnchor ( contentEl , _currentPositionAnchor ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
_noteReadingPosition ( _currentPositionAnchor , f === null ? 0 : f ) ;
2026-09-27 19:57:10 +02:00
if ( progressInput && f !== null && document . activeElement !== progressInput ) {
progressInput . value = _formatBookPercent ( f ) ;
}
2026-08-04 09:01:52 +02:00
} , 150 ) ;
} , { passive : true } ) ;
}
2026-04-01 15:41:30 +02:00
// Restore anchor on viewport resize (e.g. screen rotation, font zoom)
if ( ! isPdf ) {
_resizeObserver = new ResizeObserver ( ( ) => {
if ( _currentPositionAnchor ) {
2026-08-04 09:01:52 +02:00
_suppressScrollJumpDetect ( ) ;
2026-04-01 15:41:30 +02:00
requestAnimationFrame ( ( ) => restoreFromAnchor ( contentEl , _currentPositionAnchor ) ) ;
}
2026-08-04 10:17:01 +02:00
_repositionMarginMarkers ( ) ;
2026-04-01 15:41:30 +02:00
} ) ;
_resizeObserver . observe ( contentEl ) ;
}
2026-08-04 09:01:52 +02:00
// Offer to jump back after a sudden, large scroll (accidental fling,
// fat-fingered 'g'/'G' keybind, etc.) — see _checkScrollJump above.
_scrollRestTop = null ;
let _scrollJumpRaf = null ;
contentEl . addEventListener ( 'scroll' , ( ) => {
if ( _scrollJumpRaf ) return ;
_scrollJumpRaf = requestAnimationFrame ( ( ) => {
_scrollJumpRaf = null ;
_checkScrollJump ( contentEl ) ;
2026-08-04 10:17:01 +02:00
_repositionMarginMarkers ( ) ;
2026-08-04 09:01:52 +02:00
} ) ;
} , { passive : true } ) ;
2026-04-01 15:24:00 +02:00
enterReaderImmersiveMode ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} catch ( e ) {
2026-08-04 10:17:01 +02:00
console . error ( 'openBook failed:' , e ) ;
2026-03-20 09:45:39 +01:00
overlay . style . display = 'none' ;
2026-05-25 17:03:59 +02:00
markBookBroken ( bookId ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
async function exportEncKey ( ) {
const statusEl = $ ( 'book-key-status' ) ;
try {
const key = await getOrCreateEncKey ( ) ;
const raw = await crypto . subtle . exportKey ( 'raw' , key ) ;
const b64 = bytesToBase64 ( raw ) ;
await navigator . clipboard . writeText ( b64 ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = '✓ Schlüssel in die Zwischenablage kopiert' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
setTimeout ( ( ) => { if ( statusEl ) statusEl . textContent = '' ; } , 3000 ) ;
} catch ( e ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = 'Export fehlgeschlagen: ' + e . message ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
function showImportKey ( ) {
const body = $ ( 'sidebar-body' ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
openSidebar ( 'Schlüssel importieren' , `
< p class = "muted" > Füge den Schlüssel ein , den du im anderen Browser exportiert hast : < / p >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< textarea id = "import-key-input" class = "search-input" rows = "3" style = "width:100%;resize:none;font-family:monospace;font-size:0.75rem;" > < / t e x t a r e a >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn" style = "margin-top:8px;" data - import - key - apply > Übernehmen < / b u t t o n >
< p class = "muted" style = "margin-top:8px;" > Das ersetzt den Schlüssel in diesem Browser . Hier hochgeladene Bücher sind erst wieder lesbar , wenn du den Schlüssel zurück abgleichst . < / p >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
` );
body . addEventListener ( 'click' , async function _importClick ( e ) {
if ( ! e . target . closest ( '[data-import-key-apply]' ) ) return ;
body . removeEventListener ( 'click' , _importClick ) ;
const b64 = ( body . querySelector ( '#import-key-input' ) ? . value || '' ) . trim ( ) ;
const statusEl = $ ( 'book-key-status' ) ;
try {
const raw = base64ToBytes ( b64 ) ;
const importedKey = await crypto . subtle . importKey ( 'raw' , raw , { name : 'AES-GCM' , length : 256 } , true , [ 'encrypt' , 'decrypt' ] ) ;
const re _exported = await crypto . subtle . exportKey ( 'raw' , importedKey ) ;
2026-03-19 20:59:12 +01:00
localStorage . setItem ( ` diora_enc_key_ ${ window . USER _ID } ` , bytesToBase64 ( re _exported ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
closeSidebar ( ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( statusEl ) statusEl . textContent = '✓ Schlüssel importiert – Bücher werden neu geladen …' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
await loadBookList ( ) ;
if ( statusEl ) setTimeout ( ( ) => { statusEl . textContent = '' ; } , 3000 ) ;
} catch ( e ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( $ ( 'book-key-status' ) ) $ ( 'book-key-status' ) . textContent = 'Import fehlgeschlagen: ungültiger Schlüssel' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
} ) ;
}
async function deleteBook ( bookId ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! await customConfirm ( 'Dieses Buch löschen? Das lässt sich nicht rückgängig machen.' , { danger : true } ) ) return ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
try {
const res = await fetch ( ` /books/ ${ bookId } /delete/ ` , {
method : 'POST' ,
headers : { 'X-CSRFToken' : getCsrfToken ( ) } ,
} ) ;
const data = await res . json ( ) ;
if ( data . ok ) loadBookList ( ) ;
} catch ( e ) { }
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Opened somewhere before the furthest place reached: offer it.
function _offerFurthestPosition ( p ) {
if ( ! p ) return ;
const furthest = p . furthest _fraction || 0 ;
if ( furthest - ( p . scroll _fraction || 0 ) < 0.02 || furthest >= 0.999 ) return ;
const pos = _readPos ;
_readerToast ( ` Weiteste Stelle: ${ _formatBookPercent ( furthest ) } % ` , 'Dorthin' , ( ) => {
if ( _readPos !== pos ) return ;
_noteReaderInteraction ( ) ;
_jumpToPosition ( p . furthest _anchor || '' , furthest ) ;
} , 8000 ) ;
}
2026-05-31 15:41:35 +02:00
async function saveReaderProgress ( force = false ) {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( ! currentBookId ) return ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
let fraction ;
if ( readerSettings . pdfPaginated && currentPdfDoc && pdfTotalPages > 1 ) {
fraction = ( pdfCurrentPage - 1 ) / ( pdfTotalPages - 1 ) ;
} else {
fraction = contentEl . scrollTop / ( contentEl . scrollHeight - contentEl . clientHeight || 1 ) ;
}
2026-09-28 21:19:05 +02:00
// Chapter wrappers carry unique ids; seeing one twice means the book was
// rendered twice. Positions from that DOM are wrong — don't store them.
if ( ! currentPdfDoc && contentEl . querySelectorAll ( '#epub-chapter-0' ) . length > 1 ) {
console . warn ( 'Book rendered twice, progress not saved' ) ;
return ;
}
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Not restored yet (or another book): what's shown isn't a place read.
const pos = _readPos ;
if ( ! pos || ! pos . ready || pos . bookId !== currentBookId ) return ;
// A jump that's only a look: the place before it stays.
if ( pos . hold && ! force ) return ;
2026-04-01 15:41:30 +02:00
let anchor = '' ;
if ( ! currentPdfDoc ) {
anchor = getPositionAnchor ( contentEl ) ;
_currentPositionAnchor = anchor ;
2026-09-27 19:57:10 +02:00
// EPUB: share of the text, the same on every device and layout.
const textFraction = _fractionFromAnchor ( contentEl , anchor ) ;
if ( textFraction !== null ) fraction = textFraction ;
2026-04-01 15:41:30 +02:00
}
2026-09-27 19:57:10 +02:00
fraction = Math . min ( 1.0 , Math . max ( 0.0 , fraction ) ) ;
2026-04-01 15:41:30 +02:00
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
_noteReadingPosition ( anchor , fraction ) ;
if ( force ) {
pos . readAt = Date . now ( ) ;
pos . moved = true ;
}
// Nothing read here since opening: the server has this place already.
if ( ! pos . moved ) return ;
const entry = { book : currentBookId , fraction , anchor , read _at : pos . readAt , device : _progressDevice ( ) , ... ( force && { force : true } ) } ;
const sig = ` ${ entry . book } | ${ anchor } | ${ fraction . toFixed ( 5 ) } | ${ entry . read _at } ` ;
_lastProgressBeacon = entry ;
if ( ! force && sig === pos . sentSig ) return ; // unchanged since the last save
pos . sentSig = sig ;
_progressOutboxPut ( entry ) ;
_updateCachedBookProgress ( currentBookId , fraction , anchor , force , pos . readAt ) ;
const sent = await _sendProgress ( entry ) ;
if ( ! sent ) pos . sentSig = null ; // try again with the next save
if ( sent && force ) {
const toast = document . createElement ( 'div' ) ;
toast . className = 'reader-toast' ;
toast . textContent = 'Position gesetzt' ;
document . body . appendChild ( toast ) ;
setTimeout ( ( ) => toast . remove ( ) , 1800 ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
function closeReader ( ) {
2026-04-01 15:24:00 +02:00
exitReaderImmersiveMode ( ) ;
2026-08-31 22:29:13 +02:00
stopReadAloud ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Save progress BEFORE hiding — scrollHeight/clientHeight return 0 once display:none
saveReaderProgress ( ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
_readPos = null ; // nothing more is saved for this book
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( bookmarksDirty ) saveBookmarks ( ) ;
if ( highlightsDirty ) saveHighlights ( ) ;
const overlay = $ ( 'reader-overlay' ) ;
if ( overlay ) overlay . style . display = 'none' ;
if ( readerScrollSaveTimer ) {
clearInterval ( readerScrollSaveTimer ) ;
readerScrollSaveTimer = null ;
}
2026-04-01 15:41:30 +02:00
if ( _resizeObserver ) {
_resizeObserver . disconnect ( ) ;
_resizeObserver = null ;
}
_currentPositionAnchor = '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
2026-08-04 10:17:01 +02:00
// Close the margin panel
readerAnnotationsMarginOpen = false ;
const marginEl = $ ( 'reader-margin' ) ;
if ( marginEl ) marginEl . classList . remove ( 'open' ) ;
const marginMarkersEl = $ ( 'reader-margin-markers' ) ;
if ( marginMarkersEl ) marginMarkersEl . innerHTML = '' ;
2026-08-04 10:49:30 +02:00
// Disarm marker mode
markerModeActive = false ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
document . querySelectorAll ( '.marker-mode-btn' ) . forEach ( b => b . classList . remove ( 'active' ) ) ;
dismissNoteBottomSheet ( ) ;
2026-08-04 10:49:30 +02:00
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Clear search before wiping content
clearReaderSearch ( ) ;
// Close settings panel if open
readerSettingsPanelOpen = false ;
const sp = document . getElementById ( 'reader-settings-panel' ) ;
if ( sp ) sp . remove ( ) ;
// Reset progress input
const progressInput = $ ( 'reader-progress-input' ) ;
if ( progressInput ) { progressInput . style . display = 'none' ; progressInput . value = 0 ; }
const progressSuffix = $ ( 'reader-progress-suffix' ) ;
if ( progressSuffix ) progressSuffix . textContent = '' ;
2026-04-05 13:18:25 +02:00
// Remove touch handlers and free image blob URLs
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const contentEl = $ ( 'reader-content' ) ;
2026-04-05 13:18:25 +02:00
if ( contentEl ) {
contentEl . removeEventListener ( 'touchstart' , _pdfTouchStart ) ;
contentEl . removeEventListener ( 'touchmove' , _pdfTouchMove ) ;
contentEl . removeEventListener ( 'touchend' , _pdfTouchEnd ) ;
contentEl . removeEventListener ( 'touchcancel' , _pdfTouchEnd ) ;
contentEl . classList . remove ( 'pinch-active' ) ;
contentEl . innerHTML = '' ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
for ( const url of Object . values ( currentImageMap ) ) URL . revokeObjectURL ( url ) ;
currentImageMap = { } ;
// Reset all state
currentBookId = null ;
currentBookToc = [ ] ;
2026-08-04 13:51:05 +02:00
_currentBookCharCount = 0 ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
currentPdfDoc = null ;
currentPdfBuffer = null ;
currentBookmarks = [ ] ;
bookmarksDirty = false ;
currentHighlights = [ ] ;
highlightsDirty = false ;
_lastProgressBeacon = null ;
_lastBookmarkBeacon = null ;
_lastHighlightBeacon = null ;
dismissHighlightPopover ( ) ;
pdfCurrentPage = 1 ;
pdfTotalPages = 0 ;
_pdfPageTextBoxCache = { } ;
// Remove PDF invert class
if ( overlay ) overlay . classList . remove ( 'pdf-inverted' ) ;
}
function openTocSidebar ( ) {
if ( ! currentBookToc . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
openSidebar ( 'Inhaltsverzeichnis' , '<p class="muted">Dieses Buch hat kein Inhaltsverzeichnis.</p>' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
return ;
}
let html = '<ul class="toc-list">' ;
for ( const entry of currentBookToc ) {
const indent = entry . depth * 14 ;
// Use data-toc-href — onclick would be stripped by sanitizeSidebarHtml
html += ` <li style="padding-left: ${ indent } px">
< button class = "btn-link toc-entry" data - toc - href = "${escapeHtml(entry.href)}" > $ { escapeHtml ( entry . label ) } < / b u t t o n >
< / l i > ` ;
}
html += '</ul>' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
openSidebar ( 'Inhaltsverzeichnis' , html ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Attach delegated listener after sidebar body is populated
const body = $ ( 'sidebar-body' ) ;
body . addEventListener ( 'click' , function _tocClick ( e ) {
const btn = e . target . closest ( '.toc-entry' ) ;
if ( btn ) {
body . removeEventListener ( 'click' , _tocClick ) ;
jumpToTocEntry ( btn . getAttribute ( 'data-toc-href' ) || '' ) ;
}
} ) ;
}
function jumpToTocEntry ( href ) {
closeSidebar ( ) ;
2026-08-04 09:01:52 +02:00
_suppressScrollJumpDetect ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
setTimeout ( ( ) => {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
// PDF page jump
if ( href . startsWith ( '#pdf-page-' ) ) {
const target = contentEl . querySelector ( href ) ;
if ( target ) {
const top = target . getBoundingClientRect ( ) . top - contentEl . getBoundingClientRect ( ) . top ;
contentEl . scrollBy ( { top : top - 16 , behavior : 'smooth' } ) ;
}
return ;
}
const hashIdx = href . indexOf ( '#' ) ;
const fragment = hashIdx >= 0 ? href . slice ( hashIdx + 1 ) : '' ;
const filePath = hashIdx >= 0 ? href . slice ( 0 , hashIdx ) : href ;
let target = null ;
if ( fragment ) {
target = contentEl . querySelector ( ` # ${ CSS . escape ( fragment ) } ` ) ;
}
if ( ! target && filePath ) {
target = Array . from ( contentEl . querySelectorAll ( '[data-epub-src]' ) )
. find ( el => el . getAttribute ( 'data-epub-src' ) === filePath ) || null ;
}
if ( target ) {
const top = target . getBoundingClientRect ( ) . top - contentEl . getBoundingClientRect ( ) . top ;
contentEl . scrollBy ( { top : top - 16 , behavior : 'smooth' } ) ;
}
} , 50 ) ;
}
// ---------------------------------------------------------------------------
// Reader Settings
// ---------------------------------------------------------------------------
2026-10-02 19:32:32 +02:00
// Typography defaults. Phones keep the long-standing ones; a wide screen
// (desktop browser, diora desktop) starts bigger and wider.
const _READER _TYPE _DEFAULTS _NARROW = { fontSize : 16 , lineHeight : 1.8 , maxWidth : 65 , fontWeight : null } ;
const _READER _TYPE _DEFAULTS _WIDE = { fontSize : 20 , lineHeight : 1.6 , maxWidth : 90 , fontWeight : 375 } ;
const _READER _WIDTH _MAX = 140 ; // ch, the width slider's upper end ("Full" stays separate)
function _readerTypeDefaults ( ) {
return window . matchMedia ( '(min-width: 900px)' ) . matches
? _READER _TYPE _DEFAULTS _WIDE : _READER _TYPE _DEFAULTS _NARROW ;
}
2026-04-05 13:53:38 +02:00
function loadReaderSettings ( bookId ) {
// Reset to defaults, then apply per-book overrides
2026-10-14 18:02:33 +02:00
Object . assign ( readerSettings , { theme : 'app' , fontFamily : 'serif' , noBold : false ,
2026-10-02 19:32:32 +02:00
pdfZoom : 100 , pdfInverted : false , pdfPaginated : false , pdfSpread : false } ,
_readerTypeDefaults ( ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
try {
2026-04-05 13:53:38 +02:00
const saved = JSON . parse ( localStorage . getItem ( ` diora_reader_settings_ ${ bookId } ` ) || '{}' ) ;
2026-10-02 19:32:32 +02:00
// Settings saved before v2 hold every value, changed or not. A value equal
// to the old default was never chosen, so it gives way to today's default.
if ( ! saved . v ) {
for ( const [ key , old ] of Object . entries ( _READER _TYPE _DEFAULTS _NARROW ) ) {
if ( saved [ key ] === old ) delete saved [ key ] ;
}
}
2026-10-14 18:02:33 +02:00
// Before v3 the reader's theme was always stored, "dark" by default: that now means "Wie die App".
if ( ( saved . v || 0 ) < 3 && saved . theme === 'dark' ) delete saved . theme ;
2026-10-02 19:32:32 +02:00
delete saved . v ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
Object . assign ( readerSettings , saved ) ;
if ( saved . pdfPaginated === undefined ) {
readerSettings . pdfPaginated = window . innerWidth < 768 ;
}
} catch ( e ) { }
}
function saveReaderSettings ( ) {
2026-04-05 13:53:38 +02:00
if ( currentBookId ) {
2026-10-14 18:02:33 +02:00
localStorage . setItem ( ` diora_reader_settings_ ${ currentBookId } ` , JSON . stringify ( { ... readerSettings , v : 3 } ) ) ;
2026-04-05 13:53:38 +02:00
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
function applyReaderSettings ( isPdf ) {
const overlay = $ ( 'reader-overlay' ) ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! overlay || ! contentEl ) return ;
if ( ! isPdf ) {
contentEl . style . fontSize = readerSettings . fontSize + 'px' ;
contentEl . style . lineHeight = readerSettings . lineHeight ;
contentEl . style . setProperty ( '--reader-max-width' , readerSettings . maxWidth + 'ch' ) ;
2026-04-29 11:40:58 +02:00
const fontMap = {
2026-09-12 15:05:08 +02:00
serif : "'Source Serif 4', Georgia, 'Times New Roman', serif" ,
2026-04-29 11:40:58 +02:00
sans : 'system-ui, -apple-system, sans-serif' ,
humanist : 'Verdana, Geneva, sans-serif' ,
mono : "'Courier New', monospace" ,
} ;
contentEl . style . fontFamily = fontMap [ readerSettings . fontFamily ] || fontMap . serif ;
2026-04-29 11:52:45 +02:00
contentEl . classList . toggle ( 'reader-no-bold' , ! ! readerSettings . noBold ) ;
2026-09-28 20:25:59 +02:00
// Weight: null keeps the stylesheet's. --reader-weight lets old WebViews
// without variable-font support (which drop weights like 425) pick a
// static cut instead.
contentEl . style . fontWeight = '' ;
if ( readerSettings . fontWeight ) {
contentEl . style . fontWeight = String ( readerSettings . fontWeight ) ;
contentEl . style . setProperty ( '--reader-weight' , String ( readerSettings . fontWeight ) ) ;
} else {
contentEl . style . removeProperty ( '--reader-weight' ) ;
}
2026-04-01 15:41:30 +02:00
if ( _currentPositionAnchor && currentBookId ) {
2026-08-04 09:01:52 +02:00
_suppressScrollJumpDetect ( ) ;
2026-04-01 15:41:30 +02:00
requestAnimationFrame ( ( ) => restoreFromAnchor ( $ ( 'reader-content' ) , _currentPositionAnchor ) ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
// Theme
overlay . classList . remove ( 'reader-theme-sepia' , 'reader-theme-bright' ) ;
2026-10-14 18:02:33 +02:00
const readerTheme = readerSettings . theme === 'app'
? ( document . documentElement . getAttribute ( 'data-theme' ) === 'light' ? 'bright' : 'dark' )
: readerSettings . theme ;
if ( readerTheme === 'sepia' ) overlay . classList . add ( 'reader-theme-sepia' ) ;
else if ( readerTheme === 'bright' ) overlay . classList . add ( 'reader-theme-bright' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// PDF invert
if ( isPdf && readerSettings . pdfInverted ) overlay . classList . add ( 'pdf-inverted' ) ;
else overlay . classList . remove ( 'pdf-inverted' ) ;
2026-08-04 10:17:01 +02:00
2026-08-04 10:49:30 +02:00
// Annotations (marker mode + margin panel) are EPUB-only for now — PDFs have no text layer yet
2026-08-04 10:17:01 +02:00
const marginBtn = $ ( 'reader-margin-btn' ) ;
if ( marginBtn ) marginBtn . style . display = isPdf ? 'none' : '' ;
if ( isPdf && readerAnnotationsMarginOpen ) toggleAnnotationsMargin ( ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
document . querySelectorAll ( '.marker-mode-btn' ) . forEach ( b => { b . style . display = isPdf ? 'none' : '' ; } ) ;
2026-08-04 10:49:30 +02:00
if ( isPdf && markerModeActive ) toggleMarkerMode ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-09-28 20:25:59 +02:00
// Font weight slider: the variable Source Serif 4 takes any weight, the
// system fonts only their fixed cuts (multiples of 100).
function _readerWeightStep ( ) {
return readerSettings . fontFamily === 'serif' ? 25 : 100 ;
}
function _readerWeight ( ) {
const step = _readerWeightStep ( ) ;
const w = readerSettings . fontWeight
|| parseInt ( getComputedStyle ( $ ( 'reader-content' ) ) . fontWeight , 10 ) || 400 ;
return Math . round ( w / step ) * step ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function toggleSettingsPanel ( ) {
const overlay = $ ( 'reader-overlay' ) ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! overlay || ! contentEl ) return ;
const existing = document . getElementById ( 'reader-settings-panel' ) ;
if ( existing ) {
existing . remove ( ) ;
readerSettingsPanelOpen = false ;
return ;
}
readerSettingsPanelOpen = true ;
const isPdf = ! ! currentPdfDoc ;
const panel = document . createElement ( 'div' ) ;
panel . id = 'reader-settings-panel' ;
panel . className = 'reader-settings-panel' ;
if ( ! isPdf ) {
panel . innerHTML = `
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< label > Schrift < button class = "btn btn-sm" id = "rs-font-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - f o n t " m i n = " 1 2 " m a x = " 2 4 " s t e p = " 1 " v a l u e = " $ { r e a d e r S e t t i n g s . f o n t S i z e } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - f o n t - p l u s " > + < / b u t t o n > < s p a n i d = " r s - f o n t - v a l " > $ { r e a d e r S e t t i n g s . f o n t S i z e } p x < / s p a n > < / l a b e l >
< label > Zeilen < button class = "btn btn-sm" id = "rs-line-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - l i n e " m i n = " 1 2 " m a x = " 3 0 " s t e p = " 1 " v a l u e = " $ { M a t h . r o u n d ( r e a d e r S e t t i n g s . l i n e H e i g h t * 1 0 ) } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - l i n e - p l u s " > + < / b u t t o n > < s p a n i d = " r s - l i n e - v a l " > $ { r e a d e r S e t t i n g s . l i n e H e i g h t } < / s p a n > < / l a b e l >
< label > Breite < button class = "btn btn-sm" id = "rs-width-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - w i d t h " m i n = " 4 0 " m a x = " $ { _ R E A D E R _ W I D T H _ M A X } " s t e p = " 5 " v a l u e = " $ { M a t h . m i n ( r e a d e r S e t t i n g s . m a x W i d t h , _ R E A D E R _ W I D T H _ M A X ) } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - w i d t h - p l u s " > + < / b u t t o n > < s p a n i d = " r s - w i d t h - v a l " > $ { r e a d e r S e t t i n g s . m a x W i d t h } c h < / s p a n > < / l a b e l >
< button class = "btn btn-sm" id = "rs-width-full" > Volle Breite < / b u t t o n >
< label > Stärke < button class = "btn btn-sm" id = "rs-weight-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - w e i g h t " m i n = " 2 0 0 " m a x = " 8 0 0 " s t e p = " $ { _ r e a d e r W e i g h t S t e p ( ) } " v a l u e = " $ { _ r e a d e r W e i g h t ( ) } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - w e i g h t - p l u s " > + < / b u t t o n > < s p a n i d = " r s - w e i g h t - v a l " > $ { _ r e a d e r W e i g h t ( ) } < / s p a n > < / l a b e l >
2026-04-29 11:40:58 +02:00
< button class = "btn btn-sm ${readerSettings.fontFamily === 'serif' ? 'active' : ''}" data - rs - font = "serif" > Serif < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.fontFamily === 'sans' ? 'active' : ''}" data - rs - font = "sans" > Sans < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.fontFamily === 'humanist' ? 'active' : ''}" data - rs - font = "humanist" > Verdana < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.fontFamily === 'mono' ? 'active' : ''}" data - rs - font = "mono" > Mono < / b u t t o n >
2026-04-29 11:52:45 +02:00
< button class = "btn btn-sm ${readerSettings.noBold ? 'active' : ''}" id = "rs-no-bold" > Kein Fett < / b u t t o n >
2026-10-14 18:02:33 +02:00
< button class = "btn btn-sm ${readerSettings.theme === 'app' ? 'active' : ''}" data - rs - theme = "app" > Wie die App < / b u t t o n >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn btn-sm ${readerSettings.theme === 'dark' ? 'active' : ''}" data - rs - theme = "dark" > Dunkel < / b u t t o n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< button class = "btn btn-sm ${readerSettings.theme === 'sepia' ? 'active' : ''}" data - rs - theme = "sepia" > Sepia < / b u t t o n >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn btn-sm ${readerSettings.theme === 'bright' ? 'active' : ''}" data - rs - theme = "bright" > Hell < / b u t t o n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
` ;
} else {
panel . innerHTML = `
2026-04-02 12:22:04 +02:00
< label > Zoom < button class = "btn btn-sm" id = "rs-zoom-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - z o o m " m i n = " 5 0 " m a x = " 2 0 0 " s t e p = " 1 0 " v a l u e = " $ { r e a d e r S e t t i n g s . p d f Z o o m } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - z o o m - p l u s " > + < / b u t t o n > < s p a n i d = " r s - z o o m - v a l " > $ { r e a d e r S e t t i n g s . p d f Z o o m } % < / s p a n > < / l a b e l >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn btn-sm ${readerSettings.pdfInverted ? 'active' : ''}" id = "rs-invert" > Invertieren < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.pdfSpread ? 'active' : ''}" id = "rs-spread" > Doppelseite < / b u t t o n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
` ;
}
2026-08-04 10:17:01 +02:00
overlay . insertBefore ( panel , $ ( 'reader-body-row' ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( ! isPdf ) {
const fontRange = panel . querySelector ( '#rs-font' ) ;
const fontVal = panel . querySelector ( '#rs-font-val' ) ;
fontRange . addEventListener ( 'input' , ( ) => {
readerSettings . fontSize = parseInt ( fontRange . value , 10 ) ;
fontVal . textContent = readerSettings . fontSize + 'px' ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
2026-08-04 09:01:52 +02:00
panel . querySelector ( '#rs-font-minus' ) . addEventListener ( 'click' , ( ) => {
fontRange . value = Math . max ( + fontRange . min , parseInt ( fontRange . value , 10 ) - 1 ) ;
fontRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
panel . querySelector ( '#rs-font-plus' ) . addEventListener ( 'click' , ( ) => {
fontRange . value = Math . min ( + fontRange . max , parseInt ( fontRange . value , 10 ) + 1 ) ;
fontRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const lineRange = panel . querySelector ( '#rs-line' ) ;
const lineVal = panel . querySelector ( '#rs-line-val' ) ;
lineRange . addEventListener ( 'input' , ( ) => {
readerSettings . lineHeight = ( parseInt ( lineRange . value , 10 ) / 10 ) . toFixed ( 1 ) ;
lineVal . textContent = readerSettings . lineHeight ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
2026-08-04 09:01:52 +02:00
panel . querySelector ( '#rs-line-minus' ) . addEventListener ( 'click' , ( ) => {
lineRange . value = Math . max ( + lineRange . min , parseInt ( lineRange . value , 10 ) - 1 ) ;
lineRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
panel . querySelector ( '#rs-line-plus' ) . addEventListener ( 'click' , ( ) => {
lineRange . value = Math . min ( + lineRange . max , parseInt ( lineRange . value , 10 ) + 1 ) ;
lineRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const widthRange = panel . querySelector ( '#rs-width' ) ;
const widthVal = panel . querySelector ( '#rs-width-val' ) ;
widthRange . addEventListener ( 'input' , ( ) => {
readerSettings . maxWidth = parseInt ( widthRange . value , 10 ) ;
widthVal . textContent = readerSettings . maxWidth + 'ch' ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
2026-08-04 09:01:52 +02:00
panel . querySelector ( '#rs-width-minus' ) . addEventListener ( 'click' , ( ) => {
widthRange . value = Math . max ( + widthRange . min , parseInt ( widthRange . value , 10 ) - 5 ) ;
widthRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
panel . querySelector ( '#rs-width-plus' ) . addEventListener ( 'click' , ( ) => {
widthRange . value = Math . min ( + widthRange . max , parseInt ( widthRange . value , 10 ) + 5 ) ;
widthRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
panel . querySelector ( '#rs-width-full' ) . addEventListener ( 'click' , ( ) => {
readerSettings . maxWidth = 999 ;
widthRange . value = 90 ;
widthVal . textContent = 'full' ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
2026-09-28 20:25:59 +02:00
const weightRange = panel . querySelector ( '#rs-weight' ) ;
const weightVal = panel . querySelector ( '#rs-weight-val' ) ;
weightRange . addEventListener ( 'input' , ( ) => {
readerSettings . fontWeight = parseInt ( weightRange . value , 10 ) ;
weightVal . textContent = readerSettings . fontWeight ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
panel . querySelector ( '#rs-weight-minus' ) . addEventListener ( 'click' , ( ) => {
weightRange . value = Math . max ( + weightRange . min , parseInt ( weightRange . value , 10 ) - + weightRange . step ) ;
weightRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
panel . querySelector ( '#rs-weight-plus' ) . addEventListener ( 'click' , ( ) => {
weightRange . value = Math . min ( + weightRange . max , parseInt ( weightRange . value , 10 ) + + weightRange . step ) ;
weightRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
2026-04-29 11:40:58 +02:00
panel . querySelectorAll ( '[data-rs-font]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( ) => {
readerSettings . fontFamily = btn . dataset . rsFont ;
panel . querySelectorAll ( '[data-rs-font]' ) . forEach ( b => b . classList . toggle ( 'active' , b === btn ) ) ;
2026-09-28 20:25:59 +02:00
// The system fonts only have cuts at multiples of 100.
weightRange . step = _readerWeightStep ( ) ;
if ( readerSettings . fontWeight ) {
readerSettings . fontWeight = Math . round ( readerSettings . fontWeight / weightRange . step ) * weightRange . step ;
}
2026-04-29 11:40:58 +02:00
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
2026-09-28 20:25:59 +02:00
weightRange . value = _readerWeight ( ) ;
weightVal . textContent = _readerWeight ( ) ;
2026-04-29 11:40:58 +02:00
} ) ;
} ) ;
2026-04-29 11:52:45 +02:00
panel . querySelector ( '#rs-no-bold' ) . addEventListener ( 'click' , function ( ) {
readerSettings . noBold = ! readerSettings . noBold ;
this . classList . toggle ( 'active' , readerSettings . noBold ) ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
panel . querySelectorAll ( '[data-rs-theme]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( ) => {
readerSettings . theme = btn . dataset . rsTheme ;
panel . querySelectorAll ( '[data-rs-theme]' ) . forEach ( b => b . classList . toggle ( 'active' , b === btn ) ) ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
} ) ;
} else {
const zoomRange = panel . querySelector ( '#rs-zoom' ) ;
2026-04-02 12:22:04 +02:00
zoomRange . addEventListener ( 'input' , ( ) => applyPdfZoom ( parseInt ( zoomRange . value , 10 ) ) ) ;
panel . querySelector ( '#rs-zoom-minus' ) . addEventListener ( 'click' , ( ) =>
applyPdfZoom ( Math . max ( 50 , readerSettings . pdfZoom - 10 ) ) ) ;
panel . querySelector ( '#rs-zoom-plus' ) . addEventListener ( 'click' , ( ) =>
applyPdfZoom ( Math . min ( 200 , readerSettings . pdfZoom + 10 ) ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
panel . querySelector ( '#rs-invert' ) . addEventListener ( 'click' , function ( ) {
readerSettings . pdfInverted = ! readerSettings . pdfInverted ;
this . classList . toggle ( 'active' , readerSettings . pdfInverted ) ;
applyReaderSettings ( true ) ;
saveReaderSettings ( ) ;
} ) ;
2026-04-05 13:18:25 +02:00
panel . querySelector ( '#rs-spread' ) . addEventListener ( 'click' , function ( ) {
readerSettings . pdfSpread = ! readerSettings . pdfSpread ;
this . classList . toggle ( 'active' , readerSettings . pdfSpread ) ;
saveReaderSettings ( ) ;
reRenderPdf ( ) ;
} ) ;
}
}
2026-04-05 16:14:24 +02:00
async function applyPdfZoom ( newZoom ) {
2026-04-05 13:18:25 +02:00
readerSettings . pdfZoom = newZoom ;
const zoomRange = document . getElementById ( 'rs-zoom' ) ;
const zoomVal = document . getElementById ( 'rs-zoom-val' ) ;
if ( zoomRange ) zoomRange . value = newZoom ;
if ( zoomVal ) zoomVal . textContent = newZoom + '%' ;
saveReaderSettings ( ) ;
if ( readerSettings . pdfPaginated ) {
pdfSmartZoomPage ( pdfCurrentPage ) ;
} else {
2026-04-05 16:14:24 +02:00
await reRenderPdf ( ) ;
2026-04-05 18:50:01 +02:00
}
}
function _currentScrollPage ( contentEl ) {
const wrappers = contentEl . querySelectorAll ( '.pdf-page-wrapper' ) ;
const viewTop = contentEl . scrollTop ;
for ( const w of wrappers ) {
if ( w . offsetTop + w . offsetHeight > viewTop ) {
const n = parseInt ( w . id . replace ( 'pdf-page-' , '' ) , 10 ) ;
if ( n ) return n ;
2026-04-05 13:18:25 +02:00
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-04-05 18:50:01 +02:00
return pdfCurrentPage || 1 ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
async function reRenderPdf ( ) {
if ( ! currentPdfBuffer ) return ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
2026-04-05 18:50:01 +02:00
const pivot = readerSettings . pdfPaginated ? pdfCurrentPage : _currentScrollPage ( contentEl ) ;
await renderPdf ( currentPdfBuffer , contentEl , undefined , pivot ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( readerSettings . pdfPaginated ) enterPdfPaginatedMode ( ) ;
}
// ---------------------------------------------------------------------------
// PDF Paginated Mode
// ---------------------------------------------------------------------------
function enterPdfPaginatedMode ( ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
contentEl . classList . add ( 'pdf-paginated' ) ;
contentEl . style . overflow = 'hidden' ;
2026-03-20 20:22:11 +01:00
const pdfVp = document . getElementById ( 'pdf-viewport' ) ;
if ( pdfVp ) pdfVp . style . zoom = 1 ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const wrappers = contentEl . querySelectorAll ( '.pdf-page-wrapper' ) ;
wrappers . forEach ( ( w , i ) => {
w . style . display = ( i + 1 === pdfCurrentPage ) ? '' : 'none' ;
} ) ;
pdfSmartZoomPage ( pdfCurrentPage ) ;
// Tap left/right to navigate
contentEl . addEventListener ( 'click' , _pdfPaginatedClick ) ;
}
function exitPdfPaginatedMode ( ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
contentEl . classList . remove ( 'pdf-paginated' ) ;
contentEl . style . overflow = '' ;
contentEl . removeEventListener ( 'click' , _pdfPaginatedClick ) ;
2026-04-05 16:14:24 +02:00
reRenderPdf ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
function _pdfPaginatedClick ( e ) {
const w = e . currentTarget . clientWidth ;
if ( e . clientX < w * 0.4 ) pdfGoToPage ( pdfCurrentPage - 1 ) ;
else if ( e . clientX > w * 0.6 ) pdfGoToPage ( pdfCurrentPage + 1 ) ;
}
2026-04-05 13:18:25 +02:00
function _pdfTouchStart ( e ) {
if ( e . touches . length === 2 ) {
const dx = e . touches [ 0 ] . clientX - e . touches [ 1 ] . clientX ;
const dy = e . touches [ 0 ] . clientY - e . touches [ 1 ] . clientY ;
_pinchStartDist = Math . hypot ( dx , dy ) ;
_pinchStartZoom = readerSettings . pdfZoom ;
_isPinching = true ;
const ce = $ ( 'reader-content' ) ;
if ( ce ) ce . classList . add ( 'pinch-active' ) ;
} else {
_touchStartX = e . touches [ 0 ] . clientX ;
_isPinching = false ;
}
}
function _pdfTouchMove ( e ) {
if ( e . touches . length !== 2 || ! _isPinching ) return ;
e . preventDefault ( ) ;
const dx = e . touches [ 0 ] . clientX - e . touches [ 1 ] . clientX ;
const dy = e . touches [ 0 ] . clientY - e . touches [ 1 ] . clientY ;
const dist = Math . hypot ( dx , dy ) ;
if ( _pinchStartDist === 0 ) return ;
const liveZoom = Math . max ( 50 , Math . min ( 200 , _pinchStartZoom * ( dist / _pinchStartDist ) ) ) ;
const vp = document . getElementById ( 'pdf-viewport' ) ;
if ( vp ) vp . style . zoom = liveZoom / 100 ;
}
function _pdfTouchEnd ( e ) {
if ( _isPinching ) {
_isPinching = false ;
const ce = $ ( 'reader-content' ) ;
if ( ce ) ce . classList . remove ( 'pinch-active' ) ;
const vp = document . getElementById ( 'pdf-viewport' ) ;
const liveZoom = vp ? parseFloat ( vp . style . zoom ) * 100 : readerSettings . pdfZoom ;
const snapped = Math . max ( 50 , Math . min ( 200 , Math . round ( liveZoom / 10 ) * 10 ) ) ;
applyPdfZoom ( snapped ) ;
return ;
}
if ( ! readerSettings . pdfPaginated ) return ;
const delta = e . changedTouches [ 0 ] . clientX - _touchStartX ;
if ( delta > 50 ) pdfGoToPage ( pdfCurrentPage - 1 ) ;
else if ( delta < - 50 ) pdfGoToPage ( pdfCurrentPage + 1 ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function pdfGoToPage ( n ) {
if ( ! currentPdfDoc ) return ;
n = Math . max ( 1 , Math . min ( pdfTotalPages , n ) ) ;
if ( n === pdfCurrentPage ) return ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
const oldWrapper = contentEl . querySelector ( ` #pdf-page- ${ pdfCurrentPage } ` ) ;
if ( oldWrapper ) oldWrapper . style . display = 'none' ;
pdfCurrentPage = n ;
const newWrapper = contentEl . querySelector ( ` #pdf-page- ${ pdfCurrentPage } ` ) ;
if ( newWrapper ) newWrapper . style . display = '' ;
pdfSmartZoomPage ( pdfCurrentPage ) ;
const progressInput = $ ( 'reader-progress-input' ) ;
if ( progressInput ) progressInput . value = pdfCurrentPage ;
}
async function pdfSmartZoomPage ( pageNum ) {
if ( ! currentPdfDoc ) return ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
const wrapper = contentEl . querySelector ( ` #pdf-page- ${ pageNum } ` ) ;
if ( ! wrapper ) return ;
const canvas = wrapper . querySelector ( 'canvas' ) ;
if ( ! canvas ) return ;
const page = await currentPdfDoc . getPage ( pageNum ) ;
const naturalVp = page . getViewport ( { scale : 1 } ) ;
const pageW = naturalVp . width ;
const pageH = naturalVp . height ;
let bbox = _pdfPageTextBoxCache [ pageNum ] ;
if ( ! bbox ) {
bbox = await _computePdfTextBox ( page , pageW , pageH ) ;
_pdfPageTextBoxCache [ pageNum ] = bbox ;
}
const containerW = contentEl . clientWidth ;
const containerH = contentEl . clientHeight ;
const contentW = bbox . x2 - bbox . x1 ;
const contentH = bbox . y2 - bbox . y1 ;
const pad = 12 ;
const scale = Math . min (
( containerW - pad * 2 ) / contentW ,
( containerH - pad * 2 ) / contentH
2026-03-20 20:02:29 +01:00
) * ( readerSettings . pdfZoom / 100 ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Re-render canvas at new scale if significantly different
const currentScale = canvas . width / naturalVp . width ;
if ( Math . abs ( scale - currentScale ) / currentScale > 0.05 ) {
const vp = page . getViewport ( { scale } ) ;
canvas . width = vp . width ;
canvas . height = vp . height ;
await page . render ( { canvasContext : canvas . getContext ( '2d' ) , viewport : vp } ) . promise ;
}
// Position canvas to center the text bounding box
const renderedScale = canvas . width / naturalVp . width ;
const offsetX = - renderedScale * ( bbox . x1 - pad ) + ( containerW - renderedScale * contentW - pad * 2 ) / 2 ;
// PDF y-axis is bottom-up; canvas is top-down
const offsetY = - renderedScale * ( pageH - bbox . y2 - pad ) + ( containerH - renderedScale * contentH - pad * 2 ) / 2 ;
canvas . style . transform = ` translate( ${ offsetX } px, ${ offsetY } px) ` ;
wrapper . style . overflow = 'hidden' ;
wrapper . style . width = containerW + 'px' ;
wrapper . style . height = containerH + 'px' ;
}
async function _computePdfTextBox ( page , pageW , pageH ) {
// Tier 1: text-based
try {
const tc = await page . getTextContent ( ) ;
if ( tc . items && tc . items . length ) {
let x1 = Infinity , y1 = Infinity , x2 = - Infinity , y2 = - Infinity ;
for ( const item of tc . items ) {
if ( ! item . transform ) continue ;
const tx = item . transform [ 4 ] , ty = item . transform [ 5 ] ;
const iw = item . width || 0 , ih = item . height || 0 ;
if ( tx < x1 ) x1 = tx ;
if ( ty < y1 ) y1 = ty ;
if ( tx + iw > x2 ) x2 = tx + iw ;
if ( ty + ih > y2 ) y2 = ty + ih ;
}
const area = ( x2 - x1 ) * ( y2 - y1 ) ;
if ( isFinite ( x1 ) && area > pageW * pageH * 0.25 ) {
return { x1 , y1 , x2 , y2 } ;
}
}
} catch ( e ) { }
// Tier 2: pixel analysis at scale 0.3
try {
const lowScale = 0.3 ;
const vp = page . getViewport ( { scale : lowScale } ) ;
const offCanvas = document . createElement ( 'canvas' ) ;
offCanvas . width = vp . width ;
offCanvas . height = vp . height ;
const ctx = offCanvas . getContext ( '2d' ) ;
await page . render ( { canvasContext : ctx , viewport : vp } ) . promise ;
const { data , width , height } = ctx . getImageData ( 0 , 0 , vp . width , vp . height ) ;
let rMin = height , rMax = 0 , cMin = width , cMax = 0 ;
for ( let r = 0 ; r < height ; r ++ ) {
for ( let c = 0 ; c < width ; c ++ ) {
const idx = ( r * width + c ) * 4 ;
if ( data [ idx ] + data [ idx + 1 ] + data [ idx + 2 ] < 720 ) {
if ( r < rMin ) rMin = r ;
if ( r > rMax ) rMax = r ;
if ( c < cMin ) cMin = c ;
if ( c > cMax ) cMax = c ;
}
}
}
if ( rMin < rMax && cMin < cMax ) {
return {
x1 : cMin / lowScale ,
y1 : ( height - rMax ) / lowScale ,
x2 : cMax / lowScale ,
y2 : ( height - rMin ) / lowScale ,
} ;
}
} catch ( e ) { }
// Fallback: full page
return { x1 : 0 , y1 : 0 , x2 : pageW , y2 : pageH } ;
}
// ---------------------------------------------------------------------------
// Bookmarks
// ---------------------------------------------------------------------------
async function loadBookmarks ( bookId ) {
try {
const res = await fetch ( ` /books/ ${ bookId } /bookmarks/ ` ) ;
const { ct , iv } = await res . json ( ) ;
if ( ct ) {
2026-05-29 21:14:40 +02:00
_saveAnnotationsCache ( bookId , 'bookmarks' , ct , iv ) ; // persist for offline
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , iv , ct ) ;
currentBookmarks = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
} else {
currentBookmarks = [ ] ;
}
} catch ( e ) {
2026-05-29 21:14:40 +02:00
// offline fallback
const cached = await _loadAnnotationsCache ( bookId , 'bookmarks' ) ;
if ( cached ? . ct ) {
try {
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , cached . iv , cached . ct ) ;
currentBookmarks = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
} catch { currentBookmarks = [ ] ; }
} else {
currentBookmarks = [ ] ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
async function saveBookmarks ( ) {
if ( ! currentBookId ) return ;
try {
const key = await getOrCreateEncKey ( ) ;
const plain = new TextEncoder ( ) . encode ( JSON . stringify ( currentBookmarks ) ) ;
const { iv , ciphertext } = await encryptBytes ( key , plain ) ;
const body = JSON . stringify ( { ct : ciphertext , iv } ) ;
const url = ` /books/ ${ currentBookId } /bookmarks/ ` ;
_lastBookmarkBeacon = { url , body } ;
2026-05-29 21:14:40 +02:00
_saveAnnotationsCache ( currentBookId , 'bookmarks' , ciphertext , iv ) ; // keep local cache current
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
await fetch ( url , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body ,
} ) ;
bookmarksDirty = false ;
2026-05-30 07:22:32 +02:00
} catch ( e ) {
_markPendingSync ( currentBookId , 'bookmarks' ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
function addBookmark ( ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl || ! currentBookId ) return ;
let label , anchor , scrollFraction ;
if ( currentPdfDoc ) {
const page = pdfCurrentPage || parseInt ( $ ( 'reader-progress-input' ) ? . value , 10 ) || 1 ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
label = ` Seite ${ page } ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
anchor = ` pdf-page- ${ page } ` ;
scrollFraction = ( page - 1 ) / Math . max ( 1 , pdfTotalPages - 1 ) ;
} else {
// Find first visible chapter div
const chapters = contentEl . querySelectorAll ( '[data-epub-src]' ) ;
let visibleChapter = null ;
for ( const ch of chapters ) {
const rect = ch . getBoundingClientRect ( ) ;
if ( rect . bottom > 0 && rect . top < window . innerHeight ) {
visibleChapter = ch ;
break ;
}
}
const src = visibleChapter ? . getAttribute ( 'data-epub-src' ) || '' ;
label = src . split ( '/' ) . pop ( ) . replace ( /\.x?html?$/i , '' ) || 'Bookmark' ;
anchor = src ;
scrollFraction = contentEl . scrollTop / ( contentEl . scrollHeight - contentEl . clientHeight || 1 ) ;
}
const bm = {
id : crypto . randomUUID ( ) ,
label ,
anchor ,
scrollFraction ,
createdAt : new Date ( ) . toISOString ( ) ,
} ;
currentBookmarks . unshift ( bm ) ;
bookmarksDirty = true ;
saveBookmarks ( ) ;
// Toast
const toast = document . createElement ( 'div' ) ;
toast . className = 'reader-toast' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
toast . textContent = ` ★ Lesezeichen: ${ label } ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
document . body . appendChild ( toast ) ;
setTimeout ( ( ) => toast . remove ( ) , 2200 ) ;
}
function openBookmarksSidebar ( ) {
if ( ! currentBookmarks . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
openSidebar ( 'Lesezeichen' , '<p class="muted">Noch keine Lesezeichen. Tippe beim Lesen auf ★.</p>' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
return ;
}
let html = '<ul style="list-style:none;padding:0;">' ;
for ( const bm of currentBookmarks ) {
html += ` <li class="bookmark-entry">
< button class = "btn-link" data - jump - bookmark = "${escapeHtml(bm.id)}" style = "flex:1;text-align:left;" >
$ { escapeHtml ( bm . label ) }
< / b u t t o n >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn-icon" data - delete - bookmark = "${escapeHtml(bm.id)}" title = "Löschen" > ✕ < / b u t t o n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< / l i > ` ;
}
html += '</ul>' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
openSidebar ( 'Lesezeichen' , html ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const body = $ ( 'sidebar-body' ) ;
body . addEventListener ( 'click' , function _bmClick ( e ) {
const jumpBtn = e . target . closest ( '[data-jump-bookmark]' ) ;
const delBtn = e . target . closest ( '[data-delete-bookmark]' ) ;
if ( jumpBtn ) {
body . removeEventListener ( 'click' , _bmClick ) ;
jumpToBookmark ( jumpBtn . dataset . jumpBookmark ) ;
}
if ( delBtn ) {
const id = delBtn . dataset . deleteBookmark ;
currentBookmarks = currentBookmarks . filter ( b => b . id !== id ) ;
bookmarksDirty = true ;
saveBookmarks ( ) ;
openBookmarksSidebar ( ) ; // re-render
}
} ) ;
}
function jumpToBookmark ( id ) {
const bm = currentBookmarks . find ( b => b . id === id ) ;
if ( ! bm ) return ;
closeSidebar ( ) ;
2026-08-04 09:01:52 +02:00
_suppressScrollJumpDetect ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
setTimeout ( ( ) => {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
if ( bm . anchor . startsWith ( 'pdf-page-' ) ) {
if ( readerSettings . pdfPaginated ) {
pdfGoToPage ( parseInt ( bm . anchor . replace ( 'pdf-page-' , '' ) , 10 ) || 1 ) ;
} else {
const target = contentEl . querySelector ( '#' + bm . anchor ) ;
if ( target ) {
const top = target . getBoundingClientRect ( ) . top - contentEl . getBoundingClientRect ( ) . top ;
contentEl . scrollBy ( { top : top - 16 , behavior : 'smooth' } ) ;
}
}
} else {
const target = Array . from ( contentEl . querySelectorAll ( '[data-epub-src]' ) )
. find ( el => el . getAttribute ( 'data-epub-src' ) === bm . anchor ) ;
if ( target ) {
const top = target . getBoundingClientRect ( ) . top - contentEl . getBoundingClientRect ( ) . top ;
contentEl . scrollBy ( { top : top - 16 , behavior : 'smooth' } ) ;
} else {
contentEl . scrollTop = bm . scrollFraction * ( contentEl . scrollHeight - contentEl . clientHeight ) ;
}
}
} , 50 ) ;
}
// ---------------------------------------------------------------------------
// Reader Search
// ---------------------------------------------------------------------------
let _readerSearchDebounce = null ;
function toggleReaderSearch ( ) {
const overlay = $ ( 'reader-overlay' ) ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! overlay || ! contentEl ) return ;
const existing = document . getElementById ( 'reader-search-bar' ) ;
if ( existing ) {
existing . remove ( ) ;
readerSearchOpen = false ;
clearReaderSearch ( ) ;
return ;
}
readerSearchOpen = true ;
const bar = document . createElement ( 'div' ) ;
bar . id = 'reader-search-bar' ;
bar . className = 'reader-search-bar' ;
bar . innerHTML = `
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< input type = "text" id = "reader-search-input" class = "search-input" placeholder = "Suchen …" style = "width:160px;" >
< button class = "btn-icon" id = "rs-search-prev" title = "Vorheriger Treffer" > ↑ < / b u t t o n >
< button class = "btn-icon" id = "rs-search-next" title = "Nächster Treffer" > ↓ < / b u t t o n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< span id = "rs-search-count" class = "muted" > < / s p a n >
< button class = "btn-icon" id = "rs-search-clear" title = "Close" > ✕ < / b u t t o n >
` ;
2026-08-04 10:17:01 +02:00
overlay . insertBefore ( bar , $ ( 'reader-body-row' ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const input = bar . querySelector ( '#reader-search-input' ) ;
input . focus ( ) ;
input . addEventListener ( 'input' , ( ) => {
clearTimeout ( _readerSearchDebounce ) ;
_readerSearchDebounce = setTimeout ( ( ) => doReaderSearch ( input . value . trim ( ) ) , 300 ) ;
} ) ;
input . addEventListener ( 'keydown' , e => {
if ( e . key === 'Enter' ) { e . shiftKey ? readerSearchPrev ( ) : readerSearchNext ( ) ; }
if ( e . key === 'Escape' ) { toggleReaderSearch ( ) ; }
} ) ;
bar . querySelector ( '#rs-search-prev' ) . addEventListener ( 'click' , readerSearchPrev ) ;
bar . querySelector ( '#rs-search-next' ) . addEventListener ( 'click' , readerSearchNext ) ;
bar . querySelector ( '#rs-search-clear' ) . addEventListener ( 'click' , toggleReaderSearch ) ;
}
async function doReaderSearch ( query ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
const countEl = document . getElementById ( 'rs-search-count' ) ;
clearReaderSearchHighlights ( ) ;
searchMatches = [ ] ;
searchMatchIndex = - 1 ;
if ( ! query ) { if ( countEl ) countEl . textContent = '' ; return ; }
if ( ! currentPdfDoc ) {
// EPUB: snapshot original content
if ( ! searchOriginalContent ) {
searchOriginalContent = contentEl . innerHTML ;
} else {
contentEl . innerHTML = searchOriginalContent ;
applyHighlightsToContent ( ) ;
}
const walker = document . createTreeWalker ( contentEl , NodeFilter . SHOW _TEXT ) ;
const lq = query . toLowerCase ( ) ;
const ranges = [ ] ;
let node ;
while ( ( node = walker . nextNode ( ) ) ) {
const text = node . textContent ;
const lt = text . toLowerCase ( ) ;
let idx = 0 ;
while ( ( idx = lt . indexOf ( lq , idx ) ) !== - 1 ) {
const range = document . createRange ( ) ;
range . setStart ( node , idx ) ;
range . setEnd ( node , idx + query . length ) ;
ranges . push ( range ) ;
idx += query . length ;
}
}
// Insert marks in reverse to preserve range validity
for ( let i = ranges . length - 1 ; i >= 0 ; i -- ) {
try {
const mark = document . createElement ( 'mark' ) ;
mark . className = 'reader-search-match' ;
ranges [ i ] . surroundContents ( mark ) ;
searchMatches . unshift ( mark ) ;
} catch ( e ) { }
}
} else {
// PDF: collect text layer spans
const spans = contentEl . querySelectorAll ( '.pdf-text-layer > span' ) ;
const lq = query . toLowerCase ( ) ;
for ( const span of spans ) {
if ( span . textContent . toLowerCase ( ) . includes ( lq ) ) {
span . classList . add ( 'reader-search-match' ) ;
searchMatches . push ( span ) ;
}
}
}
if ( countEl ) countEl . textContent = searchMatches . length ? ` 1 / ${ searchMatches . length } ` : '0' ;
if ( searchMatches . length ) {
searchMatchIndex = 0 ;
scrollToSearchMatch ( 0 ) ;
}
}
function clearReaderSearchHighlights ( ) {
if ( ! currentPdfDoc ) {
// EPUB: restore from snapshot
if ( searchOriginalContent !== null ) {
const contentEl = $ ( 'reader-content' ) ;
if ( contentEl ) {
contentEl . innerHTML = searchOriginalContent ;
applyHighlightsToContent ( ) ;
}
searchOriginalContent = null ;
} else {
// Just remove marks without full restore
document . querySelectorAll ( 'mark.reader-search-match' ) . forEach ( m => {
const parent = m . parentNode ;
while ( m . firstChild ) parent . insertBefore ( m . firstChild , m ) ;
parent . removeChild ( m ) ;
} ) ;
}
} else {
// PDF: remove highlight class from spans
document . querySelectorAll ( '.reader-search-match' ) . forEach ( el => {
el . classList . remove ( 'reader-search-match' , 'active' ) ;
} ) ;
}
searchMatches = [ ] ;
searchMatchIndex = - 1 ;
}
function clearReaderSearch ( ) {
clearTimeout ( _readerSearchDebounce ) ;
clearReaderSearchHighlights ( ) ;
readerSearchOpen = false ;
const countEl = document . getElementById ( 'rs-search-count' ) ;
if ( countEl ) countEl . textContent = '' ;
}
function scrollToSearchMatch ( idx ) {
if ( ! searchMatches . length ) return ;
2026-08-04 09:01:52 +02:00
_suppressScrollJumpDetect ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
searchMatches . forEach ( ( m , i ) => m . classList . toggle ( 'active' , i === idx ) ) ;
searchMatches [ idx ] . scrollIntoView ( { behavior : 'smooth' , block : 'center' } ) ;
const countEl = document . getElementById ( 'rs-search-count' ) ;
if ( countEl ) countEl . textContent = ` ${ idx + 1 } / ${ searchMatches . length } ` ;
}
function readerSearchNext ( ) {
if ( ! searchMatches . length ) return ;
searchMatchIndex = ( searchMatchIndex + 1 ) % searchMatches . length ;
scrollToSearchMatch ( searchMatchIndex ) ;
}
function readerSearchPrev ( ) {
if ( ! searchMatches . length ) return ;
searchMatchIndex = ( searchMatchIndex - 1 + searchMatches . length ) % searchMatches . length ;
scrollToSearchMatch ( searchMatchIndex ) ;
}
// ---------------------------------------------------------------------------
// Highlights
// ---------------------------------------------------------------------------
async function loadHighlights ( bookId ) {
try {
const res = await fetch ( ` /books/ ${ bookId } /highlights/ ` ) ;
const { ct , iv } = await res . json ( ) ;
if ( ct ) {
2026-05-29 21:14:40 +02:00
_saveAnnotationsCache ( bookId , 'highlights' , ct , iv ) ; // persist for offline
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , iv , ct ) ;
currentHighlights = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
} else {
currentHighlights = [ ] ;
}
applyHighlightsToContent ( ) ;
} catch ( e ) {
2026-05-29 21:14:40 +02:00
// offline fallback
const cached = await _loadAnnotationsCache ( bookId , 'highlights' ) ;
if ( cached ? . ct ) {
try {
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , cached . iv , cached . ct ) ;
currentHighlights = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
} catch { currentHighlights = [ ] ; }
} else {
currentHighlights = [ ] ;
}
applyHighlightsToContent ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
async function saveHighlights ( ) {
if ( ! currentBookId ) return ;
try {
const key = await getOrCreateEncKey ( ) ;
const plain = new TextEncoder ( ) . encode ( JSON . stringify ( currentHighlights ) ) ;
const { iv , ciphertext } = await encryptBytes ( key , plain ) ;
const body = JSON . stringify ( { ct : ciphertext , iv } ) ;
const url = ` /books/ ${ currentBookId } /highlights/ ` ;
_lastHighlightBeacon = { url , body } ;
2026-05-29 21:14:40 +02:00
_saveAnnotationsCache ( currentBookId , 'highlights' , ciphertext , iv ) ; // keep local cache current
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
await fetch ( url , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body ,
} ) ;
highlightsDirty = false ;
2026-05-30 07:22:32 +02:00
} catch ( e ) {
_markPendingSync ( currentBookId , 'highlights' ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
let _highlightSaveDebounce = null ;
function debounceSaveHighlights ( ) {
clearTimeout ( _highlightSaveDebounce ) ;
_highlightSaveDebounce = setTimeout ( saveHighlights , 2000 ) ;
}
function applyHighlightsToContent ( ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl || currentPdfDoc ) return ;
for ( const h of currentHighlights ) {
try { renderHighlight ( h ) ; } catch ( e ) { }
}
2026-08-04 10:17:01 +02:00
_repositionMarginMarkers ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-08-04 10:17:01 +02:00
// Resolves a highlight/note's stored anchor back to a live DOM Range. Shared by
// rendering, margin-marker positioning and export ordering — all three need the
// same "where in the current DOM is this annotation" answer.
function resolveAnchorRange ( h ) {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const contentEl = $ ( 'reader-content' ) ;
2026-08-04 10:17:01 +02:00
if ( ! contentEl || ! h . anchor ) return null ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
let range = null ;
try {
2026-10-14 18:15:58 +02:00
const { blocks } = _epubBlocks ( contentEl ) ;
2026-08-04 13:28:19 +02:00
const startBlock = blocks [ h . anchor . startBlockIndex ] ;
const endBlock = blocks [ h . anchor . endBlockIndex ] ? ? startBlock ;
if ( startBlock && endBlock ) {
const start = _nodeAtCharOffset ( startBlock , h . anchor . startChar ) ;
const end = _nodeAtCharOffset ( endBlock , h . anchor . endChar ) ;
if ( start && end ) {
range = document . createRange ( ) ;
range . setStart ( start . node , start . offset ) ;
range . setEnd ( end . node , end . offset ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
} catch ( e ) { }
2026-08-04 13:28:19 +02:00
// Fallback: quote substring search within the chapter
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( ! range && h . anchor . quote ) {
2026-08-04 13:28:19 +02:00
const chapterEl = contentEl . querySelector ( ` [data-epub-src=" ${ CSS . escape ( h . anchor . chapterSrc || '' ) } "] ` )
|| contentEl ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const walker = document . createTreeWalker ( chapterEl , NodeFilter . SHOW _TEXT ) ;
let node ;
while ( ( node = walker . nextNode ( ) ) ) {
const idx = node . textContent . indexOf ( h . anchor . quote ) ;
if ( idx !== - 1 ) {
range = document . createRange ( ) ;
range . setStart ( node , idx ) ;
range . setEnd ( node , idx + h . anchor . quote . length ) ;
break ;
}
}
}
2026-08-04 10:17:01 +02:00
return range ;
}
function renderHighlight ( h ) {
2026-08-04 10:49:30 +02:00
// Freeform notes carry no in-text mark — their anchor exists purely to
// position the note card in the margin (resolveAnchorRange handles that
// directly from the stored xpath, no DOM wrapper needed).
if ( h . type === 'note' ) return ;
2026-08-04 10:17:01 +02:00
const range = resolveAnchorRange ( h ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
if ( ! range ) return ;
try {
const mark = document . createElement ( 'mark' ) ;
mark . dataset . highlightId = h . id ;
2026-08-04 10:49:30 +02:00
mark . className = 'epub-highlight' ;
mark . dataset . color = h . color || 'yellow' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
range . surroundContents ( mark ) ;
} catch ( e ) { }
}
2026-08-04 13:28:19 +02:00
// Character-offset addressing within a block element (p, li, ...), rather
// than XPath-to-a-specific-text-node: XPath sibling indices shift whenever
// ANY highlight in the same block gets wrapped/unwrapped in a <mark> (that
// splits/merges text nodes), silently corrupting OTHER highlights' anchors
// in the same block. A block's overall textContent length/order is never
// touched by mark-wrapping, so a character offset into it stays valid no
// matter how many other highlights come and go around it.
function _charOffsetInBlock ( block , node , nodeOffset ) {
const walker = document . createTreeWalker ( block , NodeFilter . SHOW _TEXT ) ;
let count = 0 , n ;
while ( ( n = walker . nextNode ( ) ) ) {
if ( n === node ) return count + nodeOffset ;
count += n . textContent . length ;
}
return count ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-08-04 13:28:19 +02:00
function _nodeAtCharOffset ( block , targetOffset ) {
const walker = document . createTreeWalker ( block , NodeFilter . SHOW _TEXT ) ;
let count = 0 , lastNode = null , n ;
while ( ( n = walker . nextNode ( ) ) ) {
lastNode = n ;
const len = n . textContent . length ;
if ( targetOffset <= count + len ) {
return { node : n , offset : Math . max ( 0 , targetOffset - count ) } ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-08-04 13:28:19 +02:00
count += len ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
2026-08-04 13:28:19 +02:00
return lastNode ? { node : lastNode , offset : lastNode . textContent . length } : null ;
}
function _blockAncestor ( node ) {
const el = node . nodeType === Node . ELEMENT _NODE ? node : node . parentElement ;
return el ? el . closest ( EPUB _BLOCK _SELECTOR ) : null ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
function buildEpubAnchor ( range ) {
const contentEl = $ ( 'reader-content' ) ;
const chapterEl = range . commonAncestorContainer . nodeType === Node . ELEMENT _NODE
? range . commonAncestorContainer . closest ( '[data-epub-src]' )
: range . commonAncestorContainer . parentElement ? . closest ( '[data-epub-src]' ) ;
2026-08-04 13:28:19 +02:00
const startBlock = _blockAncestor ( range . startContainer ) ;
const endBlock = _blockAncestor ( range . endContainer ) || startBlock ;
const blocks = Array . from ( contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
return {
type : 'epub' ,
chapterSrc : chapterEl ? . getAttribute ( 'data-epub-src' ) || '' ,
2026-08-04 13:28:19 +02:00
startBlockIndex : startBlock ? blocks . indexOf ( startBlock ) : - 1 ,
startChar : startBlock ? _charOffsetInBlock ( startBlock , range . startContainer , range . startOffset ) : 0 ,
endBlockIndex : endBlock ? blocks . indexOf ( endBlock ) : - 1 ,
endChar : endBlock ? _charOffsetInBlock ( endBlock , range . endContainer , range . endOffset ) : 0 ,
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
quote : range . toString ( ) . slice ( 0 , 200 ) ,
} ;
}
2026-08-04 10:49:30 +02:00
// Marker mode — armed via the pen button in the reader header. Text selection
// only opens the highlight popover while this is active, so normal selection
// (e.g. to copy text) isn't hijacked, and the feature has a discoverable
// on/off switch instead of being an invisible "just select text" gesture.
let markerModeActive = false ;
function toggleMarkerMode ( ) {
markerModeActive = ! markerModeActive ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
document . querySelectorAll ( '.marker-mode-btn' ) . forEach ( b => b . classList . toggle ( 'active' , markerModeActive ) ) ;
}
// Note editing/creation happens in the margin column on desktop, but a note
// bottom sheet (see showNoteBottomSheet) on narrow viewports — typing into a
// 150px margin strip on a phone keyboard is unpleasant, and opening that
// column at all is exactly the friction we're trying to remove for mobile
// note-taking. Reviewing existing notes in depth is expected to stay a
// desktop activity; this only affects where the editor for a note appears.
function _isMobileReader ( ) {
return window . innerWidth <= 600 ;
2026-08-04 10:49:30 +02:00
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
function handleReaderSelection ( e ) {
2026-08-04 10:49:30 +02:00
// Clicking an existing highlight always shows its tooltip, regardless of marker mode
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const hlMark = e . target . closest ( '.epub-highlight' ) ;
if ( hlMark ) {
dismissHighlightPopover ( ) ;
const id = hlMark . dataset . highlightId ;
const h = currentHighlights . find ( x => x . id === id ) ;
showHighlightTooltip ( hlMark , h ) ;
return ;
}
2026-08-04 10:49:30 +02:00
if ( ! markerModeActive ) return ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
// Don't hijack taps on real interactive content (footnote refs, external
// links) — but plenty of EPUBs wrap plain reading text in a bare, hrefless
// <a id="..."> used only as an internal jump target (sanitizeEpubHtml
// strips its href but the tag itself stays); those aren't actually
// clickable and must not block note-taking, so check for a surviving
// href/footnote-ref rather than just "is inside some <a>".
if ( e . target . closest ( 'button, [data-footnote-ref], a[href]' ) ) return ;
2026-08-04 10:49:30 +02:00
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
dismissHighlightPopover ( ) ;
const sel = window . getSelection ( ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
if ( sel && ! sel . isCollapsed && sel . rangeCount ) {
const range = sel . getRangeAt ( 0 ) ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl || ! contentEl . contains ( range . commonAncestorContainer ) ) return ;
if ( range . toString ( ) . trim ( ) . length === 0 ) return ;
showHighlightPopover ( range ) ;
return ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
// A plain tap with nothing selected — in marker mode this means "drop a
// freeform note here" instead of doing nothing, so a note-only annotation
// never depends on finding the margin's precise click target (especially
// hard on a narrow phone screen — see createFreeformNote/_openNoteEditor).
if ( typeof e . clientX === 'number' && typeof e . clientY === 'number' ) {
const range = _wordRangeAtPoint ( e . clientX , e . clientY ) ;
if ( range ) createFreeformNote ( range ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
function showHighlightPopover ( range ) {
const rect = range . getBoundingClientRect ( ) ;
const popover = document . createElement ( 'div' ) ;
popover . id = 'highlight-popover' ;
popover . className = 'highlight-popover' ;
popover . innerHTML = `
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "hl-color-btn" data - hl - color = "yellow" style = "background:#f1c40f" title = "Gelb" > A < / b u t t o n >
< button class = "hl-color-btn" data - hl - color = "green" style = "background:#2ecc71" title = "Grün" > A < / b u t t o n >
< button class = "hl-color-btn" data - hl - color = "blue" style = "background:#3498db" title = "Blau" > A < / b u t t o n >
< button class = "hl-color-btn" data - hl - color = "red" style = "background:#e63946" title = "Rot" > A < / b u t t o n >
< button class = "hl-note-btn" title = "Notiz hinzufügen" > ✎ < / b u t t o n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
` ;
2026-08-04 10:49:30 +02:00
popover . style . visibility = 'hidden' ; // measure before showing, to avoid a flash at the wrong spot
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
document . body . appendChild ( popover ) ;
currentHighlightPopover = popover ;
2026-08-04 10:49:30 +02:00
// Clamp within the viewport — a selection near the top/edge of the page must
// not produce an invisible, off-screen popover (flip below when there's no
// room above; keep it horizontally on-screen).
const pRect = popover . getBoundingClientRect ( ) ;
const minTop = window . scrollY + 8 ;
const maxTop = window . scrollY + window . innerHeight - pRect . height - 8 ;
let top = rect . top + window . scrollY - 44 ;
if ( top < minTop ) top = rect . bottom + window . scrollY + 8 ; // no room above -> try below
top = Math . max ( minTop , Math . min ( top , maxTop ) ) ;
let left = rect . left + window . scrollX + rect . width / 2 - pRect . width / 2 ;
left = Math . max ( window . scrollX + 8 , Math . min ( left , window . scrollX + window . innerWidth - pRect . width - 8 ) ) ;
popover . style . top = top + 'px' ;
popover . style . left = left + 'px' ;
popover . style . visibility = '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Store range info before selection is cleared
const savedRange = range . cloneRange ( ) ;
popover . addEventListener ( 'click' , e => {
const colorBtn = e . target . closest ( '.hl-color-btn' ) ;
const noteBtn = e . target . closest ( '.hl-note-btn' ) ;
if ( colorBtn ) {
createHighlight ( colorBtn . dataset . hlColor , savedRange ) ;
} else if ( noteBtn ) {
createHighlightWithNote ( savedRange ) ;
}
} ) ;
}
function showHighlightTooltip ( markEl , h ) {
const rect = markEl . getBoundingClientRect ( ) ;
const popover = document . createElement ( 'div' ) ;
popover . id = 'highlight-popover' ;
popover . className = 'highlight-popover' ;
popover . style . flexDirection = 'column' ;
popover . style . maxWidth = '220px' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const noteText = h ? . note ? escapeHtml ( h . note ) : '<span class="muted">Keine Notiz</span>' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
popover . innerHTML = `
< div style = "font-size:12px;padding-bottom:4px;" > $ { noteText } < / d i v >
< div style = "display:flex;gap:6px;" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn btn-sm" data - hl - edit - note = "${escapeHtml(h?.id || '')}" > Notiz bearbeiten < / b u t t o n >
< button class = "btn btn-sm btn-danger" data - hl - delete = "${escapeHtml(h?.id || '')}" > Löschen < / b u t t o n >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< / d i v >
` ;
2026-08-04 10:49:30 +02:00
popover . style . visibility = 'hidden' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
document . body . appendChild ( popover ) ;
currentHighlightPopover = popover ;
2026-08-04 10:49:30 +02:00
const pRect = popover . getBoundingClientRect ( ) ;
const minTop = window . scrollY + 8 ;
const maxTop = window . scrollY + window . innerHeight - pRect . height - 8 ;
let top = rect . bottom + window . scrollY + 4 ;
if ( top > maxTop ) top = rect . top + window . scrollY - pRect . height - 4 ; // flip above if no room below
top = Math . max ( minTop , Math . min ( top , maxTop ) ) ;
let left = rect . left + window . scrollX ;
left = Math . max ( window . scrollX + 8 , Math . min ( left , window . scrollX + window . innerWidth - pRect . width - 8 ) ) ;
popover . style . top = top + 'px' ;
popover . style . left = left + 'px' ;
popover . style . visibility = '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
popover . addEventListener ( 'click' , ev => {
const editBtn = ev . target . closest ( '[data-hl-edit-note]' ) ;
const delBtn = ev . target . closest ( '[data-hl-delete]' ) ;
if ( editBtn && h ) {
dismissHighlightPopover ( ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
_openNoteEditor ( h ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
if ( delBtn && h ) {
dismissHighlightPopover ( ) ;
deleteHighlight ( h . id ) ;
}
} ) ;
// Close on outside click
setTimeout ( ( ) => {
document . addEventListener ( 'click' , dismissHighlightPopover , { once : true } ) ;
} , 0 ) ;
}
function createHighlight ( color , range ) {
const anchor = buildEpubAnchor ( range ) ;
const h = {
id : crypto . randomUUID ( ) ,
2026-08-04 10:17:01 +02:00
type : 'highlight' ,
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
anchor ,
color ,
note : '' ,
createdAt : new Date ( ) . toISOString ( ) ,
} ;
currentHighlights . push ( h ) ;
highlightsDirty = true ;
window . getSelection ( ) ? . removeAllRanges ( ) ;
dismissHighlightPopover ( ) ;
renderHighlight ( h ) ;
2026-08-04 10:17:01 +02:00
_repositionMarginMarkers ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
debounceSaveHighlights ( ) ;
}
function createHighlightWithNote ( range ) {
const anchor = buildEpubAnchor ( range ) ;
const h = {
id : crypto . randomUUID ( ) ,
2026-08-04 10:17:01 +02:00
type : 'highlight' ,
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
anchor ,
color : 'yellow' ,
note : '' ,
createdAt : new Date ( ) . toISOString ( ) ,
} ;
currentHighlights . push ( h ) ;
highlightsDirty = true ;
window . getSelection ( ) ? . removeAllRanges ( ) ;
dismissHighlightPopover ( ) ;
renderHighlight ( h ) ;
2026-08-04 10:17:01 +02:00
_repositionMarginMarkers ( ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
_openNoteEditor ( h ) ;
}
// Every "add/edit a note" entry point routes here — notes are always edited
// right where they conceptually live (the margin), never in the separate
// right-hand sidebar, EXCEPT on narrow viewports, where the margin itself is
// too cramped to type into and a full-width bottom sheet takes over instead.
// `pending` is only set for a not-yet-created freeform note (see
// createFreeformNote); h is null in that case until the user actually saves
// text, so an empty tap doesn't leave a permanent ghost entry behind.
function _openNoteEditor ( h , pending ) {
if ( _isMobileReader ( ) ) {
showNoteBottomSheet ( h , pending ) ;
} else {
if ( ! readerAnnotationsMarginOpen ) toggleAnnotationsMargin ( ) ;
editNoteInlineInMargin ( h , pending ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
function deleteHighlight ( id ) {
currentHighlights = currentHighlights . filter ( h => h . id !== id ) ;
highlightsDirty = true ;
// Re-apply all highlights after removing the deleted one
const contentEl = $ ( 'reader-content' ) ;
if ( contentEl && ! currentPdfDoc ) {
// Snapshot restore not available mid-session, so remove the mark manually
const mark = contentEl . querySelector ( ` mark[data-highlight-id=" ${ id } "] ` ) ;
if ( mark ) {
const parent = mark . parentNode ;
while ( mark . firstChild ) parent . insertBefore ( mark . firstChild , mark ) ;
parent . removeChild ( mark ) ;
}
}
2026-08-04 10:17:01 +02:00
_repositionMarginMarkers ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
debounceSaveHighlights ( ) ;
}
function dismissHighlightPopover ( ) {
if ( currentHighlightPopover ) {
currentHighlightPopover . remove ( ) ;
currentHighlightPopover = null ;
}
}
2026-08-04 10:17:01 +02:00
// ---------------------------------------------------------------------------
// Margin panel — a "page margin" strip left of the reader content. Shows small
// markers next to the on-screen highlights/notes, and lets you click empty
// space to anchor a freeform note at that line's height (EPUB only).
// ---------------------------------------------------------------------------
let readerAnnotationsMarginOpen = false ;
function toggleAnnotationsMargin ( ) {
const el = $ ( 'reader-margin' ) ;
const markersEl = $ ( 'reader-margin-markers' ) ;
if ( ! el || ! markersEl || currentPdfDoc ) return ;
readerAnnotationsMarginOpen = ! readerAnnotationsMarginOpen ;
el . classList . toggle ( 'open' , readerAnnotationsMarginOpen ) ;
if ( readerAnnotationsMarginOpen ) {
// Reposition once now (pre-transition) and once more after the width
// transition settles, since content reflow can shift line heights.
_repositionMarginMarkers ( ) ;
el . addEventListener ( 'transitionend' , _repositionMarginMarkers , { once : true } ) ;
} else {
markersEl . innerHTML = '' ;
}
}
2026-08-04 10:49:30 +02:00
// Renders each highlight/note at its text position in the margin. Entries
// with note text become a readable "post-it" card (the whole point of the
// margin — you can read notes there, not just see that one exists); a plain
// highlight with no note is just a small color dot. Cards are stacked
// top-to-bottom so overlapping anchors don't overlap visually.
2026-10-14 18:15:58 +02:00
// Where each highlight sits in the book's scroll coordinates. Finding a range
// and measuring it is the expensive part, and scrolling doesn't change it:
// kept until the layout does (content, width, height) or the anchor changes.
let _marginPosCache = null ;
const _marginCardHeights = new Map ( ) ; // `${stripWidth}|${note}` (or 'dot') → px
function _marginDocY ( h , contentEl ) {
const { blocks } = _epubBlocks ( contentEl ) ;
const key = ` ${ contentEl . clientWidth } x ${ contentEl . scrollHeight } ` ;
if ( ! _marginPosCache || _marginPosCache . blocks !== blocks || _marginPosCache . key !== key ) {
_marginPosCache = { blocks , key , byId : new Map ( ) } ;
}
const hit = _marginPosCache . byId . get ( h . id ) ;
if ( hit && hit . anchor === h . anchor ) return hit . y ;
let y = null ;
try {
const range = resolveAnchorRange ( h ) ;
const rect = range && range . getBoundingClientRect ( ) ;
if ( rect && ( rect . width || rect . height ) ) {
y = rect . top + rect . height / 2 - contentEl . getBoundingClientRect ( ) . top + contentEl . scrollTop ;
}
} catch ( e ) { }
_marginPosCache . byId . set ( h . id , { anchor : h . anchor , y } ) ;
return y ;
}
// The cards live on one layer in the book's coordinates, built when the
// highlights or the layout change; scrolling only moves the layer (a transform,
// no layout). Rebuilding and measuring the cards on every scroll frame made a
// long book with the margin open stutter.
let _marginLayerKey = null ;
2026-08-04 10:17:01 +02:00
function _repositionMarginMarkers ( ) {
const markersEl = $ ( 'reader-margin-markers' ) ;
if ( ! markersEl ) return ;
2026-10-14 18:15:58 +02:00
if ( ! readerAnnotationsMarginOpen || currentPdfDoc ) {
markersEl . innerHTML = '' ;
_marginLayerKey = null ;
return ;
}
2026-08-04 11:23:16 +02:00
if ( markersEl . querySelector ( '.margin-note-editor' ) ) return ; // don't blow away an active inline edit
2026-10-14 18:15:58 +02:00
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
2026-08-04 10:17:01 +02:00
2026-10-14 18:15:58 +02:00
const { blocks } = _epubBlocks ( contentEl ) ;
2026-08-04 10:17:01 +02:00
const areaRect = markersEl . getBoundingClientRect ( ) ;
2026-10-14 18:15:58 +02:00
const width = Math . round ( areaRect . width ) ;
const key = ` ${ width } x ${ contentEl . clientWidth } x ${ contentEl . scrollHeight } | ` +
currentHighlights . map ( h => ` ${ h . id } : ${ h . color || '' } : ${ h . note || '' } ` ) . join ( '|' ) ;
let layer = markersEl . querySelector ( '.margin-layer' ) ;
if ( ! layer || key !== _marginLayerKey || ! _marginPosCache || _marginPosCache . blocks !== blocks ) {
markersEl . innerHTML = '' ;
layer = document . createElement ( 'div' ) ;
layer . className = 'margin-layer' ;
markersEl . appendChild ( layer ) ;
_renderMarginLayer ( layer , contentEl , width ) ;
_marginLayerKey = key ;
}
// Book coordinates → this strip's.
const shift = contentEl . getBoundingClientRect ( ) . top - contentEl . scrollTop - areaRect . top ;
layer . style . transform = ` translateY( ${ Math . round ( shift ) } px) ` ;
}
// Renders each highlight/note at its text position, in book coordinates.
// Entries with note text become a readable "post-it" card (the whole point of
// the margin — you can read notes there, not just see that one exists); a
// plain highlight with no note is a small colour dot. Cards are stacked top to
// bottom so overlapping anchors don't overlap visually.
function _renderMarginLayer ( layer , contentEl , width ) {
2026-08-04 10:49:30 +02:00
const entries = [ ] ;
2026-08-04 10:17:01 +02:00
for ( const h of currentHighlights ) {
2026-10-14 18:15:58 +02:00
const y = _marginDocY ( h , contentEl ) ;
if ( y !== null ) entries . push ( { h , y } ) ;
2026-08-04 10:49:30 +02:00
}
entries . sort ( ( a , b ) => a . y - b . y ) ;
2026-10-14 18:15:58 +02:00
// All cards first, then their heights in one go (each measured right after
// adding it, the browser laid out the whole book once per card). Heights are
// kept per note text and strip width.
const cards = entries . map ( ( { h , y } ) => {
2026-08-04 10:49:30 +02:00
const hasText = ! ! h . note ;
const el = document . createElement ( 'div' ) ;
el . className = 'margin-note ' + ( hasText ? 'margin-note-text' : 'margin-note-dot' ) ;
el . dataset . highlightId = h . id ;
2026-08-04 10:17:01 +02:00
if ( h . type === 'note' ) {
2026-08-04 10:49:30 +02:00
if ( hasText ) el . classList . add ( 'margin-note-freeform' ) ;
2026-08-04 10:17:01 +02:00
} else {
2026-08-04 10:49:30 +02:00
el . dataset . color = h . color || 'yellow' ;
2026-08-04 10:17:01 +02:00
}
2026-08-04 10:49:30 +02:00
if ( hasText ) el . textContent = h . note ;
2026-10-14 18:15:58 +02:00
layer . appendChild ( el ) ;
return { el , y , hasText , key : hasText ? ` ${ width } | ${ h . note } ` : 'dot' } ;
} ) ;
if ( _marginCardHeights . size > 500 ) _marginCardHeights . clear ( ) ;
const unmeasured = cards . filter ( c => ! _marginCardHeights . has ( c . key ) ) ;
for ( const c of unmeasured ) _marginCardHeights . set ( c . key , c . el . getBoundingClientRect ( ) . height || ( c . hasText ? 20 : 12 ) ) ;
2026-08-04 10:49:30 +02:00
2026-10-14 18:15:58 +02:00
let nextTop = - Infinity ;
const GAP = 4 ;
for ( const { el , y , hasText , key } of cards ) {
const top = Math . max ( hasText ? y - 8 : y - 6 , nextTop ) ;
2026-08-04 10:49:30 +02:00
el . style . top = top + 'px' ;
2026-10-14 18:15:58 +02:00
nextTop = top + _marginCardHeights . get ( key ) + GAP ;
2026-08-04 10:17:01 +02:00
}
}
function handleMarginClick ( e ) {
2026-08-04 10:49:30 +02:00
const markerBtn = e . target . closest ( '.margin-note' ) ;
2026-08-04 10:17:01 +02:00
if ( markerBtn ) {
const h = currentHighlights . find ( x => x . id === markerBtn . dataset . highlightId ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
if ( h ) _openNoteEditor ( h ) ;
2026-08-04 10:17:01 +02:00
return ;
}
// Empty space in the markers area → place a new freeform note anchored to
// the first word of whatever line sits at this click height.
if ( ! readerAnnotationsMarginOpen || currentPdfDoc ) return ;
const markersEl = $ ( 'reader-margin-markers' ) ;
if ( ! markersEl || ! markersEl . contains ( e . target ) ) return ;
2026-08-04 11:23:16 +02:00
// If an editor is already open, this click just blurs/commits it (see its
// own blur handler) — it shouldn't also create a second, unrelated note.
if ( markersEl . querySelector ( '.margin-note-editor' ) ) return ;
2026-08-04 10:17:01 +02:00
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
const x = contentEl . getBoundingClientRect ( ) . left + 4 ;
const range = _wordRangeAtPoint ( x , e . clientY ) ;
if ( range ) createFreeformNote ( range ) ;
}
// Finds the word starting at (or just after) the given viewport point, by
// probing near the left edge of the reader content at that height — since
// text is left-aligned, that lands at (or very near) the start of the line.
function _wordRangeAtPoint ( x , y ) {
let caret = null ;
if ( document . caretRangeFromPoint ) {
caret = document . caretRangeFromPoint ( x , y ) ;
} else if ( document . caretPositionFromPoint ) {
const pos = document . caretPositionFromPoint ( x , y ) ;
if ( pos ) {
caret = document . createRange ( ) ;
caret . setStart ( pos . offsetNode , pos . offset ) ;
}
}
if ( ! caret ) return null ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl || ! contentEl . contains ( caret . startContainer ) ) return null ;
let node = caret . startContainer ;
let offset = caret . startOffset ;
if ( node . nodeType !== Node . TEXT _NODE ) {
const walker = document . createTreeWalker ( contentEl , NodeFilter . SHOW _TEXT ) ;
walker . currentNode = node ;
node = walker . nextNode ( ) ;
if ( ! node ) return null ;
offset = 0 ;
}
const text = node . textContent ;
let start = offset ;
while ( start < text . length && /\s/ . test ( text [ start ] ) ) start ++ ;
if ( start >= text . length ) return null ;
let wordStart = start ;
while ( wordStart > 0 && ! /\s/ . test ( text [ wordStart - 1 ] ) ) wordStart -- ;
let wordEnd = start ;
while ( wordEnd < text . length && ! /\s/ . test ( text [ wordEnd ] ) ) wordEnd ++ ;
if ( wordEnd <= wordStart ) return null ;
const range = document . createRange ( ) ;
range . setStart ( node , wordStart ) ;
range . setEnd ( node , wordEnd ) ;
return range ;
}
function createFreeformNote ( range ) {
const anchor = buildEpubAnchor ( range ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
// Where the note's card/dot would sit in the margin, computed from the live
// range right now (before it goes stale) — used to position the editor,
// whether or not the margin is actually open.
let top = 0 ;
const markersEl = $ ( 'reader-margin-markers' ) ;
if ( markersEl ) {
const areaRect = markersEl . getBoundingClientRect ( ) ;
const rangeRect = range . getBoundingClientRect ( ) ;
top = Math . max ( 0 , rangeRect . top + rangeRect . height / 2 - areaRect . top - 8 ) ;
}
// The highlight object itself isn't created yet — see _openNoteEditor's
// `pending` param — so a tap that's immediately cancelled or left empty
// never leaves a ghost entry in currentHighlights.
_openNoteEditor ( null , { anchor , top } ) ;
}
// Commits an edit for both note-editor UIs (inline margin + mobile bottom
// sheet): updates an existing note in place, or — only if there's actual
// text — creates and persists a not-yet-existing freeform note. An empty
// tap/cancel on a pending note must never leave a permanent ghost entry.
function _commitNoteEdit ( h , pending , text ) {
if ( h ) {
if ( h . note !== text ) {
h . note = text ;
highlightsDirty = true ;
debounceSaveHighlights ( ) ;
}
return h ;
}
if ( ! text ) return null ;
const newH = {
id : crypto . randomUUID ( ) , type : 'note' , anchor : pending . anchor ,
color : null , note : text , createdAt : new Date ( ) . toISOString ( ) ,
2026-08-04 10:17:01 +02:00
} ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
currentHighlights . push ( newH ) ;
2026-08-04 10:17:01 +02:00
highlightsDirty = true ;
debounceSaveHighlights ( ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
return newH ;
2026-08-04 11:23:16 +02:00
}
// Edit a note's text right where it lives in the margin, instead of jumping
// to the separate right-hand sidebar — the whole point of setting a note in
// the margin is that you shouldn't have to look away from it to write it.
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
// `h` is null for a not-yet-created freeform note (see createFreeformNote);
// `pending` then carries its anchor + intended margin position instead.
function editNoteInlineInMargin ( h , pending ) {
2026-08-04 11:23:16 +02:00
const markersEl = $ ( 'reader-margin-markers' ) ;
if ( ! markersEl ) return ;
markersEl . querySelector ( '.margin-note-editor' ) ? . remove ( ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
let top = pending ? . top || 0 ;
if ( h ) {
const existingCard = markersEl . querySelector ( ` [data-highlight-id=" ${ h . id } "] ` ) ;
2026-10-14 18:15:58 +02:00
// The card sits on the moving layer: where it shows in the strip right now.
top = existingCard ? existingCard . getBoundingClientRect ( ) . top - markersEl . getBoundingClientRect ( ) . top : 0 ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
if ( existingCard ) existingCard . style . display = 'none' ;
}
2026-08-04 11:23:16 +02:00
const editor = document . createElement ( 'div' ) ;
editor . className = 'margin-note-editor' ;
editor . style . top = top + 'px' ;
editor . innerHTML = '<textarea class="margin-note-textarea"></textarea>' ;
markersEl . appendChild ( editor ) ;
const textarea = editor . querySelector ( 'textarea' ) ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
textarea . value = h ? . note || '' ;
2026-08-04 11:23:16 +02:00
function commit ( ) {
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
_commitNoteEdit ( h , pending , textarea . value . trim ( ) ) ;
2026-08-04 11:23:16 +02:00
editor . remove ( ) ;
2026-10-14 18:15:58 +02:00
_marginLayerKey = null ; // the edited card was hidden: draw the layer anew
2026-08-04 11:23:16 +02:00
_repositionMarginMarkers ( ) ;
}
textarea . addEventListener ( 'blur' , commit ) ;
textarea . addEventListener ( 'keydown' , e => {
if ( e . key === 'Enter' && ! e . shiftKey ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
textarea . blur ( ) ; // triggers commit()
} else if ( e . key === 'Escape' ) {
// Stop this from bubbling to the document-level Escape handler, which
// would otherwise close the whole reader (it doesn't know we just want
// to cancel this one edit) — see the global keydown listener.
e . stopPropagation ( ) ;
textarea . removeEventListener ( 'blur' , commit ) ; // discard, don't save
editor . remove ( ) ;
2026-10-14 18:15:58 +02:00
_marginLayerKey = null ;
2026-08-04 11:23:16 +02:00
_repositionMarginMarkers ( ) ;
}
} ) ;
textarea . focus ( ) ;
textarea . select ( ) ;
2026-08-04 10:17:01 +02:00
}
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
// Mobile alternative to editNoteInlineInMargin — a full-width sheet instead
// of squeezing a textarea into the 150px margin column (see _openNoteEditor).
let currentNoteBottomSheet = null ;
function dismissNoteBottomSheet ( ) {
currentNoteBottomSheet ? . remove ( ) ;
currentNoteBottomSheet = null ;
}
function showNoteBottomSheet ( h , pending ) {
dismissNoteBottomSheet ( ) ;
const sheet = document . createElement ( 'div' ) ;
sheet . className = 'note-bottom-sheet' ;
sheet . innerHTML = `
< div class = "note-bottom-sheet-backdrop" > < / d i v >
< div class = "note-bottom-sheet-panel" >
< textarea class = "note-bottom-sheet-textarea" placeholder = "Notiz…" > < / t e x t a r e a >
< div class = "note-bottom-sheet-actions" >
< button class = "btn btn-sm" data - nbs - cancel > Abbrechen < / b u t t o n >
< button class = "btn btn-sm btn-primary" data - nbs - save > Speichern < / b u t t o n >
< / d i v >
< / d i v >
` ;
document . body . appendChild ( sheet ) ;
currentNoteBottomSheet = sheet ;
const textarea = sheet . querySelector ( '.note-bottom-sheet-textarea' ) ;
textarea . value = h ? . note || '' ;
function commit ( ) {
_commitNoteEdit ( h , pending , textarea . value . trim ( ) ) ;
dismissNoteBottomSheet ( ) ;
if ( readerAnnotationsMarginOpen ) _repositionMarginMarkers ( ) ;
}
function cancel ( ) { dismissNoteBottomSheet ( ) ; }
sheet . querySelector ( '[data-nbs-save]' ) . addEventListener ( 'click' , commit ) ;
sheet . querySelector ( '[data-nbs-cancel]' ) . addEventListener ( 'click' , cancel ) ;
sheet . querySelector ( '.note-bottom-sheet-backdrop' ) . addEventListener ( 'click' , cancel ) ;
textarea . addEventListener ( 'keydown' , e => {
if ( e . key === 'Escape' ) { e . stopPropagation ( ) ; cancel ( ) ; }
} ) ;
requestAnimationFrame ( ( ) => {
sheet . classList . add ( 'open' ) ;
textarea . focus ( ) ;
} ) ;
}
2026-08-04 18:10:33 +02:00
// Same plain-text export as exportAnnotations(), but callable straight from the
// book list (no open reader, so no live DOM/TOC to resolve percent position or
// chapter titles against — falls back to the raw chapterSrc path and orders by
// the anchor's block index, which is already a global reading-order index).
async function downloadBookAnnotationsFromList ( bookId , btnEl ) {
if ( btnEl ) btnEl . disabled = true ;
try {
const res = await fetch ( ` /books/ ${ bookId } /highlights/ ` ) ;
const { ct , iv } = await res . json ( ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! ct ) { await customAlert ( 'In diesem Buch gibt es noch keine Markierungen oder Notizen.' ) ; return ; }
2026-08-04 18:10:33 +02:00
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , iv , ct ) ;
const highlights = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
if ( ! highlights . length ) { await customAlert ( 'In diesem Buch gibt es noch keine Markierungen oder Notizen.' ) ; return ; }
2026-08-04 18:10:33 +02:00
highlights . sort ( ( a , b ) => {
const ab = a . anchor ? . startBlockIndex ? ? 0 , bb = b . anchor ? . startBlockIndex ? ? 0 ;
if ( ab !== bb ) return ab - bb ;
return ( a . anchor ? . startChar ? ? 0 ) - ( b . anchor ? . startChar ? ? 0 ) ;
} ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const bookTitle = bookMetaCache [ bookId ] ? . title || ` Buch # ${ bookId } ` ;
const lines = [ ` ${ bookTitle } — Markierungen & Notizen ` , ` Exportiert: ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , '' ] ;
2026-08-04 18:10:33 +02:00
let lastChapter = null ;
for ( const h of highlights ) {
const chapterTitle = h . anchor ? . chapterSrc || '' ;
if ( chapterTitle !== lastChapter ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
lines . push ( ` ── ${ chapterTitle || 'Abschnitt ohne Titel' } ── ` ) ;
2026-08-04 18:10:33 +02:00
lastChapter = chapterTitle ;
}
if ( h . type === 'note' ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
lines . push ( ` [Notiz] bei: „ ${ ( h . anchor ? . quote || '' ) . trim ( ) } “ ` ) ;
2026-08-04 18:10:33 +02:00
if ( h . note ) lines . push ( ` ${ h . note } ` ) ;
} else {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
lines . push ( ` [Markierung · ${ _hlColorName ( h . color || 'yellow' ) } ] „ ${ ( h . anchor ? . quote || '' ) . trim ( ) } “ ` ) ;
if ( h . note ) lines . push ( ` Notiz: ${ h . note } ` ) ;
2026-08-04 18:10:33 +02:00
}
lines . push ( '' ) ;
}
const blob = new Blob ( [ lines . join ( '\n' ) ] , { type : 'text/plain;charset=utf-8' } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
a . download = ` ${ bookTitle . replace ( /[^\w\-]+/g , '_' ) } -notizen.txt ` ;
2026-08-04 18:10:33 +02:00
document . body . appendChild ( a ) ;
a . click ( ) ;
a . remove ( ) ;
URL . revokeObjectURL ( url ) ;
} catch ( e ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
await customAlert ( 'Export fehlgeschlagen: ' + e . message ) ;
2026-08-04 18:10:33 +02:00
} finally {
if ( btnEl ) btnEl . disabled = false ;
}
}
2026-08-04 10:17:01 +02:00
// ---------------------------------------------------------------------------
// Export annotations — walks all highlights/notes of the current book in
// reading order and offers them as a downloadable plain-text file.
// ---------------------------------------------------------------------------
function _tocTitleForChapterSrc ( chapterSrc ) {
if ( ! chapterSrc ) return '' ;
const entry = currentBookToc . find ( t => ( t . href || '' ) . split ( '#' ) [ 0 ] === chapterSrc ) ;
return entry ? entry . label : '' ;
}
2026-08-04 11:10:56 +02:00
// Rough "how far through the book" position for a resolved range — there's no
// real page concept for reflowable EPUB text, so this is a coordinate of last
// resort: percent of total scrollable content height, good enough to roughly
// relocate a highlight/note later.
function _percentFromRange ( range ) {
const contentEl = $ ( 'reader-content' ) ;
2026-09-27 19:57:10 +02:00
if ( ! contentEl || ! range ) return null ;
// Share of the text before the range, like the reading progress.
const block = _blockAncestor ( range . startContainer ) ;
if ( block && contentEl . contains ( block ) ) {
const blocks = Array . from ( contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ) ;
const i = blocks . indexOf ( block ) ;
if ( i >= 0 ) {
const idx = _bookCharIndex ( contentEl ) ;
const at = idx . starts [ i ] + _charOffsetInBlock ( block , range . startContainer , range . startOffset ) ;
return Math . max ( 0 , Math . min ( 100 , ( at / idx . total ) * 100 ) ) ;
}
}
if ( ! contentEl . scrollHeight ) return null ;
2026-08-04 11:10:56 +02:00
const contentRect = contentEl . getBoundingClientRect ( ) ;
const rangeRect = range . getBoundingClientRect ( ) ;
if ( ! rangeRect . width && ! rangeRect . height ) return null ;
const offsetWithinScroll = ( rangeRect . top - contentRect . top ) + contentEl . scrollTop ;
return Math . max ( 0 , Math . min ( 100 , ( offsetWithinScroll / contentEl . scrollHeight ) * 100 ) ) ;
}
2026-08-04 10:17:01 +02:00
2026-08-04 11:10:56 +02:00
function _sortedAnnotations ( ) {
const resolved = currentHighlights . map ( h => {
2026-08-04 10:17:01 +02:00
let range ;
try { range = resolveAnchorRange ( h ) ; } catch ( e ) { range = null ; }
2026-08-04 11:10:56 +02:00
return { h , range } ;
} ) ;
2026-08-04 10:17:01 +02:00
resolved . sort ( ( a , b ) => {
if ( ! a . range || ! b . range ) return 0 ;
2026-08-04 11:10:56 +02:00
return a . range . compareBoundaryPoints ( Range . START _TO _START , b . range ) ;
2026-08-04 10:17:01 +02:00
} ) ;
2026-08-04 11:10:56 +02:00
return resolved ;
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
// Highlight colours as the export and the list name them.
function _hlColorName ( color ) {
return ( { yellow : 'gelb' , green : 'grün' , blue : 'blau' , red : 'rot' } ) [ color ] || color ;
}
2026-08-04 11:10:56 +02:00
function exportAnnotations ( ) {
if ( ! currentHighlights . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
customAlert ( 'In diesem Buch gibt es noch keine Markierungen oder Notizen.' ) ;
2026-08-04 11:10:56 +02:00
return ;
}
const resolved = _sortedAnnotations ( ) ;
2026-08-04 10:17:01 +02:00
const titleEl = $ ( 'reader-title' ) ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const bookTitle = ( titleEl ? . textContent || 'Buch' ) . split ( ' — ' ) [ 0 ] ;
const lines = [ ` ${ bookTitle } — Markierungen & Notizen ` , ` Exportiert: ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , '' ] ;
2026-08-04 10:17:01 +02:00
let lastChapter = null ;
2026-08-04 11:10:56 +02:00
for ( const { h , range } of resolved ) {
2026-08-04 10:17:01 +02:00
const chapterTitle = _tocTitleForChapterSrc ( h . anchor ? . chapterSrc ) || h . anchor ? . chapterSrc || '' ;
if ( chapterTitle !== lastChapter ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
lines . push ( ` ── ${ chapterTitle || 'Abschnitt ohne Titel' } ── ` ) ;
2026-08-04 10:17:01 +02:00
lastChapter = chapterTitle ;
}
2026-08-04 11:10:56 +02:00
const pct = _percentFromRange ( range ) ;
2026-08-04 13:51:05 +02:00
const coord = pct !== null ? ` (≈ ${ _formatBookPercent ( pct / 100 ) } % im Buch) ` : '' ;
2026-08-04 10:17:01 +02:00
if ( h . type === 'note' ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
lines . push ( ` [Notiz] ${ coord } bei: „ ${ ( h . anchor ? . quote || '' ) . trim ( ) } “ ` ) ;
2026-08-04 10:17:01 +02:00
if ( h . note ) lines . push ( ` ${ h . note } ` ) ;
} else {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
lines . push ( ` [Markierung · ${ _hlColorName ( h . color || 'yellow' ) } ] ${ coord } „ ${ ( h . anchor ? . quote || '' ) . trim ( ) } “ ` ) ;
if ( h . note ) lines . push ( ` Notiz: ${ h . note } ` ) ;
2026-08-04 10:17:01 +02:00
}
lines . push ( '' ) ;
}
const blob = new Blob ( [ lines . join ( '\n' ) ] , { type : 'text/plain;charset=utf-8' } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
a . download = ` ${ bookTitle . replace ( /[^\w\-]+/g , '_' ) } -notizen.txt ` ;
2026-08-04 10:17:01 +02:00
document . body . appendChild ( a ) ;
a . click ( ) ;
a . remove ( ) ;
URL . revokeObjectURL ( url ) ;
}
2026-08-04 11:10:56 +02:00
// List of every highlight/note in the book, in reading order, each jumpable —
// the same pattern as the bookmarks sidebar, but for annotations. Unlike the
// margin panel (which only shows what's currently on-screen), this is a full
// overview, useful once a book has annotations scattered throughout.
function openAnnotationsSidebar ( ) {
if ( ! currentHighlights . length ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
openSidebar ( 'Markierungen & Notizen' , '<p class="muted">Noch keine Markierungen oder Notizen.</p>' ) ;
2026-08-04 11:10:56 +02:00
return ;
}
const resolved = _sortedAnnotations ( ) ;
let html = '<ul style="list-style:none;padding:0;">' ;
for ( const { h , range } of resolved ) {
const pct = _percentFromRange ( range ) ;
2026-08-04 13:51:05 +02:00
const pctLabel = pct !== null ? ` ≈ ${ _formatBookPercent ( pct / 100 ) } % ` : '' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const kind = h . type === 'note' ? 'Notiz' : ` Markierung ${ h . color ? ' · ' + _hlColorName ( h . color ) : '' } ` ;
const preview = h . note || h . anchor ? . quote || '(kein Text)' ;
2026-08-04 11:10:56 +02:00
html += ` <li class="bookmark-entry">
< button class = "btn-link" data - jump - annotation = "${escapeHtml(h.id)}" style = "flex:1;text-align:left;" >
< span class = "muted" style = "font-size:11px;" > $ { escapeHtml ( kind ) } $ { pctLabel } < / s p a n > < b r >
$ { escapeHtml ( preview . slice ( 0 , 80 ) ) }
< / b u t t o n >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn-icon" data - delete - annotation = "${escapeHtml(h.id)}" title = "Löschen" > ✕ < / b u t t o n >
2026-08-04 11:10:56 +02:00
< / l i > ` ;
}
html += '</ul>' ;
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
openSidebar ( 'Markierungen & Notizen' , html ) ;
2026-08-04 11:10:56 +02:00
const body = $ ( 'sidebar-body' ) ;
body . addEventListener ( 'click' , function _annClick ( e ) {
const jumpBtn = e . target . closest ( '[data-jump-annotation]' ) ;
const delBtn = e . target . closest ( '[data-delete-annotation]' ) ;
if ( jumpBtn ) {
body . removeEventListener ( 'click' , _annClick ) ;
jumpToAnnotation ( jumpBtn . dataset . jumpAnnotation ) ;
}
if ( delBtn ) {
deleteHighlight ( delBtn . dataset . deleteAnnotation ) ;
openAnnotationsSidebar ( ) ; // re-render
}
} ) ;
}
function jumpToAnnotation ( id ) {
const h = currentHighlights . find ( x => x . id === id ) ;
if ( ! h ) return ;
closeSidebar ( ) ;
_suppressScrollJumpDetect ( ) ;
setTimeout ( ( ) => {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
let range ;
try { range = resolveAnchorRange ( h ) ; } catch ( e ) { range = null ; }
if ( range ) {
const top = range . getBoundingClientRect ( ) . top - contentEl . getBoundingClientRect ( ) . top ;
contentEl . scrollBy ( { top : top - 16 , behavior : 'smooth' } ) ;
}
} , 50 ) ;
}
2026-05-29 21:04:47 +02:00
// ---------------------------------------------------------------------------
// Footnote popover
// ---------------------------------------------------------------------------
let currentFootnotePopover = null ;
let _footnotePopoverTimer = null ;
function showFootnotePopover ( link ) {
dismissFootnotePopover ( ) ;
const targetEl = document . getElementById ( link . dataset . footnoteRef ) ;
if ( ! targetEl ) return ;
2026-10-08 18:51:05 +02:00
// The note's paragraph, also when the id sits on an inline element in it
// (<p><span><a id="F400" …>143</a></span> Note…</p>).
const inline = /^(A|SPAN|SUP|SUB|B|I|EM|STRONG|SMALL)$/ . test ( targetEl . tagName ) ;
const container = targetEl . closest ( '.footnote' ) ||
( inline && targetEl . closest ( 'p, li, dd, blockquote' ) ) || targetEl . parentElement ;
2026-05-29 21:04:47 +02:00
if ( ! container ) return ;
const clone = container . cloneNode ( true ) ;
clone . querySelector ( 'sup' ) ? . remove ( ) ;
2026-10-08 18:51:05 +02:00
clone . querySelectorAll ( '.epub-pagenum' ) . forEach ( e => e . remove ( ) ) ;
// The note's own number at its start, a link back to the text ("1 Note…").
const first = Array . from ( clone . querySelectorAll ( 'a' ) ) . find ( a => a . textContent . trim ( ) ) ;
const num = first ? first . textContent . trim ( ) : '' ;
if ( /^[\[(]?\d{1,4}[\])]?\.?$/ . test ( num ) && clone . textContent . trim ( ) . startsWith ( num ) ) first . remove ( ) ;
// Only the number: the note (a poem, a long quote) runs on in the next
// paragraphs, up to the next note's anchor.
if ( ! clone . textContent . trim ( ) ) {
let len = 0 ;
for ( let next = container . nextElementSibling ; next && len < 6000 ; next = next . nextElementSibling ) {
if ( /^H[1-6]$/ . test ( next . tagName ) || next . querySelector ( '[id]' ) ) break ;
if ( ! next . textContent . trim ( ) ) continue ;
clone . appendChild ( next . cloneNode ( true ) ) ;
len += next . textContent . length ;
}
}
2026-05-29 21:04:47 +02:00
const popover = document . createElement ( 'div' ) ;
popover . className = 'footnote-popover' ;
2026-05-29 21:21:43 +02:00
const _overlay = document . getElementById ( 'reader-overlay' ) ;
[ 'reader-theme-sepia' , 'reader-theme-bright' ] . forEach ( cls => {
if ( _overlay ? . classList . contains ( cls ) ) popover . classList . add ( cls ) ;
} ) ;
2026-05-29 21:04:47 +02:00
popover . innerHTML = clone . innerHTML ;
const closeBtn = document . createElement ( 'button' ) ;
closeBtn . className = 'footnote-popover-close' ;
closeBtn . textContent = '✕' ;
closeBtn . addEventListener ( 'click' , dismissFootnotePopover ) ;
popover . appendChild ( closeBtn ) ;
const rect = link . getBoundingClientRect ( ) ;
popover . style . top = ( rect . bottom + 8 ) + 'px' ;
popover . style . left = Math . max ( 8 , rect . left - 8 ) + 'px' ;
popover . addEventListener ( 'mouseenter' , ( ) => clearTimeout ( _footnotePopoverTimer ) ) ;
popover . addEventListener ( 'mouseleave' , ( ) => {
_footnotePopoverTimer = setTimeout ( dismissFootnotePopover , 250 ) ;
} ) ;
document . body . appendChild ( popover ) ;
currentFootnotePopover = popover ;
currentFootnotePopover . _refLink = link ;
const pr = popover . getBoundingClientRect ( ) ;
if ( pr . right > window . innerWidth - 8 ) popover . style . left = ( window . innerWidth - pr . width - 8 ) + 'px' ;
if ( pr . bottom > window . innerHeight - 8 ) popover . style . top = ( rect . top - pr . height - 8 ) + 'px' ;
}
function dismissFootnotePopover ( ) {
clearTimeout ( _footnotePopoverTimer ) ;
currentFootnotePopover ? . remove ( ) ;
currentFootnotePopover = null ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// ---------------------------------------------------------------------------
2026-04-05 14:22:07 +02:00
// Radio sidebar (compact player)
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// ---------------------------------------------------------------------------
2026-04-05 14:22:07 +02:00
function openRadioSidebar ( ) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
const stationName = currentStation ? escapeHtml ( currentStation . name ) : '— kein Sender —' ;
2026-04-05 14:22:07 +02:00
const track = currentTrack ? escapeHtml ( currentTrack ) : '' ;
const vol = document . getElementById ( 'volume' ) ? . value ? ? 204 ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
2026-04-05 14:22:07 +02:00
const rows = [ ... document . querySelectorAll ( '#saved-tbody tr[data-id]' ) ] ;
const stationsHtml = rows . length
2026-04-05 17:32:05 +02:00
? rows . map ( r => ` <li><button class="btn btn-sm rsb-play-btn" data-url=" ${ escapeHtml ( r . dataset . url || '' ) } " data-name=" ${ escapeHtml ( r . dataset . name || '' ) } "> ${ escapeHtml ( r . dataset . name || '' ) } </button></li> ` ) . join ( '' )
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
: ` <li class="muted">Keine gespeicherten Sender.</li> ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const html = `
2026-04-05 14:22:07 +02:00
< div class = "rsb-nowplaying" >
< div class = "rsb-station-name" > $ { stationName } < / d i v >
$ { track ? ` <div class="rsb-track muted"> ${ track } </div> ` : '' }
< / d i v >
< div class = "rsb-controls" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button class = "btn ${isPlaying ? 'playing' : ''}" id = "rsb-playstop" > $ { isPlaying ? '⏹ Stopp' : '▶ Abspielen' } < / b u t t o n >
< label class = "rsb-vol" > Lautst . < input type = "range" id = "rsb-volume" min = "0" max = "255" value = "${vol}" > < / l a b e l >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
< / d i v >
2026-04-05 14:22:07 +02:00
< ul class = "rsb-station-list" > $ { stationsHtml } < / u l >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
` ;
2026-04-05 14:22:07 +02:00
openSidebar ( 'Radio' , html ) ;
2026-04-05 17:32:05 +02:00
const body = $ ( 'sidebar-body' ) ;
body . querySelector ( '#rsb-playstop' ) . addEventListener ( 'click' , togglePlayStop ) ;
body . querySelector ( '#rsb-volume' ) . addEventListener ( 'input' , function ( ) {
const v = this . value ;
const mainSlider = document . getElementById ( 'volume' ) ;
const mainNum = document . getElementById ( 'volume-num' ) ;
if ( mainSlider ) mainSlider . value = v ;
if ( mainNum ) mainNum . value = v ;
audio . volume = v / 255 ;
} ) ;
body . querySelectorAll ( '.rsb-play-btn' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( ) => playStation ( btn . dataset . url , btn . dataset . name , null ) ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
( function init ( ) {
// Migrate PBKDF2-derived key stored by login/register form
2026-03-19 20:59:12 +01:00
if ( window . USER _ID ) {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
const pending = localStorage . getItem ( 'diora_pending_enc_key' ) ;
if ( pending ) {
2026-03-19 20:59:12 +01:00
localStorage . setItem ( ` diora_enc_key_ ${ window . USER _ID } ` , pending ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
localStorage . removeItem ( 'diora_pending_enc_key' ) ;
}
}
// Populate saved stations from server-side context if available
if ( typeof INITIAL _SAVED !== 'undefined' && Array . isArray ( INITIAL _SAVED ) ) {
// The server already renders saved stations in the template; nothing extra needed.
// But if JS-rendered saved tab were needed we'd call addSavedRow here.
}
// Restore persisted volume, fall back to slider default
const volSlider = $ ( 'volume' ) ;
if ( volSlider ) {
const saved = localStorage . getItem ( 'diora_volume' ) ;
const vol = saved !== null ? parseInt ( saved , 10 ) : parseInt ( volSlider . value , 10 ) ;
setVolume ( vol ) ;
}
2026-09-01 07:04:27 +02:00
// Restore persisted read-aloud language
const ttsLangSelect = $ ( 'reader-tts-lang' ) ;
if ( ttsLangSelect ) ttsLangSelect . value = ttsLang ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Load recommendations on page load
loadRecommendations ( ) ;
// Initialise focus timer display
renderTimer ( ) ;
// Initialise mood/genre chips
initMoodChips ( ) ;
// Initialise curated station lists
initCuratedLists ( ) ;
// Show curated lists again when search input is cleared
const searchInput = document . getElementById ( 'search-input' ) ;
if ( searchInput ) {
searchInput . addEventListener ( 'input' , function ( ) {
if ( this . value === '' ) {
const curated = document . getElementById ( 'curated-lists' ) ;
if ( curated ) curated . style . display = '' ;
}
} ) ;
}
// Load focus session stats
loadFocusStats ( ) ;
// Apply encrypted wallpaper (if set)
applyEncryptedBackground ( ) ;
// Init book drop zone
initBookDropZone ( ) ;
2026-08-04 18:10:33 +02:00
const showReadToggle = $ ( 'book-show-read-toggle' ) ;
if ( showReadToggle ) showReadToggle . checked = _bookShowRead ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
2026-08-04 09:01:52 +02:00
// Restore active tab: a URL hash (shared/bookmarked link, or browser
// back/forward) wins over the last-used tab remembered in localStorage —
// except when offline, where books (usable offline) always takes over.
const offlineOverride = ! navigator . onLine && IS _AUTHENTICATED ;
const hashTab = ( location . hash || '' ) . replace ( /^#/ , '' ) . split ( '/' ) [ 0 ] ;
if ( ! offlineOverride && TOP _TABS . includes ( hashTab ) ) {
_routeFromHash ( ) ;
} else {
const savedTab = localStorage . getItem ( 'diora_active_tab' ) || 'radio' ;
const savedRadioTab = localStorage . getItem ( 'diora_active_radio_tab' ) || 'saved' ;
showTab ( offlineOverride ? 'books' : savedTab ) ;
showRadioTab ( savedRadioTab ) ;
}
Add podcast enhancements: AntennaPod parity features + inbox management
- Auto-play next episode from queue when current episode ends
- Sleep timer (N minutes or end-of-episode) with countdown in button
- In-feed episode filter (client-side search)
- Auto-queue new episodes per feed (⚡Q toggle, inserts at top of queue)
- More playback speeds: 1¾× and 2½× added
- Progress bars + structured meta line in all episode list views (feed, inbox, queue)
- Queue drag-and-drop reorder
- Feed list search filter and sort options (A–Z, Z–A, recently added/refreshed)
- DB migration: PodcastFeed.auto_queue, EpisodeProgress.dismissed
- Inbox: dismiss episodes without marking played, checkboxes for multi-select,
bulk actions (add to queue, mark played, download, dismiss), load-more pagination
- Refresh button in single feed view header
- Hourly background refresh of all subscribed feeds
- Full Media Session API for radio and podcast: Windows taskbar thumbnail buttons
(play/pause/stop/next/seek) now work correctly for both modes
- Playing an episode auto-adds it to the queue if not already there
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:55:11 +01:00
2026-05-30 00:12:22 +02:00
// React to connectivity changes
window . addEventListener ( 'offline' , ( ) => {
showOfflineBanner ( true ) ;
if ( IS _AUTHENTICATED ) showTab ( 'books' ) ;
} ) ;
2026-05-30 07:22:32 +02:00
window . addEventListener ( 'online' , async ( ) => {
2026-05-30 00:12:22 +02:00
showOfflineBanner ( false ) ;
2026-05-31 15:29:38 +02:00
await _syncPendingAnnotations ( ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
loadBookList ( ) ; // sends the positions read offline first (the progress outbox)
2026-05-30 00:12:22 +02:00
} ) ;
// Show banner immediately if already offline at startup
if ( ! navigator . onLine ) showOfflineBanner ( true ) ;
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00
// Reading position: positions reached by the reader's own doing count (see _noteReadingPosition).
{
const content = $ ( 'reader-content' ) ;
if ( content ) {
for ( const type of [ 'wheel' , 'touchstart' , 'mousedown' ] ) {
content . addEventListener ( type , _noteReaderInteraction , { passive : true , capture : true } ) ;
}
}
document . addEventListener ( 'keydown' , ( ) => { if ( currentBookId ) _noteReaderInteraction ( ) ; } , true ) ;
if ( IS _AUTHENTICATED ) _flushProgressOutbox ( ) ;
}
2026-05-29 21:04:47 +02:00
// Footnote hover (desktop) + tap (mobile) via event delegation
const _readerContentEl = $ ( 'reader-content' ) ;
if ( _readerContentEl ) {
_readerContentEl . addEventListener ( 'mouseover' , e => {
const link = e . target . closest ( '[data-footnote-ref]' ) ;
if ( ! link ) return ;
clearTimeout ( _footnotePopoverTimer ) ;
_footnotePopoverTimer = setTimeout ( ( ) => showFootnotePopover ( link ) , 120 ) ;
} ) ;
_readerContentEl . addEventListener ( 'mouseout' , e => {
if ( ! e . target . closest ( '[data-footnote-ref]' ) ) return ;
_footnotePopoverTimer = setTimeout ( dismissFootnotePopover , 250 ) ;
} ) ;
_readerContentEl . addEventListener ( 'click' , e => {
const link = e . target . closest ( '[data-footnote-ref]' ) ;
if ( ! link ) return ;
e . preventDefault ( ) ;
if ( currentFootnotePopover ? . _refLink === link ) {
dismissFootnotePopover ( ) ;
} else {
showFootnotePopover ( link ) ;
}
} ) ;
2026-08-04 10:17:01 +02:00
// Text selection → highlight color/note popover
_readerContentEl . addEventListener ( 'mouseup' , handleReaderSelection ) ;
_readerContentEl . addEventListener ( 'touchend' , e => {
const target = e . target ;
Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px-
Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen
Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich
ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop
bleibt bei der bisherigen Anordnung in der Notes-Leiste).
Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl)
jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr
den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der
Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle
Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem
Handy-Keyboard unangenehm zu bedienen ist.
Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt
und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass
ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights
hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende
Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape-
Cancel auf Desktop.
Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren;
dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit
in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine
Desktop-Aufgabe.
Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression):
Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern,
kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin
funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor,
Escape-Cancel, Blur-Commit) unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:13:55 +02:00
const touch = e . changedTouches && e . changedTouches [ 0 ] ;
const clientX = touch ? touch . clientX : undefined ;
const clientY = touch ? touch . clientY : undefined ;
setTimeout ( ( ) => handleReaderSelection ( { target , clientX , clientY } ) , 50 ) ;
2026-08-04 10:17:01 +02:00
} ) ;
}
// Margin ("page margin") panel — click a marker to view/edit, click empty
// space to anchor a new freeform note at that line's height
const _readerMarginEl = $ ( 'reader-margin' ) ;
if ( _readerMarginEl ) {
_readerMarginEl . addEventListener ( 'click' , handleMarginClick ) ;
2026-05-29 21:04:47 +02:00
}
2026-03-16 19:19:22 +01:00
} ) ( ) ;