2026-03-16 19:19:22 +01:00
/ * *
* diora — radio player
* Handles playback , SSE metadata , search , station management , and affiliate links .
* /
'use strict' ;
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
let currentStation = null ; // { url, name, id } | null
let currentTrack = '' ;
let sseSource = null ;
let isPlaying = false ;
let currentPlayId = 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
// Podcast state
let podcastMode = false ;
let currentEpisode = null ; // {id, title, audioUrl, durationSeconds, feedId}
let seekSaveTimer = null ;
let podcastFeeds = [ ] ;
let podcastQueue = [ ] ;
let podcastCurrentView = 'feeds' ;
let podcastCurrentFeedId = null ;
const podcastEpCache = { } ; // id → episode data, avoids encoding strings in onclick attrs
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
let sleepTimerInterval = null ;
let sleepTimerEndSecs = 0 ;
let sleepTimerEndOfEp = false ;
let _dragSrcEl = null ;
let feedSortOrder = 'alpha' ;
2026-03-16 19:19:22 +01:00
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 } ) ;
}
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-03-22 11:45:40 +01:00
if ( location . protocol === 'https:' && url . startsWith ( 'http://' ) ) {
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 = '' ;
2026-03-16 19:19:22 +01:00
$ ( 'play-stop-btn' ) . textContent = '⏹ Stop' ;
$ ( 'play-stop-btn' ) . classList . add ( 'playing' ) ;
$ ( 'save-station-btn' ) . style . display = '' ;
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 ) {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 podcast progress before stopping
if ( podcastMode && currentEpisode ) {
savePodcastProgress ( ) ;
}
if ( seekSaveTimer ) { clearInterval ( seekSaveTimer ) ; seekSaveTimer = null ; }
2026-03-16 19:19:22 +01:00
audio . pause ( ) ;
audio . src = '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
audio . ontimeupdate = null ;
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
audio . onended = null ;
2026-03-21 17:22:51 +01:00
audio . onerror = null ;
2026-03-16 19:19:22 +01:00
isPlaying = 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
podcastMode = 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
const seekBar = $ ( 'podcast-seek-bar' ) ;
if ( seekBar ) seekBar . style . display = 'none' ;
2026-03-16 19:19:22 +01:00
if ( sseSource ) {
sseSource . close ( ) ;
sseSource = null ;
}
$ ( 'play-stop-btn' ) . textContent = '▶ Play' ;
$ ( 'play-stop-btn' ) . classList . remove ( 'playing' ) ;
$ ( 'save-station-btn' ) . style . display = 'none' ;
$ ( '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' ) ;
stationEl . classList . remove ( 'podcast-station-link' ) ;
stationEl . onclick = null ;
const trackEl = $ ( 'now-playing-track' ) ;
trackEl . classList . remove ( 'podcast-track-link' ) ;
trackEl . onclick = null ;
2026-03-16 19:19:22 +01:00
if ( clearStation ) {
currentStation = null ;
currentTrack = '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
stationEl . textContent = '— no station —' ;
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 ( ) {
if ( ! currentPlayId ) return ;
try {
await fetch ( '/radio/play/stop/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { play _id : currentPlayId } )
} ) ;
} catch ( e ) { }
currentPlayId = null ;
}
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
if ( podcastMode && currentEpisode ) {
navigator . sendBeacon ( '/podcasts/progress/save/' , JSON . stringify ( {
episode _id : currentEpisode . id ,
position _seconds : Math . floor ( audio . currentTime ) ,
} ) ) ;
}
// 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 ) {
container . innerHTML = '<p class="muted">Play more stations to get recommendations.</p>' ;
return ;
}
const label = data . context ;
let html = ` <p class="recommendations-context">Based on your ${ label } listening:</p><ul class="recommendations-list"> ` ;
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 ;
}
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 >
2026-03-16 20:16:30 +01:00
< td > < button class = "btn-delete-history" onclick = "deleteHistoryEntry(null, this)" title = "Remove" > ✕ < / 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' ) ;
statusEl . textContent = 'Searching…' ;
tableEl . style . display = 'none' ;
tbody . innerHTML = '' ;
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 ) {
statusEl . textContent = 'No stations found.' ;
return ;
}
statusEl . textContent = ` ${ stations . length } result(s) ` ;
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 || '' ,
} ) } ) ' >
& # 9654 ; Play
< / b u t t o n >
< / t d >
` ;
tbody . appendChild ( tr ) ;
} ) ;
} catch ( err ) {
statusEl . textContent = 'Search failed: ' + err . message ;
}
}
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 ) ;
}
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 ) {
2026-08-04 09:01:52 +02:00
await customAlert ( 'Please log in to save stations.' ) ;
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)"
title = "Toggle favorite" > & # 9733 ; < / b u t t o n >
< / 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})" >
& # 9654 ; Play
< / b u t t o n >
< / t d >
< td >
< button class = "btn btn-sm btn-danger"
onclick = "removeStation(${station.id})" >
Remove
< / b u t t o n >
< / t d >
` ;
tbody . appendChild ( tr ) ;
}
// ---------------------------------------------------------------------------
// Remove station
// ---------------------------------------------------------------------------
async function toggleFav ( pk ) {
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
const btn = document . querySelector ( ` #saved-row- ${ pk } .fav-btn ` ) ;
if ( btn ) btn . classList . toggle ( 'active' , data . is _favorite ) ;
// 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' ;
tr . innerHTML = '<td colspan="7" class="empty-msg">No saved stations yet.</td>' ;
tbody . appendChild ( tr ) ;
}
}
} catch ( err ) {
console . error ( 'removeStation error:' , err ) ;
}
}
// ---------------------------------------------------------------------------
// Toggle favorite
// ---------------------------------------------------------------------------
async function toggleFav ( pk , btnEl ) {
try {
const res = await fetch ( ` /radio/favorite/ ${ pk } / ` , {
method : 'POST' ,
headers : { 'X-CSRFToken' : getCsrfToken ( ) } ,
} ) ;
if ( res . ok ) {
const data = await res . json ( ) ;
if ( data . is _favorite ) {
btnEl . classList . add ( 'active' ) ;
} else {
btnEl . classList . remove ( 'active' ) ;
}
}
} catch ( err ) {
console . error ( 'toggleFav 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 ;
showTimerNotification ( 'Break time! 5 minutes.' ) ;
// auto-pause playback during break
if ( audio . src && ! audio . paused ) audio . pause ( ) ;
} else {
// break ended → reset to work
timerIsBreak = false ;
timerSeconds = TIMER _WORK ;
showTimerNotification ( 'Break over. Back to work.' ) ;
}
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 ? '⏸' : '▶' ;
if ( label ) label . textContent = timerIsBreak ? 'break' : 'focus' ;
// 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 ) {
widget . textContent = ` Today: ${ data . today _sessions } session ${ data . today _sessions !== 1 ? 's' : '' } · ${ data . today _minutes } min ` ;
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 ) {
tbody . innerHTML = '<tr><td colspan="3" class="empty-msg">No focus sessions yet. Start the timer!</td></tr>' ;
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 = [
2026-08-04 09:01:52 +02:00
{ label : 'Focus' , tag : 'ambient' } ,
{ label : 'Lo-fi' , tag : 'lofi' } ,
{ label : 'Jazz' , tag : 'jazz' } ,
{ label : 'Classical' , tag : 'classical' } ,
{ label : 'Ambient' , tag : 'ambient' } ,
{ label : 'Metal' , tag : 'metal' } ,
{ label : 'Electronic' , tag : 'electronic' } ,
{ label : 'Talk' , 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' ,
2026-08-04 09:01:52 +02:00
label : 'Focus' ,
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' ,
2026-08-04 09:01:52 +02:00
label : 'Classical' ,
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' ;
section . innerHTML = ` <div class="curated-label">★ Featured</div> ` ;
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 = `
2026-08-04 09:01:52 +02:00
< span > You listen to < strong > $ { escapeHtml ( stationName ) } < / s t r o n g > a l o t — c o n s i d e r s u p p o r t i n g t h e m ♥ < / s p a n >
2026-03-16 20:57:07 +01:00
< button onclick = "dismissDonationHint('${escapeAttr(stationUrl)}')" title = "Dismiss" > ✕ < / b u t t o n >
` ;
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 ) {
const note = await customPrompt ( 'Station note:' , 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
// ---------------------------------------------------------------------------
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 TOP _TABS = [ 'radio' , 'focus' , 'podcasts' , 'books' ] ;
const RADIO _SUB _TABS = [ 'search' , 'saved' , 'history' ] ;
2026-08-04 09:01:52 +02:00
const PODCAST _ROUTABLE _VIEWS = [ 'search' , 'feeds' , 'inbox' , 'queue' ] ;
// ---------------------------------------------------------------------------
// 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' ) ;
} else if ( _currentTopTab === 'podcasts' ) {
hash += '/' + ( podcastCurrentView || 'feeds' ) ;
}
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 {
if ( tab === 'podcasts' && PODCAST _ROUTABLE _VIEWS . includes ( sub ) ) {
podcastCurrentView = sub ;
}
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 === 'podcasts' ) loadPodcastTab ( ) ;
if ( name === 'books' ) loadBookList ( ) ;
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
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
// Podcasts
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
function loadPodcastTab ( ) {
loadFeedList ( ) . then ( ( ) => {
showPodcastView ( podcastCurrentView ) ;
} ) ;
}
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
function showPodcastView ( view ) {
podcastCurrentView = view ;
const panes = [ 'search' , 'feeds' , 'inbox' , 'episodes' , 'queue' ] ;
panes . forEach ( p => {
const el = document . getElementById ( ` podcast- ${ p } -pane ` ) ;
if ( el ) el . style . display = ( p === view ) ? '' : 'none' ;
} ) ;
2026-08-04 09:01:52 +02:00
if ( PODCAST _ROUTABLE _VIEWS . includes ( view ) ) _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 ( view === 'feeds' ) renderFeedList ( ) ;
if ( view === 'inbox' ) loadAndRenderInbox ( ) ;
if ( view === 'queue' ) loadAndRenderQueue ( ) ;
}
async function doPodcastSearch ( ) {
const q = $ ( 'podcast-search-input' ) . value . trim ( ) ;
if ( ! q ) return ;
const statusEl = $ ( 'podcast-search-status' ) ;
const listEl = $ ( 'podcast-search-list' ) ;
statusEl . textContent = 'Searching…' ;
listEl . innerHTML = '' ;
2026-03-16 19:19:22 +01:00
try {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ( '/podcasts/search/?q=' + encodeURIComponent ( q ) ) ;
2026-03-16 19:19:22 +01:00
const data = await res . json ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 . error ) { statusEl . textContent = 'Error: ' + data . error ; return ; }
const results = data . results || [ ] ;
statusEl . textContent = results . length ? ` ${ results . length } result(s) ` : 'No results.' ;
results . forEach ( r => {
const div = document . createElement ( 'div' ) ;
div . className = 'podcast-search-item' ;
div . innerHTML = `
$ { r . artwork _url ? ` <img class="podcast-thumb" src=" ${ escapeHtml ( r . artwork _url ) } " alt=""> ` : '<div class="podcast-thumb-placeholder"></div>' }
< div class = "podcast-search-info" >
< div class = "podcast-feed-title" > $ { escapeHtml ( r . title ) } < / d i v >
< div class = "muted" > $ { escapeHtml ( r . author ) } < / d i v >
< / d i v >
< button class = "btn btn-sm podcast-subscribe-btn" > Subscribe < / b u t t o n >
` ;
// Attach via addEventListener to avoid encoding strings in onclick attribute
div . querySelector ( '.podcast-subscribe-btn' ) . addEventListener ( 'click' , ( ) => {
subscribeFeed ( r . rss _url , r . title ) ;
} ) ;
listEl . appendChild ( div ) ;
} ) ;
2026-03-16 19:19:22 +01:00
} 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
statusEl . textContent = 'Search failed.' ;
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
function podcastSearchOpen ( ) {
showPodcastView ( 'search' ) ;
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
2026-08-04 09:01:52 +02:00
async function addFeedByUrl ( ) {
const url = await customPrompt ( 'RSS feed URL:' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ( url ) subscribeFeed ( url . trim ( ) , '' ) ;
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
async function subscribeFeed ( rssUrl , title ) {
if ( ! rssUrl ) return ;
const statusEl = $ ( 'podcast-search-status' ) || $ ( 'opml-status' ) ;
try {
const res = await fetch ( '/podcasts/feeds/add/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { rss _url : rssUrl , title : title || rssUrl } ) ,
} ) ;
const data = await res . json ( ) ;
if ( data . ok ) {
await loadFeedList ( ) ;
showPodcastView ( 'feeds' ) ;
} else if ( statusEl ) {
statusEl . textContent = 'Error: ' + ( data . error || 'unknown' ) ;
}
} catch ( e ) {
if ( statusEl ) statusEl . textContent = 'Failed to subscribe.' ;
}
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
async function loadFeedList ( ) {
try {
const res = await fetch ( '/podcasts/feeds/' ) ;
const data = await res . json ( ) ;
podcastFeeds = data . feeds || [ ] ;
} catch ( e ) {
podcastFeeds = [ ] ;
}
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
function renderFeedList ( ) {
const container = $ ( 'podcast-feed-list' ) ;
if ( ! container ) return ;
if ( ! podcastFeeds . length ) {
container . innerHTML = '<p class="muted">No subscriptions yet. Search or import OPML to add feeds.</p>' ;
return ;
}
container . innerHTML = '' ;
podcastFeeds . forEach ( feed => {
const div = document . createElement ( 'div' ) ;
div . className = 'podcast-feed-item' ;
div . innerHTML = `
$ { feed . artwork _url
? ` <img class="podcast-thumb" src=" ${ escapeHtml ( feed . artwork _url ) } " alt=""> `
: '<div class="podcast-thumb-placeholder"></div>' }
< div class = "podcast-feed-info" >
< div class = "podcast-feed-title" > $ { escapeHtml ( feed . title ) } < / d i v >
$ { feed . author ? ` <div class="muted"> ${ escapeHtml ( feed . author ) } </div> ` : '' }
< / d i v >
< div class = "podcast-feed-actions" >
< button class = "btn btn-sm" onclick = "openFeed(${feed.id})" > Episodes < / b u t t o n >
< button class = "btn btn-sm" onclick = "refreshFeed(${feed.id})" title = "Refresh feed" > ↻ < / b u t t o n >
2026-08-04 09:01:52 +02:00
< button class = "btn btn-sm ${feed.auto_queue ? 'active' : ''}" onclick = "toggleFeedAutoQueue(${feed.id}, this)" title = "${feed.auto_queue ? 'Auto-queue ON' : 'Auto-queue new episodes'}" > Auto < / 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 btn-danger" onclick = "removeFeed(${feed.id})" > Remove < / b u t t o n >
< / d i v >
` ;
container . appendChild ( div ) ;
2026-03-16 19:19:22 +01:00
} ) ;
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
const filterVal = ( $ ( 'feed-filter-input' ) || { } ) . value || '' ;
if ( filterVal ) filterFeeds ( filterVal ) ;
}
function filterFeeds ( query ) {
const container = $ ( 'podcast-feed-list' ) ;
if ( ! container ) return ;
const q = query . toLowerCase ( ) . trim ( ) ;
container . querySelectorAll ( '.podcast-feed-item' ) . forEach ( item => {
const title = ( item . querySelector ( '.podcast-feed-title' ) || { } ) . textContent ? . toLowerCase ( ) || '' ;
const author = ( item . querySelector ( '.muted' ) || { } ) . textContent ? . toLowerCase ( ) || '' ;
item . style . display = ( ! q || title . includes ( q ) || author . includes ( q ) ) ? '' : 'none' ;
} ) ;
}
function sortFeeds ( order ) {
feedSortOrder = order ;
if ( order === 'alpha' ) podcastFeeds . sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) ;
if ( order === 'alpha-desc' ) podcastFeeds . sort ( ( a , b ) => b . title . localeCompare ( a . title ) ) ;
if ( order === 'added' ) podcastFeeds . sort ( ( a , b ) => ( b . added _at || '' ) . localeCompare ( a . added _at || '' ) ) ;
2026-03-20 09:07:01 +01:00
if ( order === 'latest_episode' ) podcastFeeds . sort ( ( a , b ) => ( b . latest _episode _at || '' ) . localeCompare ( a . latest _episode _at || '' ) ) ;
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
renderFeedList ( ) ;
}
async function toggleFeedAutoQueue ( feedId , btn ) {
try {
const res = await fetch ( ` /podcasts/feeds/ ${ feedId } /set-auto-queue/ ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { } ) ,
} ) ;
const data = await res . json ( ) ;
if ( data . ok ) {
const feed = podcastFeeds . find ( f => f . id === feedId ) ;
if ( feed ) feed . auto _queue = data . auto _queue ;
btn . classList . toggle ( 'active' , data . auto _queue ) ;
btn . title = data . auto _queue ? 'Auto-queue ON' : 'Auto-queue new episodes' ;
}
} catch ( e ) { }
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
async function openFeed ( feedId ) {
podcastCurrentFeedId = feedId ;
showPodcastView ( 'episodes' ) ;
const headerEl = $ ( 'podcast-feed-header' ) ;
const listEl = $ ( 'podcast-episode-list' ) ;
if ( headerEl ) headerEl . innerHTML = '<p class="muted">Loading…</p>' ;
if ( listEl ) listEl . innerHTML = '' ;
try {
const res = await fetch ( ` /podcasts/feeds/ ${ feedId } /episodes/ ` ) ;
const data = await res . json ( ) ;
const feed = data . feed ;
const episodes = data . episodes || [ ] ;
if ( headerEl ) {
headerEl . innerHTML = `
< div class = "podcast-feed-header-inner" >
$ { feed . artwork _url ? ` <img class="podcast-thumb-lg" src=" ${ escapeHtml ( feed . artwork _url ) } " alt=""> ` : '' }
< div >
< div class = "podcast-feed-title" > $ { escapeHtml ( feed . title ) } < / d i v >
$ { feed . author ? ` <div class="muted"> ${ escapeHtml ( feed . author ) } </div> ` : '' }
< / d i v >
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
< button class = "btn btn-sm feed-refresh-btn" id = "feed-refresh-btn" onclick = "refreshOpenFeed(this)" title = "Refresh feed" > ↻ Refresh < / 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 >
` ;
}
renderEpisodeList ( episodes , feedId , listEl ) ;
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
const filterBar = $ ( 'episode-search-bar' ) ;
if ( filterBar ) { filterBar . style . display = '' ; $ ( 'episode-filter-input' ) . value = '' ; }
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) {
if ( headerEl ) headerEl . innerHTML = '<p class="muted">Failed to load episodes.</p>' ;
2026-03-16 19:19:22 +01:00
}
}
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
function filterEpisodes ( query ) {
const listEl = $ ( 'podcast-episode-list' ) ;
if ( ! listEl ) return ;
const q = query . toLowerCase ( ) . trim ( ) ;
listEl . querySelectorAll ( '.episode-item' ) . forEach ( item => {
const text = ( item . querySelector ( '.episode-title' ) || { } ) . textContent ? . toLowerCase ( ) || '' ;
item . style . display = ( ! q || text . includes ( q ) ) ? '' : '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
function renderEpisodeList ( episodes , feedId , container ) {
if ( ! container ) return ;
if ( ! episodes . length ) {
container . innerHTML = '<p class="muted">No episodes found.</p>' ;
return ;
}
container . innerHTML = '' ;
episodes . forEach ( ep => {
// Cache episode data by id so onclick attrs only need the id (avoids encoding
// strings with quotes inside HTML attributes which breaks the attribute parser)
podcastEpCache [ ep . id ] = {
id : ep . id ,
title : ep . title ,
description : ep . description || '' ,
audioUrl : ep . audio _url ,
durationSeconds : ep . duration _seconds ,
positionSeconds : ep . position _seconds || 0 ,
feedId : feedId || 0 ,
played : ep . played ,
} ;
const div = document . createElement ( 'div' ) ;
div . className = 'episode-item' + ( ep . played ? ' episode-played' : '' ) ;
div . id = ` episode-item- ${ ep . id } ` ;
const artSrc = ep . artwork _url || ( feedId ? ( podcastFeeds . find ( f => f . id === feedId ) || { } ) . artwork _url || '' : '' ) ;
const dur = formatDuration ( ep . duration _seconds ) ;
const dateStr = ep . pub _date ? ep . pub _date . slice ( 0 , 10 ) : '' ;
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
const posStr = ep . position _seconds > 0 ? formatDuration ( ep . position _seconds ) + ' played' : '' ;
const progressPct = ( ep . duration _seconds > 0 && ep . position _seconds > 0 )
? Math . min ( 100 , ( ep . position _seconds / ep . duration _seconds ) * 100 ) : 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
div . innerHTML = `
$ { artSrc ? ` <img class="podcast-thumb" src=" ${ escapeHtml ( artSrc ) } " alt=""> ` : '<div class="podcast-thumb-placeholder"></div>' }
< div class = "episode-info" >
< div class = "episode-title ep-clickable" onclick = "openEpisodeSidebar(${ep.id})" title = "Show notes" > $ { escapeHtml ( ep . title ) } < / d i v >
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
< div class = "episode-meta" >
$ { dateStr ? ` <span class="episode-date"> ${ escapeHtml ( dateStr ) } </span> ` : '' }
$ { dur !== '0:00' ? ` <span class="episode-dur"> ${ escapeHtml ( dur ) } </span> ` : '' }
$ { posStr ? ` <span class="episode-pos muted"> ${ escapeHtml ( posStr ) } </span> ` : '' }
< / d i v >
$ { progressPct > 0 ? ` <div class="episode-progress-bar"><div class="episode-progress-fill" style="width: ${ progressPct . toFixed ( 1 ) } %"></div></div> ` : '' }
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 = "episode-actions" >
< button class = "btn btn-sm btn-play" onclick = "playEpisodeById(${ep.id})" > ▶ < / b u t t o n >
2026-08-04 09:01:52 +02:00
< button class = "btn btn-sm" onclick = "openEpisodeSidebar(${ep.id})" title = "Show notes" > Notes < / 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" onclick = "queueAddEpisode(${ep.id})" title = "${ep.in_queue ? 'In queue' : 'Add to queue'}" > $ { ep . in _queue ? '✓Q' : '+Q' } < / b u t t o n >
< button class = "btn btn-sm" onclick = "toggleMarkPlayed(${ep.id}, this)" title = "Mark played" > $ { ep . played ? '✓' : '○' } < / b u t t o n >
< button class = "btn btn-sm" onclick = "downloadEpisodeById(${ep.id}, this)" title = "Download" > ⬇ < / b u t t o n >
< / d i v >
` ;
container . appendChild ( div ) ;
} ) ;
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
function playEpisodeById ( id ) {
const ep = podcastEpCache [ id ] ;
if ( ! ep ) return ;
playEpisode ( ep . id , ep . title , ep . audioUrl , ep . durationSeconds , ep . positionSeconds , ep . feedId ) ;
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
function downloadEpisodeById ( id , btn ) {
const ep = podcastEpCache [ id ] ;
if ( ! ep ) return ;
downloadEpisode ( ep . audioUrl , ep . title , btn ) ;
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
function playEpisode ( id , title , url , durationSeconds , positionSeconds , feedId ) {
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
// Auto-enqueue if not already in queue
const inQueue = podcastQueue . some ( q => q [ 'episode__id' ] === id ) ;
if ( ! inQueue ) {
fetch ( '/podcasts/queue/add/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : id } ) ,
} ) . then ( ( ) => {
// update local queue state and any visible +Q buttons
if ( podcastCurrentView === 'queue' ) loadAndRenderQueue ( ) ;
const qBtn = document . querySelector ( ` #episode-item- ${ id } .episode-actions .btn-sm:nth-child(3) ` ) ;
if ( qBtn && ( qBtn . textContent === '+Q' || qBtn . textContent . includes ( 'Q' ) ) ) {
qBtn . textContent = '✓Q' ; qBtn . title = 'In queue' ;
}
} ) . 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
stopPlayback ( false ) ;
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
podcastMode = true ;
isPlaying = true ; // fix: was missing, causing stop button to do nothing
currentEpisode = { id , title , audioUrl : url , durationSeconds , feedId } ;
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
audio . src = url ;
2026-03-16 19:19:22 +01:00
const volSlider = $ ( 'volume' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ( volSlider ) audio . volume = volSlider . value / 255 ;
if ( positionSeconds > 0 ) {
audio . addEventListener ( 'loadedmetadata' , function onMeta ( ) {
audio . currentTime = positionSeconds ;
audio . removeEventListener ( 'loadedmetadata' , onMeta ) ;
} ) ;
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
audio . play ( ) . catch ( e => console . warn ( 'Podcast play blocked:' , e ) ) ;
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
const feedTitle = ( podcastFeeds . find ( f => f . id === feedId ) || { } ) . title || 'Podcast' ;
const stationEl = $ ( 'now-playing-station' ) ;
stationEl . textContent = feedTitle ;
stationEl . classList . add ( 'podcast-station-link' ) ;
stationEl . onclick = ( ) => { showTab ( 'podcasts' ) ; openFeed ( feedId ) ; } ;
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
const trackEl = $ ( 'now-playing-track' ) ;
trackEl . textContent = title ;
trackEl . classList . add ( 'podcast-track-link' ) ;
trackEl . onclick = ( ) => openEpisodeSidebar ( id ) ;
$ ( 'play-stop-btn' ) . style . display = '' ;
$ ( 'play-stop-btn' ) . textContent = '⏹ Stop' ;
$ ( 'play-stop-btn' ) . classList . add ( 'playing' ) ;
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
const seekBar = $ ( 'podcast-seek-bar' ) ;
if ( seekBar ) seekBar . style . display = '' ;
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
const slider = $ ( 'seek-slider' ) ;
if ( slider && durationSeconds > 0 ) slider . max = durationSeconds ;
// Reset speed to 1× for each new episode
setPlaybackRate ( 1 ) ;
audio . ontimeupdate = podcastTimeUpdate ;
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
audio . onended = podcastOnEnded ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
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
// Media Session API — maps hardware media keys, lock-screen controls, and
// Windows taskbar thumbnail buttons (play/pause, previous, next)
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ( 'mediaSession' in navigator ) {
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
const feedTitle = ( podcastFeeds . find ( f => f . id === feedId ) || { } ) . title || '' ;
const artSrc = ( podcastFeeds . find ( f => f . id === feedId ) || { } ) . artwork _url || '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
navigator . mediaSession . metadata = new MediaMetadata ( {
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
title ,
artist : feedTitle ,
artwork : artSrc ? [ { src : artSrc , sizes : '512x512' , type : 'image/jpeg' } ] : [ ] ,
2026-03-16 19:19:22 +01:00
} ) ;
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
navigator . mediaSession . setActionHandler ( 'play' , ( ) => { audio . play ( ) ; isPlaying = true ; } ) ;
navigator . mediaSession . setActionHandler ( 'pause' , ( ) => { audio . pause ( ) ; isPlaying = false ; } ) ;
navigator . mediaSession . setActionHandler ( 'stop' , ( ) => stopPlayback ( 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
navigator . mediaSession . setActionHandler ( 'seekbackward' , ( ) => skipBack ( ) ) ;
navigator . mediaSession . setActionHandler ( 'seekforward' , ( ) => skipForward ( ) ) ;
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
navigator . mediaSession . setActionHandler ( 'nexttrack' , ( ) => podcastOnEnded ( ) ) ;
try { navigator . mediaSession . setActionHandler ( 'previoustrack' , ( ) => skipBack ( ) ) ; } catch ( _ ) { }
navigator . mediaSession . playbackState = 'playing' ;
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
if ( seekSaveTimer ) clearInterval ( seekSaveTimer ) ;
seekSaveTimer = setInterval ( savePodcastProgress , 15000 ) ;
}
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
function podcastTimeUpdate ( ) {
const pos = Math . floor ( audio . currentTime ) ;
const dur = currentEpisode ? currentEpisode . durationSeconds : 0 ;
const curEl = $ ( 'seek-current' ) ;
if ( curEl ) curEl . textContent = formatDuration ( pos ) ;
const durEl = $ ( 'seek-duration' ) ;
if ( durEl ) durEl . textContent = formatDuration ( dur || Math . floor ( audio . duration ) || 0 ) ;
const slider = $ ( 'seek-slider' ) ;
if ( slider ) {
if ( dur > 0 ) {
slider . max = dur ;
} else if ( audio . duration && isFinite ( audio . duration ) ) {
slider . max = Math . floor ( audio . duration ) ;
}
slider . value = pos ;
}
}
function skipBack ( ) {
audio . currentTime = Math . max ( 0 , audio . currentTime - 15 ) ;
}
function skipForward ( ) {
const dur = audio . duration ;
audio . currentTime = dur && isFinite ( dur )
? Math . min ( dur , audio . currentTime + 30 )
: audio . currentTime + 30 ;
}
function setPlaybackRate ( rate ) {
audio . playbackRate = rate ;
// Keep pitch natural at non-1× speeds (supported in all modern browsers)
audio . preservesPitch = true ;
document . querySelectorAll ( '.speed-btn' ) . forEach ( btn => {
btn . classList . toggle ( 'active' , parseFloat ( btn . textContent ) === rate
|| ( rate === 0.75 && btn . textContent . startsWith ( '¾' ) )
|| ( rate === 1.25 && btn . textContent . startsWith ( '1¼' ) )
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
|| ( rate === 1.5 && btn . textContent . startsWith ( '1½' ) )
|| ( rate === 1.75 && btn . textContent . startsWith ( '1¾' ) )
|| ( rate === 2.5 && btn . textContent . startsWith ( '2½' ) ) ) ;
} ) ;
}
async function podcastOnEnded ( ) {
if ( ! podcastMode || ! currentEpisode ) return ;
await fetch ( '/podcasts/progress/mark-played/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : currentEpisode . id , played : true } ) ,
} ) . catch ( ( ) => { } ) ;
if ( sleepTimerEndOfEp ) { clearSleepTimer ( ) ; audio . pause ( ) ; return ; }
const finishedId = currentEpisode . id ;
try {
const res = await fetch ( '/podcasts/queue/' ) ;
const data = await res . json ( ) ;
const items = data . queue || [ ] ;
const currentIdx = items . findIndex ( item => item [ 'episode__id' ] === finishedId ) ;
const nextItem = currentIdx >= 0 ? items [ currentIdx + 1 ] : null ;
await fetch ( '/podcasts/queue/remove/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : finishedId } ) ,
} ) . catch ( ( ) => { } ) ;
if ( nextItem ) {
const nextEpId = nextItem [ 'episode__id' ] ;
const cached = podcastEpCache [ nextEpId ] || { } ;
playEpisode ( nextEpId , nextItem [ 'episode__title' ] , nextItem [ 'episode__audio_url' ] ,
nextItem [ 'episode__duration_seconds' ] , cached . positionSeconds || 0 , nextItem [ 'episode__feed__id' ] ) ;
} else {
stopPlayback ( false ) ;
}
if ( podcastCurrentView === 'queue' ) loadAndRenderQueue ( ) ;
} catch ( e ) {
stopPlayback ( false ) ;
}
}
function openSleepTimerMenu ( ) {
const existing = document . getElementById ( 'sleep-timer-menu' ) ;
if ( existing ) { existing . remove ( ) ; return ; }
const options = [
{ label : 'Off' , value : 0 } , { label : '5m' , value : 5 } , { label : '10m' , value : 10 } ,
{ label : '15m' , value : 15 } , { label : '30m' , value : 30 } , { label : '45m' , value : 45 } ,
{ label : '60m' , value : 60 } , { label : 'End of episode' , value : - 1 } ,
] ;
const menu = document . createElement ( 'div' ) ;
menu . id = 'sleep-timer-menu' ;
menu . className = 'sleep-timer-menu' ;
options . forEach ( opt => {
const btn = document . createElement ( 'button' ) ;
btn . className = 'sleep-timer-option' ;
btn . textContent = opt . label ;
btn . onclick = ( ) => { setSleepTimer ( opt . value ) ; menu . remove ( ) ; } ;
menu . appendChild ( btn ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} ) ;
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
document . getElementById ( 'sleep-timer-btn' ) . insertAdjacentElement ( 'afterend' , menu ) ;
}
function setSleepTimer ( minutes ) {
clearSleepTimer ( ) ;
const btn = document . getElementById ( 'sleep-timer-btn' ) ;
if ( minutes === 0 ) { if ( btn ) btn . textContent = 'Sleep' ; return ; }
if ( minutes === - 1 ) {
sleepTimerEndOfEp = true ;
if ( btn ) btn . textContent = 'Sleep:EoE' ;
return ;
}
sleepTimerEndOfEp = false ;
sleepTimerEndSecs = Math . floor ( Date . now ( ) / 1000 ) + minutes * 60 ;
sleepTimerInterval = setInterval ( ( ) => {
const remaining = sleepTimerEndSecs - Math . floor ( Date . now ( ) / 1000 ) ;
if ( remaining <= 0 ) { clearSleepTimer ( ) ; audio . pause ( ) ; isPlaying = false ; return ; }
const m = Math . floor ( remaining / 60 ) ;
const s = remaining % 60 ;
if ( btn ) btn . textContent = ` ${ m } : ${ String ( s ) . padStart ( 2 , '0' ) } ` ;
} , 1000 ) ;
}
function clearSleepTimer ( ) {
if ( sleepTimerInterval ) { clearInterval ( sleepTimerInterval ) ; sleepTimerInterval = null ; }
sleepTimerEndOfEp = false ;
sleepTimerEndSecs = 0 ;
const btn = document . getElementById ( 'sleep-timer-btn' ) ;
if ( btn ) btn . textContent = 'Sleep' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 savePodcastProgress ( ) {
if ( ! currentEpisode ) return ;
const pos = Math . floor ( audio . currentTime ) ;
try {
await fetch ( '/podcasts/progress/save/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : currentEpisode . id , position _seconds : pos } ) ,
} ) ;
} catch ( e ) { }
}
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
let _inboxOffset = 0 ;
2026-04-04 21:10:14 +02:00
const _inboxPageSize = DIORA _CONFIG . podcastInboxPageSize ;
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
async function loadAndRenderInbox ( append = 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 listEl = $ ( 'podcast-inbox-list' ) ;
if ( ! listEl ) return ;
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 ( ! append ) {
_inboxOffset = 0 ;
listEl . innerHTML = '<p class="muted">Loading…</p>' ;
inboxUpdateBulkBar ( ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 {
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
const res = await fetch ( ` /podcasts/inbox/?limit= ${ _inboxPageSize } &offset= ${ _inboxOffset } ` ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 data = await res . json ( ) ;
const episodes = data . episodes || [ ] ;
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 ( ! append ) listEl . innerHTML = '' ;
if ( ! episodes . length && ! append ) {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 = '<p class="muted">Inbox empty — all caught up!</p>' ;
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
$ ( 'inbox-load-more-bar' ) . style . display = '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
return ;
}
episodes . forEach ( ep => {
podcastEpCache [ ep . id ] = {
id : ep . id ,
title : ep . title ,
2026-03-20 09:12:36 +01:00
description : ep . description || '' ,
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
audioUrl : ep . audio _url ,
durationSeconds : ep . duration _seconds ,
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
positionSeconds : ep . position _seconds || 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
feedId : ep [ 'feed__id' ] ,
played : 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
const progressPct = ( ep . duration _seconds > 0 && ep . position _seconds > 0 )
? Math . min ( 100 , ( ep . position _seconds / ep . duration _seconds ) * 100 ) : 0 ;
const dur = formatDuration ( ep . duration _seconds ) ;
const dateStr = ep . pub _date ? ep . pub _date . slice ( 0 , 10 ) : '' ;
const inQueue = ep . in _queue ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 . className = 'episode-item' ;
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
div . dataset . epId = ep . 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 . innerHTML = `
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
< label class = "inbox-checkbox-label" >
< input type = "checkbox" class = "inbox-cb" data - ep - id = "${ep.id}" onchange = "inboxOnCheck()" >
< / 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
$ { ep [ 'feed__artwork_url' ] ? ` <img class="podcast-thumb" src=" ${ escapeHtml ( ep [ 'feed__artwork_url' ] ) } " alt=""> ` : '<div class="podcast-thumb-placeholder"></div>' }
< div class = "episode-info" >
2026-03-20 09:12:36 +01:00
< div class = "episode-title ep-clickable" onclick = "openEpisodeSidebar(${ep.id})" title = "Show notes" > $ { escapeHtml ( ep . title ) } < / d i v >
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
< div class = "episode-meta" >
2026-03-20 09:12:36 +01:00
< span class = "episode-date episode-feed-link" onclick = "openFeed(${ep['feed__id']})" title = "Open feed" > $ { escapeHtml ( ep [ 'feed__title' ] ) } < / s p a n >
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
$ { dateStr ? ` <span class="episode-dur"> ${ escapeHtml ( dateStr ) } </span> ` : '' }
$ { dur !== '0:00' ? ` <span class="episode-dur"> ${ escapeHtml ( dur ) } </span> ` : '' }
< / d i v >
$ { progressPct > 0 ? ` <div class="episode-progress-bar"><div class="episode-progress-fill" style="width: ${ progressPct . toFixed ( 1 ) } %"></div></div> ` : '' }
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 = "episode-actions" >
< button class = "btn btn-sm btn-play" onclick = "playEpisodeById(${ep.id})" > ▶ < / b u t t o n >
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
< button class = "btn btn-sm" onclick = "queueAddEpisode(${ep.id})" title = "${inQueue ? 'In queue' : 'Add to queue'}" > $ { inQueue ? '✓Q' : '+Q' } < / 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" onclick = "downloadEpisodeById(${ep.id}, this)" title = "Download" > ⬇ < / b u t t o n >
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
< button class = "btn btn-sm btn-danger" onclick = "inboxDismissOne(${ep.id}, this)" title = "Dismiss" > ✕ < / 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 >
` ;
listEl . appendChild ( div ) ;
} ) ;
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
_inboxOffset += episodes . length ;
const moreBar = $ ( 'inbox-load-more-bar' ) ;
const countLabel = $ ( 'inbox-count-label' ) ;
if ( episodes . length === _inboxPageSize ) {
moreBar . style . display = '' ;
if ( countLabel ) countLabel . textContent = ` ${ _inboxOffset } loaded ` ;
} else {
moreBar . style . display = 'none' ;
if ( countLabel ) countLabel . textContent = '' ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) {
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 ( ! append ) listEl . innerHTML = '<p class="muted">Failed to load inbox.</p>' ;
}
}
function inboxLoadMore ( ) {
loadAndRenderInbox ( true ) ;
}
function inboxGetSelectedIds ( ) {
return Array . from ( document . querySelectorAll ( '.inbox-cb:checked' ) )
. map ( cb => parseInt ( cb . dataset . epId , 10 ) ) ;
}
function inboxOnCheck ( ) {
inboxUpdateBulkBar ( ) ;
// sync select-all state
const all = document . querySelectorAll ( '.inbox-cb' ) ;
const checked = document . querySelectorAll ( '.inbox-cb:checked' ) ;
const selectAll = $ ( 'inbox-select-all' ) ;
if ( selectAll ) {
selectAll . indeterminate = checked . length > 0 && checked . length < all . length ;
selectAll . checked = all . length > 0 && checked . length === all . length ;
}
}
function inboxSelectAll ( checked ) {
document . querySelectorAll ( '.inbox-cb' ) . forEach ( cb => { cb . checked = checked ; } ) ;
inboxUpdateBulkBar ( ) ;
}
function inboxUpdateBulkBar ( ) {
const ids = inboxGetSelectedIds ( ) ;
const bar = $ ( 'inbox-bulk-actions' ) ;
const countEl = $ ( 'inbox-selection-count' ) ;
if ( ! bar ) return ;
if ( ids . length > 0 ) {
bar . style . display = '' ;
if ( countEl ) countEl . textContent = ` ${ ids . length } selected ` ;
} else {
bar . style . display = 'none' ;
}
}
async function inboxBulkDismiss ( ) {
const ids = inboxGetSelectedIds ( ) ;
if ( ! ids . length ) return ;
try {
await fetch ( '/podcasts/progress/dismiss/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _ids : ids , dismissed : true } ) ,
} ) ;
ids . forEach ( id => {
const div = document . querySelector ( ` .episode-item[data-ep-id=" ${ id } "] ` ) ;
if ( div ) div . remove ( ) ;
} ) ;
inboxUpdateBulkBar ( ) ;
const selectAll = $ ( 'inbox-select-all' ) ;
if ( selectAll ) { selectAll . checked = false ; selectAll . indeterminate = false ; }
if ( ! document . querySelector ( '.inbox-cb' ) ) {
const listEl = $ ( 'podcast-inbox-list' ) ;
if ( listEl && ! listEl . querySelector ( '.episode-item' ) ) {
listEl . innerHTML = '<p class="muted">Inbox empty — all caught up!</p>' ;
}
}
} catch ( e ) { }
}
async function inboxDismissOne ( epId , btn ) {
try {
await fetch ( '/podcasts/progress/dismiss/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _ids : [ epId ] , dismissed : true } ) ,
} ) ;
const div = document . querySelector ( ` .episode-item[data-ep-id=" ${ epId } "] ` ) ;
if ( div ) div . remove ( ) ;
if ( ! document . querySelector ( '.inbox-cb' ) ) {
const listEl = $ ( 'podcast-inbox-list' ) ;
if ( listEl && ! listEl . querySelector ( '.episode-item' ) ) {
listEl . innerHTML = '<p class="muted">Inbox empty — all caught up!</p>' ;
}
}
} catch ( e ) { }
}
async function inboxBulkQueueAdd ( ) {
const ids = inboxGetSelectedIds ( ) ;
for ( const id of ids ) {
await queueAddEpisode ( id ) ;
}
// Update queue button states
ids . forEach ( id => {
const div = document . querySelector ( ` .episode-item[data-ep-id=" ${ id } "] ` ) ;
if ( div ) {
const qBtn = div . querySelector ( '.episode-actions .btn-sm:nth-child(2)' ) ;
if ( qBtn ) { qBtn . textContent = '✓Q' ; qBtn . title = 'In queue' ; }
}
} ) ;
}
async function inboxBulkMarkPlayed ( ) {
const ids = inboxGetSelectedIds ( ) ;
try {
for ( const id of ids ) {
await fetch ( '/podcasts/progress/mark-played/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : id , played : true } ) ,
} ) ;
}
ids . forEach ( id => {
const div = document . querySelector ( ` .episode-item[data-ep-id=" ${ id } "] ` ) ;
if ( div ) div . remove ( ) ;
} ) ;
inboxUpdateBulkBar ( ) ;
const selectAll = $ ( 'inbox-select-all' ) ;
if ( selectAll ) { selectAll . checked = false ; selectAll . indeterminate = false ; }
} catch ( e ) { }
}
async function inboxBulkDownload ( ) {
const ids = inboxGetSelectedIds ( ) ;
for ( const id of ids ) {
const ep = podcastEpCache [ id ] ;
if ( ep ) await downloadEpisode ( ep . audioUrl , ep . title , null ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
async function loadAndRenderQueue ( ) {
const ol = $ ( 'podcast-queue-ol' ) ;
if ( ! ol ) return ;
ol . innerHTML = '<li class="muted">Loading…</li>' ;
try {
const res = await fetch ( '/podcasts/queue/' ) ;
const data = await res . json ( ) ;
const items = data . queue || [ ] ;
podcastQueue = items ;
ol . innerHTML = '' ;
if ( ! items . length ) {
ol . innerHTML = '<li class="muted">Queue is empty.</li>' ;
return ;
}
items . forEach ( item => {
const epId = item [ 'episode__id' ] ;
podcastEpCache [ epId ] = {
id : epId ,
title : item [ 'episode__title' ] ,
audioUrl : item [ 'episode__audio_url' ] ,
durationSeconds : item [ 'episode__duration_seconds' ] ,
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
positionSeconds : item [ 'position_seconds' ] || 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
feedId : item [ 'episode__feed__id' ] ,
played : 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
const progressPct = ( item [ 'episode__duration_seconds' ] > 0 && item [ 'position_seconds' ] > 0 )
? Math . min ( 100 , ( item [ 'position_seconds' ] / item [ 'episode__duration_seconds' ] ) * 100 ) : 0 ;
const dur = formatDuration ( item [ 'episode__duration_seconds' ] ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 li = document . createElement ( 'li' ) ;
li . className = 'episode-item' ;
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
li . draggable = true ;
li . dataset . epId = epId ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
li . innerHTML = `
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
< span class = "drag-handle" > ⠿ < / 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
< div class = "episode-info" >
< div class = "episode-title" > $ { escapeHtml ( item [ 'episode__title' ] ) } < / d i v >
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
< div class = "episode-meta" >
2026-03-20 09:12:36 +01:00
< span class = "episode-date episode-feed-link" onclick = "openFeed(${item['episode__feed__id']})" title = "Open feed" > $ { escapeHtml ( item [ 'episode__feed__title' ] ) } < / s p a n >
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
$ { dur !== '0:00' ? ` <span class="episode-dur"> ${ escapeHtml ( dur ) } </span> ` : '' }
< / d i v >
$ { progressPct > 0 ? ` <div class="episode-progress-bar"><div class="episode-progress-fill" style="width: ${ progressPct . toFixed ( 1 ) } %"></div></div> ` : '' }
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 = "episode-actions" >
< button class = "btn btn-sm btn-play" onclick = "playEpisodeById(${epId})" > ▶ < / b u t t o n >
< button class = "btn btn-sm" onclick = "downloadEpisodeById(${epId}, this)" title = "Download" > ⬇ < / b u t t o n >
< button class = "btn btn-sm btn-danger" onclick = "queueRemoveEpisode(${epId})" > ✕ < / b u t t o n >
< / d i v >
` ;
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
li . addEventListener ( 'dragstart' , queueDragStart ) ;
li . addEventListener ( 'dragover' , queueDragOver ) ;
li . addEventListener ( 'drop' , queueDrop ) ;
li . addEventListener ( 'dragend' , queueDragEnd ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
ol . appendChild ( li ) ;
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
} catch ( e ) {
ol . innerHTML = '<li class="muted">Failed to load queue.</li>' ;
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
}
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
function queueDragStart ( e ) {
_dragSrcEl = this ;
e . dataTransfer . effectAllowed = 'move' ;
this . classList . add ( 'dragging' ) ;
}
function queueDragOver ( e ) {
e . preventDefault ( ) ;
const ol = document . getElementById ( 'podcast-queue-ol' ) ;
const dragging = ol . querySelector ( '.dragging' ) ;
if ( ! dragging || dragging === this ) return ;
const rect = this . getBoundingClientRect ( ) ;
ol . insertBefore ( dragging , e . clientY < rect . top + rect . height / 2 ? this : this . nextSibling ) ;
}
function queueDrop ( e ) { e . preventDefault ( ) ; }
function queueDragEnd ( ) {
this . classList . remove ( 'dragging' ) ;
_dragSrcEl = null ;
const ol = document . getElementById ( 'podcast-queue-ol' ) ;
const newOrder = Array . from ( ol . querySelectorAll ( 'li[data-ep-id]' ) )
. map ( li => parseInt ( li . dataset . epId , 10 ) ) ;
fetch ( '/podcasts/queue/reorder/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { order : newOrder } ) ,
} ) . 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
async function queueAddEpisode ( id ) {
try {
await fetch ( '/podcasts/queue/add/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : id } ) ,
} ) ;
} catch ( e ) { }
}
async function queueRemoveEpisode ( id ) {
try {
await fetch ( '/podcasts/queue/remove/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : id } ) ,
} ) ;
if ( podcastCurrentView === 'queue' ) loadAndRenderQueue ( ) ;
} catch ( e ) { }
}
async function toggleMarkPlayed ( id , btn ) {
const ep = podcastEpCache [ id ] ;
const current = ep ? ep . played : btn . textContent === '✓' ;
const newPlayed = ! current ;
try {
const res = await fetch ( '/podcasts/progress/mark-played/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { episode _id : id , played : newPlayed } ) ,
} ) ;
if ( res . ok ) {
if ( ep ) ep . played = newPlayed ;
btn . textContent = newPlayed ? '✓' : '○' ;
const item = document . getElementById ( ` episode-item- ${ id } ` ) ;
if ( item ) item . classList . toggle ( 'episode-played' , newPlayed ) ;
}
} catch ( e ) { }
}
async function refreshFeed ( feedId ) {
try {
const res = await fetch ( '/podcasts/feeds/refresh/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { feed _id : feedId } ) ,
} ) ;
const data = await res . json ( ) ;
if ( data . ok && podcastCurrentFeedId === feedId ) {
openFeed ( feedId ) ;
}
} catch ( e ) { }
}
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
async function refreshOpenFeed ( btn ) {
if ( ! podcastCurrentFeedId ) return ;
if ( btn ) { btn . disabled = true ; btn . textContent = '↻ …' ; }
try {
const res = await fetch ( '/podcasts/feeds/refresh/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { feed _id : podcastCurrentFeedId } ) ,
} ) ;
const data = await res . json ( ) ;
if ( data . ok ) {
await openFeed ( podcastCurrentFeedId ) ;
// openFeed re-renders the header, btn reference is stale — nothing to restore
return ;
}
} catch ( e ) { }
if ( btn ) { btn . disabled = false ; btn . textContent = '↻ Refresh' ; }
}
2026-03-20 09:18:38 +01:00
async function refreshAllFeedsBtn ( btn ) {
if ( btn ) { btn . disabled = true ; btn . textContent = '↻ 0/' + podcastFeeds . length ; }
let done = 0 ;
for ( const feed of podcastFeeds ) {
try {
await fetch ( '/podcasts/feeds/refresh/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { feed _id : feed . id } ) ,
} ) ;
} catch ( e ) { }
done ++ ;
if ( btn ) btn . textContent = ` ↻ ${ done } / ${ podcastFeeds . length } ` ;
}
await loadFeedList ( ) ;
if ( podcastCurrentView === 'feeds' ) renderFeedList ( ) ;
if ( podcastCurrentView === 'inbox' ) loadAndRenderInbox ( ) ;
if ( podcastCurrentView === 'episodes' && podcastCurrentFeedId ) openFeed ( podcastCurrentFeedId ) ;
if ( btn ) { btn . disabled = false ; btn . textContent = '↻ All' ; }
}
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
async function refreshAllFeeds ( ) {
if ( ! IS _AUTHENTICATED || ! podcastFeeds . length ) return ;
for ( const feed of podcastFeeds ) {
try {
await fetch ( '/podcasts/feeds/refresh/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { feed _id : feed . id } ) ,
} ) ;
} catch ( e ) { }
}
// Reload feed metadata and refresh the currently open view
await loadFeedList ( ) ;
if ( podcastCurrentView === 'feeds' ) renderFeedList ( ) ;
if ( podcastCurrentView === 'inbox' ) loadAndRenderInbox ( ) ;
if ( podcastCurrentView === 'episodes' && podcastCurrentFeedId ) openFeed ( podcastCurrentFeedId ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 removeFeed ( feedId ) {
2026-08-04 09:01:52 +02:00
if ( ! await customConfirm ( 'Remove this podcast?' , { 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 {
await fetch ( ` /podcasts/feeds/ ${ feedId } /remove/ ` , {
method : 'POST' ,
headers : { 'X-CSRFToken' : getCsrfToken ( ) } ,
} ) ;
podcastFeeds = podcastFeeds . filter ( f => f . id !== feedId ) ;
renderFeedList ( ) ;
} catch ( e ) { }
}
async function importOPML ( input ) {
const file = input . files [ 0 ] ;
if ( ! file ) return ;
const statusEl = $ ( 'opml-status' ) ;
if ( statusEl ) statusEl . textContent = 'Importing…' ;
const form = new FormData ( ) ;
form . append ( 'file' , file ) ;
form . append ( 'csrfmiddlewaretoken' , getCsrfToken ( ) ) ;
try {
const res = await fetch ( '/podcasts/feeds/import/' , { method : 'POST' , body : form } ) ;
const data = await res . json ( ) ;
if ( data . ok ) {
if ( statusEl ) statusEl . textContent = ` ✓ ${ data . added } added, ${ data . skipped } already subscribed ` ;
await loadFeedList ( ) ;
renderFeedList ( ) ;
} else {
if ( statusEl ) statusEl . textContent = 'Error: ' + ( data . error || 'unknown' ) ;
}
} catch ( e ) {
if ( statusEl ) statusEl . textContent = 'Import failed.' ;
}
input . value = '' ;
}
async function downloadEpisode ( url , title , btn ) {
if ( ! ( 'caches' in window ) ) {
2026-08-04 09:01:52 +02:00
await customAlert ( 'Cache API not supported in this browser.' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 origText = btn ? btn . textContent : '⬇' ;
if ( btn ) { btn . textContent = '…' ; btn . disabled = true ; }
try {
const cache = await caches . open ( 'diora-podcast-v1' ) ;
const existing = await cache . match ( url ) ;
if ( existing ) {
if ( btn ) { btn . textContent = '✓' ; btn . disabled = false ; }
return ;
}
// Use no-cors so cross-origin audio URLs don't block the fetch
const resp = await fetch ( url , { mode : 'no-cors' } ) ;
await cache . put ( url , resp ) ;
if ( btn ) { btn . textContent = '✓' ; btn . disabled = false ; }
} catch ( e ) {
if ( btn ) { btn . textContent = origText ; btn . disabled = false ; }
2026-08-04 09:01:52 +02:00
await customAlert ( 'Download failed: ' + 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
}
}
// ---------------------------------------------------------------------------
// 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 ) {
if ( ! html ) return '<p class="muted">No show notes available.</p>' ;
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 ;
}
function openEpisodeSidebar ( id ) {
const ep = podcastEpCache [ id ] ;
if ( ! ep ) return ;
openSidebar ( ep . title , ep . description || '' ) ;
}
function formatDuration ( seconds ) {
if ( ! seconds || seconds <= 0 ) return '0:00' ;
const h = Math . floor ( seconds / 3600 ) ;
const m = Math . floor ( ( seconds % 3600 ) / 60 ) ;
const s = seconds % 60 ;
if ( h > 0 ) {
return ` ${ h } : ${ String ( m ) . padStart ( 2 , '0' ) } : ${ String ( s ) . padStart ( 2 , '0' ) } ` ;
}
return ` ${ m } : ${ String ( s ) . padStart ( 2 , '0' ) } ` ;
}
// ---------------------------------------------------------------------------
// 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,
// but only if the reader isn't open (would interrupt reading).
navigator . serviceWorker . addEventListener ( 'message' , e => {
if ( e . data ? . type === 'SW_ACTIVATED' && ! currentBookId ) {
window . location . reload ( ) ;
}
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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' ) ;
status . textContent = 'Importing…' ;
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 ) {
status . textContent = ` ✓ ${ data . added } added, ${ data . skipped } already saved ` ;
if ( data . added > 0 ) location . reload ( ) ;
} else {
status . textContent = ` Error: ${ data . error } ` ;
}
} catch ( e ) {
status . textContent = 'Upload failed' ;
}
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 ) ) {
2026-08-04 09:01:52 +02:00
await customAlert ( 'Only JPEG, PNG, or WebP images are allowed.' ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) {
2026-08-04 09:01:52 +02:00
await customAlert ( ` Image must be ${ DIORA _CONFIG . bgMaxBytes / 1024 / 1024 } MB or smaller. ` ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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' ) ;
if ( ! rootfileEl ) throw new Error ( 'No rootfile in container.xml' ) ;
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' ) ;
const title = opfDoc . querySelector ( 'metadata > title, metadata > *|title' ) ? . textContent ? . trim ( ) || 'Unknown Title' ;
const author = opfDoc . querySelector ( 'metadata > creator, metadata > *|creator' ) ? . textContent ? . trim ( ) || 'Unknown Author' ;
// 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 ) ;
2026-04-29 11:40:58 +02:00
const chapters = [ ] ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ] ;
try {
2026-04-29 11:40:58 +02:00
let chapterText = await zip . file ( href ) . async ( 'text' ) ;
// 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> ` ) ;
if ( i % 8 === 0 ) await new Promise ( r => requestAnimationFrame ( r ) ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) { /* skip missing */ }
}
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' ) ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) {
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' ) ;
}
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 ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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(*))' ;
function getPositionAnchor ( contentEl ) {
const blocks = Array . from ( contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) ) ;
if ( ! blocks . length ) return '' ;
const containerTop = contentEl . getBoundingClientRect ( ) . top ;
const containerBottom = containerTop + contentEl . clientHeight ;
let bestIndex = 0 ;
let bestDelta = Infinity ;
for ( let i = 0 ; i < blocks . length ; i ++ ) {
const rect = blocks [ i ] . getBoundingClientRect ( ) ;
if ( rect . height < 1 ) continue ;
if ( rect . top > containerBottom ) break ;
const delta = rect . top - containerTop ;
if ( delta <= 0 && Math . abs ( delta ) < Math . abs ( bestDelta ) ) {
bestIndex = i ;
bestDelta = delta ;
} else if ( delta > 0 && bestDelta === Infinity ) {
bestIndex = i ;
bestDelta = delta ;
}
}
const rect = blocks [ bestIndex ] . getBoundingClientRect ( ) ;
const innerFraction = Math . max ( 0 , Math . min ( 1 , ( containerTop - rect . top ) / ( rect . height || 1 ) ) ) ;
return ` ${ bestIndex } : ${ innerFraction . toFixed ( 6 ) } ` ;
}
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' ;
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 ) => {
2026-05-29 21:14:40 +02:00
const req = indexedDB . open ( 'diora_book_cache' , 2 ) ;
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' } ) ;
} ;
2026-04-01 16:10:03 +02:00
req . onsuccess = e => resolve ( e . target . result ) ;
req . onerror = ( ) => reject ( ) ;
} ) ;
}
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-04-29 11:52:45 +02:00
fontFamily : 'serif' , noBold : false ,
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 ;
2026-03-20 12:28:00 +01:00
listEl . innerHTML = '<p class="muted">Loading books…</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 {
2026-03-20 12:28:00 +01:00
listEl . innerHTML = '<p class="muted">Fetching book list from server…</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 ) {
listEl . innerHTML = ` <p class="muted">Server error ${ res . status } loading books.</p> ` ;
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 ) ) {
2026-03-20 12:28:00 +01:00
listEl . innerHTML = ` <p class="muted">Unexpected response from server (not an array).</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 ) {
listEl . innerHTML = '<p class="muted">No books yet. Drop an .epub or .pdf above.</p>' ;
return ;
}
2026-03-20 12:28:00 +01:00
listEl . innerHTML = ` <p class="muted">Found ${ books . length } book(s) on server. Decrypting…</p> ` ;
2026-03-20 09:45:39 +01:00
let key ;
try {
key = await getOrCreateEncKey ( ) ;
} catch ( e ) {
listEl . innerHTML = ` <p class="muted">Encryption not available: ${ e . message } . Make sure you are on HTTPS.</p> ` ;
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-08-17 11:27:16 +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 || '' } ;
decrypted . push ( { id : b . id , title : meta . title || '?' , author : meta . author || '' , type : meta . type || 'epub' , folder : meta . folder || '' , isbn : meta . isbn || '' , shelfTag : meta . shelfTag || '' , 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 ) {
2026-08-17 11:27:16 +02:00
bookMetaCache [ b . id ] = { title : ` Book # ${ b . id } ` , author : '' , type : 'epub' , filename : '' , folder : '' , isbn : '' , shelfTag : '' } ;
decrypted . push ( { id : b . id , title : ` Book # ${ 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-08-17 11:27:16 +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 || '' } ;
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 {
if ( listEl ) listEl . innerHTML = ` <p class="muted">Error loading books: ${ e . message } </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
}
}
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
} ) ;
if ( ! ( await res . json ( ) ) . ok ) throw new Error ( 'Server error' ) ;
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 ( ) ;
if ( ! data . ok ) throw new Error ( 'Server error' ) ;
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 ) ;
const keyWarning = b . keyOk === false ? '<span title="Wrong encryption key — import the correct key to open this book" style="color:var(--accent,#e63946);margin-left:4px;">⚠ wrong key</span>' : '' ;
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" >
$ { pct > 0 ? ` <span class="muted book-progress"> ${ pct } % read</span> ` : '' }
$ { 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" >
2026-03-20 09:45:39 +01:00
< button class = "btn btn-sm" onclick = "openBook(${b.id})" $ { b . keyOk === false ? ' disabled title="Import the correct encryption key first"' : '' } > Open < / b u t t o n >
2026-08-17 11:27:16 +02:00
< div class = "book-item-menu" >
< button class = "btn btn-sm book-menu-toggle" title = "Weitere Optionen" onclick = "toggleBookMenu(${b.id})" > ⋮ < / b u t t o n >
< div class = "book-item-menu-list" id = "book-menu-${b.id}" >
$ { 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 11:27:16 +02:00
function toggleBookMenu ( bookId ) {
const menu = document . getElementById ( ` book-menu- ${ bookId } ` ) ;
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 = '' ;
// Always-visible "recently read" shelf, independent of folder navigation and the
// read/unread filter below — it's a shortcut back into whatever was open last.
const recent = _recentlyReadBooks ( books ) ;
if ( recent . length ) {
html += '<div class="book-recent-shelf">'
+ '<h3 class="book-recent-title">Zuletzt gelesen</h3>'
+ '<div class="book-recent-row">'
+ recent . map ( b => ` <button class="book-recent-item" onclick="openBook( ${ b . id } )" title=" ${ escapeHtml ( b . title ) } ">
< span class = "book-recent-item-title" > $ { escapeHtml ( b . title ) } < / s p a n >
$ { b . author ? ` <span class="book-recent-item-author"> ${ escapeHtml ( b . author ) } </span> ` : '' }
< / b u t t o n > ` ) . j o i n ( ' ' )
+ '</div></div>' ;
}
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 || '' ,
... 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 ( ) ;
if ( ! data . ok ) throw new Error ( 'Server error' ) ;
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 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
// allowed to briefly touch the server with a plaintext ISBN.
async function lookupBookMetadata ( bookId ) {
const book = _lastBookListData . find ( b => b . id === bookId ) ;
if ( ! book ) return ;
const isbn = ( bookMetaCache [ bookId ] || { } ) . isbn || book . isbn || '' ;
if ( ! isbn ) {
await customAlert ( 'Keine ISBN in den Metadaten dieses Buchs gefunden (nur bei neueren Uploads automatisch erkannt).' ) ;
return ;
}
try {
const res = await fetch ( ` /books/metadata-lookup/?isbn= ${ encodeURIComponent ( isbn ) } ` ) ;
const data = await res . json ( ) ;
if ( ! data . label ) {
await customAlert ( 'Keine Regal-Kategorie gefunden (weder bei der DNB noch bei Open Library).' ) ;
return ;
}
await _updateBookMeta ( bookId , { shelfTag : data . label } ) ;
renderBookList ( _lastBookListData ) ;
} 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 : '' ;
if ( ! pw ) { if ( statusEl ) statusEl . textContent = 'Please enter your password.' ; return ; }
if ( statusEl ) statusEl . textContent = 'Deriving key…' ;
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
if ( statusEl ) statusEl . textContent = '✓ Unlocked' ;
const prompt = $ ( 'enc-key-prompt' ) ;
const uploadArea = $ ( 'book-upload-area' ) ;
if ( prompt ) prompt . style . display = 'none' ;
if ( uploadArea ) uploadArea . style . display = '' ;
loadBookList ( ) ;
} catch ( err ) {
if ( statusEl ) statusEl . textContent = 'Error: ' + err . 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 uploadEbook ( file ) {
const statusEl = $ ( 'book-upload-status' ) ;
const isPdf = /\.pdf$/i . test ( file . name ) ;
const isEpub = /\.epub$/i . test ( file . name ) ;
if ( ! isPdf && ! isEpub ) {
if ( statusEl ) statusEl . textContent = 'Only .epub and .pdf files are supported.' ;
return ;
}
2026-04-04 21:05:51 +02:00
if ( file . size > DIORA _CONFIG . ebookMaxBytes ) {
if ( statusEl ) statusEl . textContent = ` File too large (max ${ DIORA _CONFIG . ebookMaxBytes / 1024 / 1024 } MB). ` ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ;
}
if ( statusEl ) statusEl . textContent = 'Encrypting…' ;
try {
const buf = await file . arrayBuffer ( ) ;
let title = file . name . replace ( /\.(epub|pdf)$/i , '' ) ;
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 ( ) ;
2026-08-17 11:27:16 +02:00
const metaJson = new TextEncoder ( ) . encode ( JSON . stringify ( { title , author , filename : file . name , 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 ) ,
] ) ;
if ( statusEl ) statusEl . textContent = 'Uploading…' ;
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 ) {
if ( statusEl ) statusEl . textContent = ` ✓ " ${ title } " uploaded ` ;
loadBookList ( ) ;
} else {
if ( statusEl ) statusEl . textContent = 'Error: ' + ( data . error || 'upload failed' ) ;
}
} catch ( e ) {
if ( statusEl ) statusEl . textContent = 'Upload failed: ' + e . message ;
}
}
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-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)
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 ( e . target . closest ( 'button, a, input, select, label, #reader-settings-panel, .reader-header, .footnote-popover, #reader-margin, #highlight-popover, .note-bottom-sheet' ) ) return ;
// 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
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) {
const overlay = $ ( 'reader-overlay' ) ;
const contentEl = $ ( 'reader-content' ) ;
const titleEl = $ ( 'reader-title' ) ;
if ( ! overlay || ! contentEl ) return ;
titleEl . textContent = 'Loading…' ;
contentEl . innerHTML = '' ;
overlay . style . display = '' ;
try {
2026-04-05 13:53:38 +02:00
loadReaderSettings ( 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
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 ;
const result = await parseEpub ( plain ) ;
title = result . title || title ;
author = result . author || author ;
toc = result . toc ;
currentImageMap = result . imageMap ;
2026-04-29 11:40:58 +02:00
// Inject chapters progressively so the browser stays responsive on large books
const BATCH = 8 ;
for ( let i = 0 ; i < result . chapters . length ; i += BATCH ) {
const frag = document . createDocumentFragment ( ) ;
const tmp = document . createElement ( 'div' ) ;
for ( let j = i ; j < Math . min ( i + BATCH , result . chapters . length ) ; j ++ ) {
tmp . innerHTML = result . chapters [ j ] ;
while ( tmp . firstChild ) frag . appendChild ( tmp . firstChild ) ;
}
contentEl . appendChild ( frag ) ;
await new Promise ( r => requestAnimationFrame ( r ) ) ;
}
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 ;
// 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.
if ( ! isPdfBook && currentHighlights . length && ! readerAnnotationsMarginOpen ) {
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 ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 . scrollTop = ( pct / 100 ) * contentEl . scrollHeight ;
}
} ) ;
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 ) {
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 . scrollTop = fraction * ( contentEl . scrollHeight - contentEl . clientHeight ) ;
}
}
} 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 ;
} else {
const f = contentEl . scrollTop / ( contentEl . scrollHeight - contentEl . clientHeight || 1 ) ;
2026-08-04 13:51:05 +02:00
progressInput . value = _formatBookPercent ( f ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) ;
} , 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 ) ;
if ( statusEl ) statusEl . textContent = '✓ Key copied to clipboard' ;
setTimeout ( ( ) => { if ( statusEl ) statusEl . textContent = '' ; } , 3000 ) ;
} catch ( e ) {
if ( statusEl ) statusEl . textContent = 'Export failed: ' + e . message ;
}
}
function showImportKey ( ) {
const body = $ ( 'sidebar-body' ) ;
openSidebar ( 'Import encryption key' , `
< p class = "muted" > Paste the key exported from your other browser : < / p >
< 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 >
< button class = "btn" style = "margin-top:8px;" data - import - key - apply > Apply < / b u t t o n >
< p class = "muted" style = "margin-top:8px;" > This replaces the key in this browser . Books uploaded here won ' t be readable until you sync the key back . < / p >
` );
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 ( ) ;
if ( statusEl ) statusEl . textContent = '✓ Key imported — reloading books…' ;
await loadBookList ( ) ;
if ( statusEl ) setTimeout ( ( ) => { statusEl . textContent = '' ; } , 3000 ) ;
} catch ( e ) {
if ( $ ( 'book-key-status' ) ) $ ( 'book-key-status' ) . textContent = 'Import failed: invalid key' ;
}
} ) ;
}
async function deleteBook ( bookId ) {
2026-08-04 09:01:52 +02:00
if ( ! await customConfirm ( 'Delete this book? This cannot be undone.' , { 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 ) ;
}
fraction = Math . min ( 1.0 , Math . max ( 0.0 , fraction ) ) ;
2026-04-01 15:41:30 +02:00
let anchor = '' ;
if ( ! currentPdfDoc ) {
anchor = getPositionAnchor ( contentEl ) ;
_currentPositionAnchor = anchor ;
}
2026-07-10 20:52:27 +02:00
_updateCachedBookProgress ( currentBookId , fraction , anchor , force ) ;
2026-05-29 21:14:40 +02:00
2026-05-31 15:41:35 +02:00
const body = JSON . stringify ( { scroll _fraction : fraction , position _anchor : anchor , ... ( force && { force : true } ) } ) ;
_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 ( ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ) {
openSidebar ( 'Table of Contents' , '<p class="muted">No table of contents found in this book.</p>' ) ;
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>' ;
openSidebar ( 'Table of Contents' , html ) ;
// 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-04-05 13:53:38 +02:00
function loadReaderSettings ( bookId ) {
// Reset to defaults, then apply per-book overrides
Object . assign ( readerSettings , { fontSize : 16 , lineHeight : 1.8 , maxWidth : 65 , theme : 'dark' ,
2026-04-29 11:52:45 +02:00
fontFamily : 'serif' , noBold : false ,
2026-04-05 13:53:38 +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
try {
2026-04-05 13:53:38 +02:00
const saved = JSON . parse ( localStorage . getItem ( ` diora_reader_settings_ ${ 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
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 ) {
localStorage . setItem ( ` diora_reader_settings_ ${ currentBookId } ` , JSON . stringify ( readerSettings ) ) ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 = {
serif : "Georgia, 'Times New Roman', serif" ,
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-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
}
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 = `
2026-08-04 09:01:52 +02:00
< label > Font < 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 > Line < 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 > Width < 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 = " 9 0 " s t e p = " 5 " v a l u e = " $ { r e a d e r S e t t i n g s . m a x W i d t h } " > < 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 >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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" id = "rs-width-full" > Full < / b u t t o n >
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 >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 === 'dark' ? 'active' : ''}" data - rs - theme = "dark" > Dark < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.theme === 'sepia' ? 'active' : ''}" data - rs - theme = "sepia" > Sepia < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.theme === 'bright' ? 'active' : ''}" data - rs - theme = "bright" > Bright < / b u t t o n >
` ;
} 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 >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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.pdfInverted ? 'active' : ''}" id = "rs-invert" > Invert < / b u t t o n >
2026-04-05 13:18:25 +02:00
< button class = "btn btn-sm ${readerSettings.pdfSpread ? 'active' : ''}" id = "rs-spread" > Spread < / 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-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 ) ) ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
} ) ;
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 ;
label = ` Page ${ page } ` ;
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' ;
toast . textContent = ` ★ Bookmarked: ${ label } ` ;
document . body . appendChild ( toast ) ;
setTimeout ( ( ) => toast . remove ( ) , 2200 ) ;
}
function openBookmarksSidebar ( ) {
if ( ! currentBookmarks . length ) {
openSidebar ( 'Bookmarks' , '<p class="muted">No bookmarks yet. Press ★ while reading.</p>' ) ;
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 >
< button class = "btn-icon" data - delete - bookmark = "${escapeHtml(bm.id)}" title = "Delete" > ✕ < / b u t t o n >
< / l i > ` ;
}
html += '</ul>' ;
openSidebar ( 'Bookmarks' , html ) ;
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 = `
< input type = "text" id = "reader-search-input" class = "search-input" placeholder = "Search…" style = "width:160px;" >
< button class = "btn-icon" id = "rs-search-prev" title = "Previous" > ↑ < / b u t t o n >
< button class = "btn-icon" id = "rs-search-next" title = "Next" > ↓ < / b u t t o n >
< 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 = `
< button class = "hl-color-btn" data - hl - color = "yellow" style = "background:#f1c40f" title = "Yellow" > A < / b u t t o n >
< button class = "hl-color-btn" data - hl - color = "green" style = "background:#2ecc71" title = "Green" > A < / b u t t o n >
< button class = "hl-color-btn" data - hl - color = "blue" style = "background:#3498db" title = "Blue" > A < / b u t t o n >
< button class = "hl-color-btn" data - hl - color = "red" style = "background:#e63946" title = "Red" > A < / b u t t o n >
< button class = "hl-note-btn" title = "Add note" > ✎ < / b u t t o n >
` ;
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' ;
const noteText = h ? . note ? escapeHtml ( h . note ) : '<span class="muted">No note</span>' ;
popover . innerHTML = `
< div style = "font-size:12px;padding-bottom:4px;" > $ { noteText } < / d i v >
< div style = "display:flex;gap:6px;" >
< button class = "btn btn-sm" data - hl - edit - note = "${escapeHtml(h?.id || '')}" > Edit note < / b u t t o n >
< button class = "btn btn-sm btn-danger" data - hl - delete = "${escapeHtml(h?.id || '')}" > Delete < / b u t t o n >
< / 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 ( ) ;
if ( ! ct ) { await customAlert ( 'No highlights or notes in this book yet.' ) ; return ; }
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , iv , ct ) ;
const highlights = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
if ( ! highlights . length ) { await customAlert ( 'No highlights or notes in this book yet.' ) ; return ; }
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 ) ;
} ) ;
const bookTitle = bookMetaCache [ bookId ] ? . title || ` Book # ${ bookId } ` ;
const lines = [ ` ${ bookTitle } — Annotations ` , ` Exported: ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , '' ] ;
let lastChapter = null ;
for ( const h of highlights ) {
const chapterTitle = h . anchor ? . chapterSrc || '' ;
if ( chapterTitle !== lastChapter ) {
lines . push ( ` ── ${ chapterTitle || 'Untitled section' } ── ` ) ;
lastChapter = chapterTitle ;
}
if ( h . type === 'note' ) {
lines . push ( ` [Note] near: " ${ ( h . anchor ? . quote || '' ) . trim ( ) } " ` ) ;
if ( h . note ) lines . push ( ` ${ h . note } ` ) ;
} else {
lines . push ( ` [Highlight · ${ h . color || 'yellow' } ] " ${ ( h . anchor ? . quote || '' ) . trim ( ) } " ` ) ;
if ( h . note ) lines . push ( ` Note: ${ h . note } ` ) ;
}
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 ;
a . download = ` ${ bookTitle . replace ( /[^\w\-]+/g , '_' ) } -annotations.txt ` ;
document . body . appendChild ( a ) ;
a . click ( ) ;
a . remove ( ) ;
URL . revokeObjectURL ( url ) ;
} catch ( e ) {
await customAlert ( 'Export failed: ' + e . message ) ;
} 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' ) ;
if ( ! contentEl || ! range || ! contentEl . scrollHeight ) return null ;
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 ;
}
function exportAnnotations ( ) {
if ( ! currentHighlights . length ) {
customAlert ( 'No highlights or notes in this book yet.' ) ;
return ;
}
const resolved = _sortedAnnotations ( ) ;
2026-08-04 10:17:01 +02:00
const titleEl = $ ( 'reader-title' ) ;
const bookTitle = ( titleEl ? . textContent || 'Book' ) . split ( ' — ' ) [ 0 ] ;
const lines = [ ` ${ bookTitle } — Annotations ` , ` Exported: ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } ` , '' ] ;
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 ) {
lines . push ( ` ── ${ chapterTitle || 'Untitled section' } ── ` ) ;
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' ) {
2026-08-04 11:10:56 +02:00
lines . push ( ` [Note] ${ coord } near: " ${ ( h . anchor ? . quote || '' ) . trim ( ) } " ` ) ;
2026-08-04 10:17:01 +02:00
if ( h . note ) lines . push ( ` ${ h . note } ` ) ;
} else {
2026-08-04 11:10:56 +02:00
lines . push ( ` [Highlight · ${ h . color || 'yellow' } ] ${ coord } " ${ ( h . anchor ? . quote || '' ) . trim ( ) } " ` ) ;
2026-08-04 10:17:01 +02:00
if ( h . note ) lines . push ( ` Note: ${ h . note } ` ) ;
}
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 ;
a . download = ` ${ bookTitle . replace ( /[^\w\-]+/g , '_' ) } -annotations.txt ` ;
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 ) {
openSidebar ( 'Highlights & Notes' , '<p class="muted">No highlights or notes yet.</p>' ) ;
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 ) } % ` : '' ;
2026-08-04 11:10:56 +02:00
const kind = h . type === 'note' ? 'Note' : ` Highlight ${ h . color ? ' · ' + h . color : '' } ` ;
const preview = h . note || h . anchor ? . quote || '(no text)' ;
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 >
< button class = "btn-icon" data - delete - annotation = "${escapeHtml(h.id)}" title = "Delete" > ✕ < / b u t t o n >
< / l i > ` ;
}
html += '</ul>' ;
openSidebar ( 'Highlights & Notes' , html ) ;
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 ;
const container = targetEl . closest ( '.footnote' ) || targetEl . parentElement ;
if ( ! container ) return ;
const clone = container . cloneNode ( true ) ;
clone . querySelector ( 'sup' ) ? . remove ( ) ;
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 ( ) {
const stationName = currentStation ? escapeHtml ( currentStation . name ) : '— no station —' ;
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 ( '' )
2026-04-05 14:22:07 +02:00
: ` <li class="muted">No saved stations.</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" >
2026-04-05 17:32:05 +02:00
< button class = "btn ${isPlaying ? 'playing' : ''}" id = "rsb-playstop" > $ { isPlaying ? '⏹ Stop' : '▶ Play' } < / b u t t o n >
< label class = "rsb-vol" > vol < 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.
}
// Seed podcast feeds from server context
if ( typeof INITIAL _PODCAST _FEEDS !== 'undefined' && Array . isArray ( INITIAL _PODCAST _FEEDS ) ) {
podcastFeeds = INITIAL _PODCAST _FEEDS ;
}
// Wire seek slider
const seekSlider = $ ( 'seek-slider' ) ;
if ( seekSlider ) {
seekSlider . addEventListener ( 'input' , function ( ) {
if ( podcastMode ) audio . currentTime = parseInt ( this . value , 10 ) ;
} ) ;
}
// 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 ) ;
}
// 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 ) ;
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
// Hourly background feed refresh (only when authenticated)
if ( IS _AUTHENTICATED ) {
setInterval ( refreshAllFeeds , 60 * 60 * 1000 ) ;
}
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
} ) ( ) ;