@ -157,7 +157,9 @@ function customPrompt(message, defaultValue = '') {
function playStation ( url , name , stationId ) {
stopPlayback ( false ) ;
if ( location . protocol === 'https:' && url . startsWith ( 'http://' ) ) {
// A secure page can't load plain-http streams (mixed content). Native shells
// that play the stream themselves set window.DIORA_PLAYS_HTTP_STREAMS.
if ( location . protocol === 'https:' && url . startsWith ( 'http://' ) && ! window . DIORA _PLAYS _HTTP _STREAMS ) {
window . open ( url , '_blank' ) ;
return ;
}
@ -280,15 +282,18 @@ async function startPlaySession(stationName, stationUrl) {
}
async function stopPlaySession ( ) {
if ( ! currentPlayId ) return ;
// Take the id before awaiting: playStation() calls this without waiting and
// starts the next session right away, whose id must not be cleared here.
const playId = currentPlayId ;
if ( ! playId ) return ;
currentPlayId = null ;
try {
await fetch ( '/radio/play/stop/' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'X-CSRFToken' : getCsrfToken ( ) } ,
body : JSON . stringify ( { play _id : currentP layId} )
body : JSON . stringify ( { play _id : p layId} )
} ) ;
} catch ( e ) { }
currentPlayId = null ;
}
window . addEventListener ( 'beforeunload' , ( ) => {
@ -3254,7 +3259,7 @@ async function _syncPendingAnnotations() {
// Reader settings
let readerSettings = { fontSize : 16 , lineHeight : 1.8 , maxWidth : 65 , theme : 'dark' ,
fontFamily : 'serif' , noBold : false ,
fontFamily : 'serif' , noBold : false , fontWeight : null ,
pdfZoom : 100 , pdfInverted : false , pdfPaginated : false , pdfSpread : false } ;
let readerSettingsPanelOpen = false ;
let currentPdfDoc = null ;
@ -4505,7 +4510,30 @@ function exitReaderImmersiveMode() {
document . removeEventListener ( 'click' , _immHandleTap ) ;
}
// Serializes openBook: it empties #reader-content, then appends the chapters
// across several awaits. A second call in between (a second tap while a slow
// e-reader is still decrypting) appended the whole book a second time — and
// progress saved from that doubled DOM had block indices beyond the real book,
// which then out-ranked every real position on the server.
let _openBookInFlight = null ;
let _repairProgressAnchor = false ; // stored anchor didn't fit this book
async function openBook ( bookId ) {
if ( _openBookInFlight ) {
if ( _openBookInFlight . bookId === bookId ) return _openBookInFlight . promise ;
await _openBookInFlight . promise . catch ( ( ) => { } ) ;
}
const promise = _openBookOnce ( bookId ) ;
_openBookInFlight = { bookId , promise } ;
try {
return await promise ;
} finally {
if ( _openBookInFlight && _openBookInFlight . promise === promise ) _openBookInFlight = null ;
}
}
async function _openBookOnce ( bookId ) {
_repairProgressAnchor = false ;
const overlay = $ ( 'reader-overlay' ) ;
const contentEl = $ ( 'reader-content' ) ;
const titleEl = $ ( 'reader-title' ) ;
@ -4690,7 +4718,14 @@ async function openBook(bookId) {
}
await new Promise ( r => requestAnimationFrame ( r ) ) ;
if ( ! restoreFromAnchor ( contentEl , anchor ) && fraction > 0 ) {
contentEl . scrollTop = fraction * ( contentEl . scrollHeight - contentEl . clientHeight ) ;
restoreFromAnchor ( contentEl , _anchorForFraction ( contentEl , fraction ) ) ;
}
// An anchor past the end of this book can only come from a broken
// render. The server keeps the furthest block index, so it would block
// every real position from now on: overwrite it with the next save.
const [ storedBlock ] = _anchorParts ( anchor ) ;
if ( storedBlock >= contentEl . querySelectorAll ( EPUB _BLOCK _SELECTOR ) . length ) {
_repairProgressAnchor = true ;
}
}
} catch ( e ) { }
@ -4844,6 +4879,13 @@ async function saveReaderProgress(force = false) {
fraction = contentEl . scrollTop / ( contentEl . scrollHeight - contentEl . clientHeight || 1 ) ;
}
// Chapter wrappers carry unique ids; seeing one twice means the book was
// rendered twice. Positions from that DOM are wrong — don't store them.
if ( ! currentPdfDoc && contentEl . querySelectorAll ( '#epub-chapter-0' ) . length > 1 ) {
console . warn ( 'Book rendered twice, progress not saved' ) ;
return ;
}
let anchor = '' ;
if ( ! currentPdfDoc ) {
anchor = getPositionAnchor ( contentEl ) ;
@ -4854,9 +4896,11 @@ async function saveReaderProgress(force = false) {
}
fraction = Math . min ( 1.0 , Math . max ( 0.0 , fraction ) ) ;
_updateCachedBookProgress ( currentBookId , fraction , anchor , force ) ;
_updateCachedBookProgress ( currentBookId , fraction , anchor , force || ( _repairProgressAnchor && ! ! anchor ) ) ;
const body = JSON . stringify ( { scroll _fraction : fraction , position _anchor : anchor , ... ( force && { force : true } ) } ) ;
const repair = _repairProgressAnchor && ! ! anchor ;
if ( repair ) _repairProgressAnchor = false ;
const body = JSON . stringify ( { scroll _fraction : fraction , position _anchor : anchor , ... ( ( force || repair ) && { force : true } ) } ) ;
_lastProgressBeacon = { url : ` /books/ ${ currentBookId } /progress/ ` , body } ;
try {
@ -5026,7 +5070,7 @@ function jumpToTocEntry(href) {
function loadReaderSettings ( bookId ) {
// Reset to defaults, then apply per-book overrides
Object . assign ( readerSettings , { fontSize : 16 , lineHeight : 1.8 , maxWidth : 65 , theme : 'dark' ,
fontFamily : 'serif' , noBold : false ,
fontFamily : 'serif' , noBold : false , fontWeight : null ,
pdfZoom : 100 , pdfInverted : false , pdfPaginated : false , pdfSpread : false } ) ;
try {
const saved = JSON . parse ( localStorage . getItem ( ` diora_reader_settings_ ${ bookId } ` ) || '{}' ) ;
@ -5060,6 +5104,16 @@ function applyReaderSettings(isPdf) {
} ;
contentEl . style . fontFamily = fontMap [ readerSettings . fontFamily ] || fontMap . serif ;
contentEl . classList . toggle ( 'reader-no-bold' , ! ! readerSettings . noBold ) ;
// Weight: null keeps the stylesheet's. --reader-weight lets old WebViews
// without variable-font support (which drop weights like 425) pick a
// static cut instead.
contentEl . style . fontWeight = '' ;
if ( readerSettings . fontWeight ) {
contentEl . style . fontWeight = String ( readerSettings . fontWeight ) ;
contentEl . style . setProperty ( '--reader-weight' , String ( readerSettings . fontWeight ) ) ;
} else {
contentEl . style . removeProperty ( '--reader-weight' ) ;
}
if ( _currentPositionAnchor && currentBookId ) {
_suppressScrollJumpDetect ( ) ;
requestAnimationFrame ( ( ) => restoreFromAnchor ( $ ( 'reader-content' ) , _currentPositionAnchor ) ) ;
@ -5083,6 +5137,19 @@ function applyReaderSettings(isPdf) {
if ( isPdf && markerModeActive ) toggleMarkerMode ( ) ;
}
// Font weight slider: the variable Source Serif 4 takes any weight, the
// system fonts only their fixed cuts (multiples of 100).
function _readerWeightStep ( ) {
return readerSettings . fontFamily === 'serif' ? 25 : 100 ;
}
function _readerWeight ( ) {
const step = _readerWeightStep ( ) ;
const w = readerSettings . fontWeight
|| parseInt ( getComputedStyle ( $ ( 'reader-content' ) ) . fontWeight , 10 ) || 400 ;
return Math . round ( w / step ) * step ;
}
function toggleSettingsPanel ( ) {
const overlay = $ ( 'reader-overlay' ) ;
const contentEl = $ ( 'reader-content' ) ;
@ -5108,6 +5175,7 @@ function toggleSettingsPanel() {
< label > Line < button class = "btn btn-sm" id = "rs-line-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - l i n e " m i n = " 1 2 " m a x = " 3 0 " s t e p = " 1 " v a l u e = " $ { M a t h . r o u n d ( r e a d e r S e t t i n g s . l i n e H e i g h t * 1 0 ) } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - l i n e - p l u s " > + < / b u t t o n > < s p a n i d = " r s - l i n e - v a l " > $ { r e a d e r S e t t i n g s . l i n e H e i g h t } < / s p a n > < / l a b e l >
< label > Width < button class = "btn btn-sm" id = "rs-width-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - w i d t h " m i n = " 4 0 " m a x = " 9 0 " s t e p = " 5 " v a l u e = " $ { r e a d e r S e t t i n g s . m a x W i d t h } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - w i d t h - p l u s " > + < / b u t t o n > < s p a n i d = " r s - w i d t h - v a l " > $ { r e a d e r S e t t i n g s . m a x W i d t h } c h < / s p a n > < / l a b e l >
< button class = "btn btn-sm" id = "rs-width-full" > Full < / b u t t o n >
< label > Weight < button class = "btn btn-sm" id = "rs-weight-minus" > − < / b u t t o n > < i n p u t t y p e = " r a n g e " i d = " r s - w e i g h t " m i n = " 2 0 0 " m a x = " 8 0 0 " s t e p = " $ { _ r e a d e r W e i g h t S t e p ( ) } " v a l u e = " $ { _ r e a d e r W e i g h t ( ) } " > < b u t t o n c l a s s = " b t n b t n - s m " i d = " r s - w e i g h t - p l u s " > + < / b u t t o n > < s p a n i d = " r s - w e i g h t - v a l " > $ { _ r e a d e r W e i g h t ( ) } < / s p a n > < / l a b e l >
< button class = "btn btn-sm ${readerSettings.fontFamily === 'serif' ? 'active' : ''}" data - rs - font = "serif" > Serif < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.fontFamily === 'sans' ? 'active' : ''}" data - rs - font = "sans" > Sans < / b u t t o n >
< button class = "btn btn-sm ${readerSettings.fontFamily === 'humanist' ? 'active' : ''}" data - rs - font = "humanist" > Verdana < / b u t t o n >
@ -5187,12 +5255,36 @@ function toggleSettingsPanel() {
saveReaderSettings ( ) ;
} ) ;
const weightRange = panel . querySelector ( '#rs-weight' ) ;
const weightVal = panel . querySelector ( '#rs-weight-val' ) ;
weightRange . addEventListener ( 'input' , ( ) => {
readerSettings . fontWeight = parseInt ( weightRange . value , 10 ) ;
weightVal . textContent = readerSettings . fontWeight ;
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
} ) ;
panel . querySelector ( '#rs-weight-minus' ) . addEventListener ( 'click' , ( ) => {
weightRange . value = Math . max ( + weightRange . min , parseInt ( weightRange . value , 10 ) - + weightRange . step ) ;
weightRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
panel . querySelector ( '#rs-weight-plus' ) . addEventListener ( 'click' , ( ) => {
weightRange . value = Math . min ( + weightRange . max , parseInt ( weightRange . value , 10 ) + + weightRange . step ) ;
weightRange . dispatchEvent ( new Event ( 'input' ) ) ;
} ) ;
panel . querySelectorAll ( '[data-rs-font]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( ) => {
readerSettings . fontFamily = btn . dataset . rsFont ;
panel . querySelectorAll ( '[data-rs-font]' ) . forEach ( b => b . classList . toggle ( 'active' , b === btn ) ) ;
// The system fonts only have cuts at multiples of 100.
weightRange . step = _readerWeightStep ( ) ;
if ( readerSettings . fontWeight ) {
readerSettings . fontWeight = Math . round ( readerSettings . fontWeight / weightRange . step ) * weightRange . step ;
}
applyReaderSettings ( false ) ;
saveReaderSettings ( ) ;
weightRange . value = _readerWeight ( ) ;
weightVal . textContent = _readerWeight ( ) ;
} ) ;
} ) ;