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 , '"' ) ;
}
// ---------------------------------------------------------------------------
// 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 ) {
alert ( 'Please log in to save stations.' ) ;
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 = [
{ label : '🎯 Focus' , tag : 'ambient' } ,
{ label : '☕ Lo-fi' , tag : 'lofi' } ,
{ label : '🎷 Jazz' , tag : 'jazz' } ,
{ label : '🎻 Classical' , tag : 'classical' } ,
2026-03-16 21:58:32 +01:00
{ label : '🌧 Ambient' , tag : 'ambient' } ,
2026-03-16 19:19:22 +01:00
{ label : '🤘 Metal' , tag : 'metal' } ,
{ label : '🎉 Electronic' , tag : 'electronic' } ,
{ label : '📻 Talk' , tag : 'talk' } ,
] ;
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' ,
label : '🎯 Focus' ,
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' ,
label : '☕ Lo-fi / Chill' ,
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' ,
label : '🌑 Dark / Industrial' ,
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' ,
label : '🎻 Classical' ,
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 ;
const existing = document . getElementById ( 'donation-hint' ) ;
if ( existing ) existing . remove ( ) ;
const el = document . createElement ( 'div' ) ;
el . id = 'donation-hint' ;
el . innerHTML = `
< 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 >
< 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
// ---------------------------------------------------------------------------
function editNotes ( pk , current ) {
const note = prompt ( 'Station note:' , current || '' ) ;
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-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 ) ;
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 ) ;
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' ;
} ) ;
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
function addFeedByUrl ( ) {
const url = prompt ( 'RSS feed URL:' ) ;
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 >
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.auto_queue ? 'active' : ''}" onclick = "toggleFeedAutoQueue(${feed.id}, this)" title = "${feed.auto_queue ? 'Auto-queue ON' : 'Auto-queue new episodes'}" > ⚡ 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 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 >
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 = "openEpisodeSidebar(${ep.id})" title = "Show 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 ) {
if ( ! confirm ( 'Remove this podcast?' ) ) return ;
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 ) ) {
alert ( 'Cache API not supported in this browser.' ) ;
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 ; }
alert ( 'Download failed: ' + e . message ) ;
}
}
// ---------------------------------------------------------------------------
// 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 small = Math . round ( contentEl . clientHeight * 0.08 ) ;
const large = Math . round ( contentEl . clientHeight * 0.85 ) ;
if ( e . key === 'j' ) { e . preventDefault ( ) ; contentEl . scrollBy ( { top : small , behavior : 'smooth' } ) ; }
if ( e . key === 'k' ) { e . preventDefault ( ) ; contentEl . scrollBy ( { top : - small , behavior : 'smooth' } ) ; }
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
}
} ) ;
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' , ( ) => {
navigator . serviceWorker . register ( '/static/js/sw.js' ) . catch ( err => {
console . warn ( 'Service worker registration failed:' , err ) ;
} ) ;
} ) ;
}
// ---------------------------------------------------------------------------
// 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 ) ) {
alert ( 'Only JPEG, PNG, or WebP images are allowed.' ) ;
return ;
}
2026-04-04 21:05:51 +02:00
if ( file . size > DIORA _CONFIG . bgMaxBytes ) {
alert ( ` 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' ) || '' ,
} ;
} ) ;
// 4. Build image map: abs zip path → blob URL
const imageMap = { } ;
for ( const { href , mediaType } of Object . values ( manifest ) ) {
if ( mediaType . startsWith ( 'image/' ) ) {
try {
const buf = await zip . file ( href ) . async ( 'arraybuffer' ) ;
imageMap [ href ] = URL . createObjectURL ( new Blob ( [ buf ] , { type : mediaType } ) ) ;
} catch ( e ) { /* missing asset */ }
}
}
// 5. Parse TOC
const toc = await _parseEpubToc ( zip , opfDoc , manifest ) ;
// 6. Get spine and concatenate chapters
const spineItems = Array . from ( opfDoc . querySelectorAll ( 'spine > itemref' ) )
. map ( ref => manifest [ ref . getAttribute ( 'idref' ) ] ? . href )
. filter ( Boolean ) ;
const parts = [ ] ;
for ( let i = 0 ; i < spineItems . length ; i ++ ) {
const href = spineItems [ i ] ;
try {
const chapterText = await zip . file ( href ) . async ( 'text' ) ;
const chapterDir = href . includes ( '/' ) ? href . substring ( 0 , href . lastIndexOf ( '/' ) + 1 ) : '' ;
const withBlobs = _injectImageBlobs ( chapterText , chapterDir , imageMap ) ;
const sanitized = sanitizeEpubHtml ( withBlobs ) ;
parts . push ( ` <div id="epub-chapter- ${ i } " data-epub-src=" ${ href } "> ${ sanitized } </div> ` ) ;
} catch ( e ) { /* skip missing */ }
}
return { title , author , html : parts . join ( '\n' ) , toc , imageMap } ;
}
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 ;
}
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 => {
if ( attr . name . startsWith ( 'on' ) ) el . removeAttribute ( attr . name ) ;
} ) ;
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' ) ;
} 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 = '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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-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-04-01 16:10:03 +02:00
// ---------------------------------------------------------------------------
// Book cache — IndexedDB, evict after 4 weeks of inactivity
// ---------------------------------------------------------------------------
const _BOOK _CACHE _STORE = 'books' ;
const _BOOK _CACHE _EVICT _MS = 4 * 7 * 24 * 60 * 60 * 1000 ;
function _openBookCacheDb ( ) {
return new Promise ( ( resolve , reject ) => {
const req = indexedDB . open ( 'diora_book_cache' , 1 ) ;
req . onupgradeneeded = e => e . target . result . createObjectStore ( _BOOK _CACHE _STORE , { keyPath : 'id' } ) ;
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 ;
} ) ;
} catch { }
}
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 { }
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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-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-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 ) ) ;
bookMetaCache [ b . id ] = { title : meta . title || '?' , author : meta . author || '' , type : meta . type || 'epub' } ;
2026-03-20 20:41:13 +01:00
decrypted . push ( { id : b . id , title : meta . title || '?' , author : meta . author || '' , type : meta . type || 'epub' , scroll _fraction : b . scroll _fraction , uploaded _at : b . uploaded _at , last _read : b . last _read || null , keyOk : true } ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
} catch ( e ) {
bookMetaCache [ b . id ] = { title : ` Book # ${ b . id } ` , author : '' , type : 'epub' } ;
2026-03-20 20:41:13 +01:00
decrypted . push ( { id : b . id , title : ` Book # ${ b . id } ` , author : '' , type : 'epub' , scroll _fraction : b . scroll _fraction , uploaded _at : b . uploaded _at , last _read : b . last _read || null , keyOk : 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
}
}
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 ) ;
} ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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-03-20 09:45:39 +01:00
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
}
}
function renderBookList ( books ) {
const listEl = $ ( 'book-list' ) ;
if ( ! listEl ) return ;
let html = '' ;
for ( const b of books ) {
const pct = Math . round ( ( b . scroll _fraction || 0 ) * 100 ) ;
2026-03-20 09:45:39 +01:00
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>' : '' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
html += ` <div class="book-item">
< div class = "book-item-info" >
2026-03-20 09:45:39 +01:00
< strong class = "book-title" > $ { escapeHtml ( b . title ) } $ { keyWarning } < / 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 >
$ { pct > 0 ? ` <span class="muted book-progress"> ${ pct } % read</span> ` : '' }
< / 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 >
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 = "deleteBook(${b.id})" > Delete < / b u t t o n >
< / d i v >
< / d i v > ` ;
}
listEl . innerHTML = html ;
}
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 = '' ;
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 ( ) || '' ;
}
} catch ( e ) { /* use filename as title */ }
}
const key = await getOrCreateEncKey ( ) ;
const metaJson = new TextEncoder ( ) . encode ( JSON . stringify ( { title , author , filename : file . name , type } ) ) ;
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 ;
}
async function renderPdf ( arrayBuffer , contentEl , scaleOverride ) {
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
// Viewport wrapper: CSS zoom controls display scale without re-rendering
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 ) ;
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 } ) ;
2026-04-05 13:18:25 +02:00
const pageWidth = readerSettings . pdfSpread ? ( containerWidth - 8 ) / 2 : containerWidth ;
2026-04-05 16:14:24 +02:00
// Zoom baked into canvas resolution — no CSS zoom, stays sharp at any DPR
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 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 } ` ;
// Inner container gives canvas + text layer a shared position:relative origin,
// independent of the outer flex wrapper's centering.
const inner = document . createElement ( 'div' ) ;
inner . className = 'pdf-page-inner' ;
2026-04-01 15:48:44 +02:00
const dpr = window . devicePixelRatio || 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 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-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
// Text layer disabled — re-enable once overlay rendering is resolved
}
pdfTotalPages = pdf . numPages ;
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 ) {
// Ignore taps on interactive elements (buttons, links, inputs, settings panel)
if ( e . target . closest ( 'button, a, input, select, label, #reader-settings-panel, .reader-header' ) ) return ;
_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 {
const res = await fetch ( ` /books/ ${ bookId } /data/ ` ) ;
( { 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:08:44 +02:00
contentEl . innerHTML = '<div class="pdf-loading-overlay"><span class="pdf-loading-spinner"></span></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
const result = await renderPdf ( plain , contentEl ) ;
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 ;
contentEl . innerHTML = result . html ;
}
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-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 . min = 1 ;
progressInput . max = numPages ;
progressInput . value = 1 ;
if ( progressSuffix ) progressSuffix . textContent = ` / ${ numPages } ` ;
} else {
progressInput . min = 0 ;
progressInput . max = 100 ;
progressInput . value = 0 ;
if ( progressSuffix ) progressSuffix . textContent = '%' ;
}
progressInput . addEventListener ( 'change' , function ( ) {
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 {
const pct = Math . min ( 100 , Math . max ( 0 , parseInt ( this . value , 10 ) || 0 ) ) ;
this . value = pct ;
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
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 progressRes = await fetch ( '/books/' ) ;
const allBooks = await progressRes . json ( ) ;
const bookData = allBooks . find ( b => b . id === bookId ) ;
const fraction = bookData ? ( bookData . scroll _fraction || 0 ) : 0 ;
2026-04-01 15:41:30 +02:00
const anchor = bookData ? ( bookData . position _anchor || '' ) : '' ;
_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 ) { }
// 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 ) ;
progressInput . value = Math . round ( f * 100 ) ;
}
} ) ;
// 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-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 ) {
requestAnimationFrame ( ( ) => restoreFromAnchor ( contentEl , _currentPositionAnchor ) ) ;
}
} ) ;
_resizeObserver . observe ( contentEl ) ;
}
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-03-20 09:45:39 +01:00
overlay . style . display = 'none' ;
alert ( ` Failed to open book: ${ e . message } ` ) ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
}
}
async function 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 ) {
if ( ! confirm ( 'Delete this book? This cannot be undone.' ) ) return ;
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 ) { }
}
async function saveReaderProgress ( ) {
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 ;
}
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
// Cache for sendBeacon on unload
_lastProgressBeacon = {
url : ` /books/ ${ currentBookId } /progress/ ` ,
2026-04-01 15:41:30 +02:00
body : JSON . stringify ( { scroll _fraction : fraction , position _anchor : anchor } ) ,
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text 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 ( ) } ,
body : _lastProgressBeacon . body ,
} ) ;
} catch ( e ) { }
}
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
// 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 = [ ] ;
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 ( ) ;
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' ,
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-01 15:41:30 +02:00
if ( _currentPositionAnchor && currentBookId ) {
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' ) ;
}
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 = `
< label > Font < input type = "range" id = "rs-font" min = "12" max = "24" step = "1" value = "${readerSettings.fontSize}" > < span id = "rs-font-val" > $ { readerSettings . fontSize } px < / s p a n > < / l a b e l >
< label > Line < input type = "range" id = "rs-line" min = "12" max = "30" step = "1" value = "${Math.round(readerSettings.lineHeight * 10)}" > < span id = "rs-line-val" > $ { readerSettings . lineHeight } < / s p a n > < / l a b e l >
< label > Width < input type = "range" id = "rs-width" min = "40" max = "90" step = "5" value = "${readerSettings.maxWidth}" > < span id = "rs-width-val" > $ { readerSettings . maxWidth } ch < / s p a n > < / l a b e l >
< button class = "btn btn-sm" id = "rs-width-full" > Full < / b u t t o n >
< 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
` ;
}
overlay . insertBefore ( panel , contentEl ) ;
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 ( ) ;
} ) ;
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 ( ) ;
} ) ;
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 ( ) ;
} ) ;
panel . querySelector ( '#rs-width-full' ) . addEventListener ( 'click' , ( ) => {
readerSettings . maxWidth = 999 ;
widthRange . value = 90 ;
widthVal . textContent = 'full' ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
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
const contentEl2 = $ ( 'reader-content' ) ;
const fraction = contentEl2
? contentEl2 . scrollTop / ( contentEl2 . scrollHeight - contentEl2 . clientHeight || 1 )
: 0 ;
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 13:18:25 +02:00
if ( contentEl2 && fraction > 0 ) {
2026-04-05 16:14:24 +02:00
contentEl2 . scrollTop = fraction * ( contentEl2 . scrollHeight - contentEl2 . clientHeight ) ;
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
}
}
async function reRenderPdf ( ) {
if ( ! currentPdfBuffer ) return ;
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl ) return ;
await renderPdf ( currentPdfBuffer , contentEl ) ;
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 ) {
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , iv , ct ) ;
currentBookmarks = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
} else {
currentBookmarks = [ ] ;
}
} catch ( e ) {
currentBookmarks = [ ] ;
}
}
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 } ;
await fetch ( url , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body ,
} ) ;
bookmarksDirty = false ;
} catch ( e ) { }
}
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 ( ) ;
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 >
` ;
overlay . insertBefore ( bar , contentEl ) ;
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 ;
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 ) {
const key = await getOrCreateEncKey ( ) ;
const plain = await decryptBytes ( key , iv , ct ) ;
currentHighlights = JSON . parse ( new TextDecoder ( ) . decode ( plain ) ) ;
} else {
currentHighlights = [ ] ;
}
applyHighlightsToContent ( ) ;
} catch ( e ) {
currentHighlights = [ ] ;
}
}
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 } ;
await fetch ( url , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body ,
} ) ;
highlightsDirty = false ;
} catch ( e ) { }
}
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 ) { }
}
}
function renderHighlight ( h ) {
const contentEl = $ ( 'reader-content' ) ;
if ( ! contentEl || ! h . anchor ) return ;
const chapterEl = contentEl . querySelector ( ` [data-epub-src=" ${ CSS . escape ( h . anchor . chapterSrc || '' ) } "] ` )
|| contentEl ;
let range = null ;
try {
const startNode = xpathToNode ( h . anchor . startXpath , chapterEl ) ;
const endNode = xpathToNode ( h . anchor . endXpath , chapterEl ) ;
if ( startNode && endNode ) {
range = document . createRange ( ) ;
range . setStart ( startNode , h . anchor . startOffset ) ;
range . setEnd ( endNode , h . anchor . endOffset ) ;
}
} catch ( e ) { }
// Fallback: quote substring search
if ( ! range && h . anchor . quote ) {
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 ;
}
}
}
if ( ! range ) return ;
try {
const mark = document . createElement ( 'mark' ) ;
mark . className = 'epub-highlight' ;
mark . dataset . highlightId = h . id ;
mark . dataset . color = h . color || 'yellow' ;
range . surroundContents ( mark ) ;
} catch ( e ) { }
}
function xpathToNode ( xpath , root ) {
if ( ! xpath ) return null ;
const result = document . evaluate ( xpath , root , null , XPathResult . FIRST _ORDERED _NODE _TYPE , null ) ;
return result . singleNodeValue ;
}
function getXPathForNode ( node , root ) {
const parts = [ ] ;
let current = node ;
while ( current && current !== root ) {
const parent = current . parentNode ;
if ( ! parent ) break ;
if ( current . nodeType === Node . TEXT _NODE ) {
const siblings = Array . from ( parent . childNodes ) . filter ( n => n . nodeType === Node . TEXT _NODE ) ;
const idx = siblings . indexOf ( current ) ;
parts . unshift ( ` text()[ ${ idx + 1 } ] ` ) ;
} else {
const siblings = Array . from ( parent . children ) . filter ( n => n . tagName === current . tagName ) ;
const idx = siblings . indexOf ( current ) ;
parts . unshift ( ` ${ current . tagName . toLowerCase ( ) } [ ${ idx + 1 } ] ` ) ;
}
current = parent ;
}
return parts . join ( '/' ) ;
}
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]' ) ;
const root = chapterEl || contentEl ;
return {
type : 'epub' ,
chapterSrc : chapterEl ? . getAttribute ( 'data-epub-src' ) || '' ,
startXpath : getXPathForNode ( range . startContainer , root ) ,
startOffset : range . startOffset ,
endXpath : getXPathForNode ( range . endContainer , root ) ,
endOffset : range . endOffset ,
quote : range . toString ( ) . slice ( 0 , 200 ) ,
} ;
}
function handleReaderSelection ( e ) {
// If clicking an existing highlight, show tooltip
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 ;
}
dismissHighlightPopover ( ) ;
const sel = window . getSelection ( ) ;
if ( ! sel || sel . isCollapsed || ! sel . rangeCount ) return ;
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 ) ;
}
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 >
` ;
popover . style . top = ( rect . top + window . scrollY - 44 ) + 'px' ;
popover . style . left = ( rect . left + window . scrollX + rect . width / 2 - 70 ) + 'px' ;
document . body . appendChild ( popover ) ;
currentHighlightPopover = popover ;
// 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 >
` ;
popover . style . top = ( rect . bottom + window . scrollY + 4 ) + 'px' ;
popover . style . left = ( rect . left + window . scrollX ) + 'px' ;
document . body . appendChild ( popover ) ;
currentHighlightPopover = popover ;
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 ( ) ;
openNoteEditor ( h ) ;
}
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 ( ) ,
anchor ,
color ,
note : '' ,
createdAt : new Date ( ) . toISOString ( ) ,
} ;
currentHighlights . push ( h ) ;
highlightsDirty = true ;
window . getSelection ( ) ? . removeAllRanges ( ) ;
dismissHighlightPopover ( ) ;
renderHighlight ( h ) ;
debounceSaveHighlights ( ) ;
}
function createHighlightWithNote ( range ) {
const anchor = buildEpubAnchor ( range ) ;
const h = {
id : crypto . randomUUID ( ) ,
anchor ,
color : 'yellow' ,
note : '' ,
createdAt : new Date ( ) . toISOString ( ) ,
} ;
currentHighlights . push ( h ) ;
highlightsDirty = true ;
window . getSelection ( ) ? . removeAllRanges ( ) ;
dismissHighlightPopover ( ) ;
renderHighlight ( h ) ;
openNoteEditor ( h ) ;
}
function openNoteEditor ( h ) {
openSidebar ( 'Edit note' , `
< textarea id = "hl-note-input" class = "search-input" rows = "5" style = "width:100%;resize:vertical;" > $ { escapeHtml ( h . note || '' ) } < / t e x t a r e a >
< button class = "btn" style = "margin-top:8px;" data - save - note = "${escapeHtml(h.id)}" > Save note < / b u t t o n >
` );
const body = $ ( 'sidebar-body' ) ;
body . addEventListener ( 'click' , function _noteClick ( e ) {
const btn = e . target . closest ( '[data-save-note]' ) ;
if ( ! btn ) return ;
body . removeEventListener ( 'click' , _noteClick ) ;
const text = ( body . querySelector ( '#hl-note-input' ) ? . value || '' ) . trim ( ) ;
h . note = text ;
highlightsDirty = true ;
debounceSaveHighlights ( ) ;
closeSidebar ( ) ;
} ) ;
}
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 ) ;
}
}
debounceSaveHighlights ( ) ;
}
function dismissHighlightPopover ( ) {
if ( currentHighlightPopover ) {
currentHighlightPopover . remove ( ) ;
currentHighlightPopover = null ;
}
}
// ---------------------------------------------------------------------------
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 ( ) ;
// Restore last active tab
const savedTab = localStorage . getItem ( 'diora_active_tab' ) || 'radio' ;
2026-03-19 19:43:23 +01:00
const savedRadioTab = localStorage . getItem ( 'diora_active_radio_tab' ) || 'saved' ;
Add ebook reader features: highlights, bookmarks, search, settings, PDF paginated mode
- Backend: EBookHighlights and EBookBookmarks models with encrypted blob storage;
GET/POST views with size guards (700 KB / 100 KB); migration applied
- Reader header: search, settings, bookmark add/list buttons
- Font & layout settings panel (font size, line height, max width, themes for EPUB;
zoom, invert, paginated for PDF); persisted in localStorage
- Bookmarks: encrypted per-book blob, toast on add, sidebar with jump/delete
- Full-text search: EPUB TreeWalker mark injection, PDF span search; Ctrl+F / F3;
arrow key cycling; highlights re-applied on search clear
- PDF paginated mode: single-page view, tap-zone / swipe / arrow key navigation,
smart zoom (text bounding box → scale+translate canvas), auto-enable on mobile
- Progress tracking fixes: save before hiding overlay (was always writing 100%),
wait for EPUB images to load before restoring scroll, PDF paginated uses page
fraction, sendBeacon cache for unload/visibilitychange reliability
- PDF text layer disabled pending overlay rendering fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 13:08:42 +01:00
showTab ( 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
// Hourly background feed refresh (only when authenticated)
if ( IS _AUTHENTICATED ) {
setInterval ( refreshAllFeeds , 60 * 60 * 1000 ) ;
}
2026-03-16 19:19:22 +01:00
} ) ( ) ;