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)
if ( _lastProgressBeacon ) navigator . sendBeacon ( _lastProgressBeacon . url , _lastProgressBeacon . body ) ;
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 ( ) ;
}
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 ) ;
}
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 } ') ` ;
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 ;
}
}
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 ;
toast . querySelector ( '.reader-toast-btn' ) . addEventListener ( 'click' , ( ) => {
const contentEl = $ ( 'reader-content' ) ;
if ( contentEl ) {
_suppressScrollJumpDetect ( ) ;
contentEl . scrollTo ( { top : backTop , behavior : 'smooth' } ) ;
}
_dismissScrollJumpToast ( ) ;
} ) ;
setTimeout ( _dismissScrollJumpToast , 6000 ) ;
}
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 ) ;
}
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 [ ] ; }
}
2026-07-10 20:52:27 +02:00
async function _updateCachedBookProgress ( bookId , fraction , anchor , force = false ) {
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 ;
2026-07-10 20:52:27 +02:00
// Only advance (or on force), so a transiently wrong save can't lose progress.
if ( entry && ( force || _cmpProgress ( anchor , fraction , entry . position _anchor , entry . scroll _fraction ) >= 0 ) ) {
2026-05-29 21:14:40 +02:00
entry . scroll _fraction = fraction ;
entry . position _anchor = anchor ;
entry . last _read = new Date ( ) . toISOString ( ) ;
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>' ;
2026-03-20 09:45:39 +01:00
const res = await fetch ( '/books/' , { cache : 'no-store' } ) ;
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 } ;
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 || '' , 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 : '' } ;
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 || '' , uploaded _at : b . uploaded _at , last _read : b . last _read || null , keyOk : false , 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
}
}
2026-05-31 15:41:35 +02:00
// If local cache is further ahead than the server, push it and use it for display.
// Server uses max-semantics so this is always safe.
const _localMeta = await _loadCachedBookMeta ( ) ;
const _localById = Object . fromEntries ( _localMeta . map ( b => [ b . id , b ] ) ) ;
for ( const b of decrypted ) {
const loc = _localById [ b . id ] ;
2026-07-10 20:52:27 +02:00
if ( loc && _cmpProgress ( loc . position _anchor , loc . scroll _fraction , b . position _anchor , b . scroll _fraction ) > 0 ) {
2026-05-31 15:41:35 +02:00
fetch ( ` /books/ ${ b . id } /progress/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { scroll _fraction : loc . scroll _fraction , position _anchor : loc . position _anchor || '' } ) ,
} ) . catch ( ( ) => { } ) ;
b . scroll _fraction = loc . scroll _fraction ;
b . position _anchor = loc . position _anchor ;
b . last _read = loc . last _read ;
2026-05-30 07:22:32 +02:00
}
}
2026-03-20 20:41:13 +01:00
decrypted . sort ( ( a , b ) => {
if ( a . last _read && b . last _read ) return b . last _read . localeCompare ( a . last _read ) ;
if ( a . last _read ) return - 1 ;
if ( b . last _read ) return 1 ;
return 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
const cachedBooks = await _loadCachedBookMeta ( ) ;
if ( cachedBooks . length ) {
cachedBooks . sort ( ( a , b ) => {
if ( a . last _read && b . last _read ) return b . last _read . localeCompare ( a . last _read ) ;
if ( a . last _read ) return - 1 ;
if ( b . last _read ) return 1 ;
return ( 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' ;
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 ) {
_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 ) ;
} ) ;
}
// Walks blocks/sentences from startBlockIndex onward, fetching one sentence
// ahead while the current one plays so there's no gap between them.
async function _ttsPlayLoop ( blocks , startBlockIndex , runToken ) {
function * sentenceStream ( ) {
for ( let bi = startBlockIndex ; bi < blocks . length ; bi ++ ) {
const block = blocks [ bi ] ;
const text = ( block . textContent || '' ) . trim ( ) ;
if ( ! text ) continue ;
for ( const sentence of _ttsSplitSentences ( text ) ) yield { block , sentence } ;
}
}
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 ;
if ( block !== lastBlock ) { lastBlock = block ; blockSearchOffset = 0 ; }
blockSearchOffset = _ttsHighlightSentence ( block , sentence , blockSearchOffset ) ;
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 ( ) ;
}
function _ttsUpdateBar ( ) {
if ( ! ttsBarEl ) return ;
const btn = ttsBarEl . querySelector ( '.tts-pause-btn' ) ;
if ( btn ) btn . textContent = ttsPaused ? '▶' : '⏸' ;
}
function _ttsShowBar ( ) {
_ttsRemoveBar ( ) ;
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 ;
const btn = $ ( 'reader-tts-btn' ) ;
if ( btn ) { btn . classList . add ( 'active' ) ; btn . title = 'Vorlesen beenden' ; }
_ttsShowBar ( ) ;
_ttsPlayLoop ( blocks , startIndex , runToken ) ;
}
function stopReadAloud ( ) {
ttsRunToken ++ ;
ttsActive = false ;
ttsPaused = false ;
if ( ttsAudio ) {
try { ttsAudio . pause ( ) ; ttsAudio . src = '' ; } catch ( e ) { }
ttsAudio = null ;
}
_ttsClearHighlight ( ) ;
_ttsRemoveBar ( ) ;
const btn = $ ( 'reader-tts-btn' ) ;
if ( btn ) { btn . classList . remove ( 'active' ) ; btn . title = 'Vorlesen' ; }
}
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 ;
let _repairProgressAnchor = false ; // stored anchor didn't fit 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
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 ) {
_repairProgressAnchor = 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 ( ) ;
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 = '' ;
try {
const progressRes = await fetch ( '/books/' ) ;
const allBooks = await progressRes . json ( ) ;
const bookData = allBooks . find ( b => b . id === bookId ) ;
fraction = bookData ? ( bookData . scroll _fraction || 0 ) : 0 ;
anchor = bookData ? ( bookData . position _anchor || '' ) : '' ;
} catch {
// offline — use cached meta
const cached = await _loadCachedBookMeta ( ) ;
const bookData = cached . find ( b => b . id === bookId ) ;
fraction = bookData ? ( bookData . scroll _fraction || 0 ) : 0 ;
anchor = bookData ? ( bookData . position _anchor || '' ) : '' ;
}
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 ) ) ;
}
// An anchor past the end of this book can only come from a broken
// render. The server keeps the furthest block index, so it would block
// every real position from now on: overwrite it with the next save.
const [ storedBlock ] = _anchorParts ( anchor ) ;
if ( storedBlock >= contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) . length ) {
_repairProgressAnchor = 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
}
}
} catch ( e ) { }
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 ) ;
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 ) { }
}
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 ;
}
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
2026-09-28 21:19:05 +02:00
_updateCachedBookProgress ( currentBookId , fraction , anchor , force || ( _repairProgressAnchor && ! ! anchor ) ) ;
2026-05-29 21:14:40 +02:00
2026-09-28 21:19:05 +02:00
const repair = _repairProgressAnchor && ! ! anchor ;
if ( repair ) _repairProgressAnchor = false ;
const body = JSON . stringify ( { scroll _fraction : fraction , position _anchor : anchor , ... ( ( force || repair ) && { force : true } ) } ) ;
2026-05-31 15:41:35 +02:00
_lastProgressBeacon = { url : ` /books/ ${ currentBookId } /progress/ ` , body } ;
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 {
await fetch ( ` /books/ ${ currentBookId } /progress/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
2026-05-31 15:41:35 +02:00
body ,
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-31 15:41:35 +02:00
if ( force ) {
const toast = document . createElement ( 'div' ) ;
toast . className = 'reader-toast' ;
toast . textContent = 'Position gesetzt' ;
document . body . appendChild ( toast ) ;
setTimeout ( ( ) => toast . remove ( ) , 1800 ) ;
}
} catch ( e ) { }
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 ( ) ;
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-02 19:32:32 +02:00
Object . assign ( readerSettings , { theme : 'dark' , fontFamily : 'serif' , noBold : false ,
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 ] ;
}
}
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-02 19:32:32 +02:00
localStorage . setItem ( ` diora_reader_settings_ ${ currentBookId } ` , JSON . stringify ( { ... readerSettings , v : 2 } ) ) ;
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' ) ;
if ( readerSettings . theme === 'sepia' ) overlay . classList . add ( 'reader-theme-sepia' ) ;
else if ( readerSettings . theme === 'bright' ) overlay . classList . add ( 'reader-theme-bright' ) ;
// 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 >
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-08-04 13:28:19 +02:00
const blocks = Array . from ( contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ) ;
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-08-04 10:17:01 +02:00
function _repositionMarginMarkers ( ) {
const markersEl = $ ( 'reader-margin-markers' ) ;
if ( ! markersEl ) 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-08-04 10:17:01 +02:00
markersEl . innerHTML = '' ;
if ( ! readerAnnotationsMarginOpen || currentPdfDoc ) return ;
const areaRect = markersEl . getBoundingClientRect ( ) ;
2026-08-04 14:09:02 +02:00
// A generous pre-filter band: a note card can be considerably taller than
// its own anchor point, so a card whose anchor is somewhat outside the
// strictly-visible area can still be partially (or fully) on-screen once
// rendered. A tight band here excluded it before it ever got a chance to
// be measured — the exact bug reported ("tall notes stay hidden until
// scrolled much further than expected").
const band = Math . max ( areaRect . height , 400 ) ;
2026-08-04 10:49:30 +02:00
const entries = [ ] ;
2026-08-04 10:17:01 +02:00
for ( const h of currentHighlights ) {
let range ;
try { range = resolveAnchorRange ( h ) ; } catch ( e ) { continue ; }
if ( ! range ) continue ;
const rect = range . getBoundingClientRect ( ) ;
if ( ! rect . width && ! rect . height ) continue ; // detached/invalid
2026-08-04 10:49:30 +02:00
const y = rect . top + rect . height / 2 - areaRect . top ;
2026-08-04 14:09:02 +02:00
if ( y < - band || y > areaRect . height + band ) continue ;
2026-08-04 10:49:30 +02:00
entries . push ( { h , y } ) ;
}
entries . sort ( ( a , b ) => a . y - b . y ) ;
let nextTop = - Infinity ;
const GAP = 4 ;
2026-08-04 14:09:02 +02:00
const rendered = [ ] ;
2026-08-04 10:49:30 +02:00
for ( const { h , y } of entries ) {
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 ;
const desiredTop = hasText ? y - 8 : y - 6 ;
const top = Math . max ( desiredTop , nextTop ) ;
el . style . top = top + 'px' ;
markersEl . appendChild ( el ) ;
const measured = el . getBoundingClientRect ( ) . height || ( hasText ? 20 : 12 ) ;
nextTop = top + measured + GAP ;
2026-08-04 14:09:02 +02:00
rendered . push ( { el , top , measured } ) ;
}
// Now that actual card heights are known, do the real cull: drop anything
// that ended up entirely outside the visible band after stacking (e.g.
// pushed down by a very tall card above it).
for ( const { el , top , measured } of rendered ) {
if ( top + measured < - 40 || top > areaRect . height + 40 ) el . remove ( ) ;
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 } "] ` ) ;
top = existingCard ? ( parseFloat ( existingCard . style . top ) || 0 ) : 0 ;
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 ( ) ;
_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 ( ) ;
_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 ( ) ;
2026-05-31 15:41:35 +02:00
loadBookList ( ) ; // loadBookList pushes any locally-advanced progress automatically
2026-05-30 00:12:22 +02:00
} ) ;
// Show banner immediately if already offline at startup
if ( ! navigator . onLine ) showOfflineBanner ( true ) ;
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
} ) ( ) ;