Lettore 0.3.0: bookmark per posizione (carattere) e paginazione corretta
- Bookmark cw:v2:<offset assoluto caratteri> invece di capitolo+frazione; migrazione automatica delle chiavi v1 (parseBookmarkKey) - Lunghezze testo per capitolo in C++ (text node del DOM, entità decodificate, body-only) per mappare offset assoluto -> capitolo - Paginazione: stile iniettato height:auto/overflow:visible + unlock JS dei contenitori hidden — il CSS di molti EPUB bloccava lo scroll e ogni tap saltava al capitolo successivo - prevPage dall'inizio capitolo -> ultima pagina del capitolo precedente - Cambio font mantiene la posizione (restore per carattere) - Label menu: pagina corrente/totale del capitolo; max su documentElement+body - Test: harness C++ (Qt) e test JS con DOM mock
This commit is contained in:
+175
-44
@@ -12,9 +12,18 @@ Page {
|
||||
|
||||
property int currentChapter: 0
|
||||
property double chapterFraction: 0.0
|
||||
// posizione di lettura nel capitolo corrente, in caratteri di testo
|
||||
// (conteggio JS sui text node: stabile al variare di font/margini)
|
||||
property int charOffset: 0
|
||||
// offset da ripristinare al load del capitolo (-1 = nessuno)
|
||||
property int restoreCharOffset: -1
|
||||
// ultimo offset assoluto salvato (evita scritture continue)
|
||||
property int lastSavedAbs: -1
|
||||
// pagina corrente / pagine totali del capitolo (per la label del menu)
|
||||
property int pageIndex: 1
|
||||
property int pageCount: 1
|
||||
property bool ready: false
|
||||
property bool restoring: false
|
||||
property int saveCounter: 0
|
||||
property string errorMessage: ""
|
||||
// reflow: scala il font (il testo si ri-avvolge), non lo zoom grafico
|
||||
property double fontScale: 1.0
|
||||
@@ -72,7 +81,8 @@ Page {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
text: (book.chapterCount > 0)
|
||||
? (page.currentChapter + 1) + "/" + book.chapterCount
|
||||
? page.pageIndex + "/" + page.pageCount + " · "
|
||||
+ (page.currentChapter + 1) + "/" + book.chapterCount
|
||||
+ " · " + book.chapterTitle(page.currentChapter)
|
||||
+ " · " + Math.round(page.overallProgress() * 100) + "%"
|
||||
: "…"
|
||||
@@ -126,13 +136,28 @@ Page {
|
||||
return
|
||||
loadWatchdog.stop()
|
||||
page.ready = true
|
||||
// ripristina la posizione in frazione sull'altezza ricalcolata
|
||||
if (page.chapterFraction > 0.01) {
|
||||
web.runJavaScript(
|
||||
"window.scrollTo(0," + page.chapterFraction
|
||||
+ "*Math.max(1,document.documentElement.scrollHeight-window.innerHeight));true",
|
||||
function() { })
|
||||
}
|
||||
// sblocca lo scroll se il CSS del libro lo blocca, poi ripristina
|
||||
// la posizione per OFFSET DI CARATTERE (stabile con font/margini)
|
||||
web.runJavaScript(page.unlockScrollJs(), function() {
|
||||
if (page.restoreCharOffset >= 0) {
|
||||
var off = page.restoreCharOffset
|
||||
page.restoreCharOffset = -1
|
||||
if (off >= book.chapterTextLength(page.currentChapter)) {
|
||||
// oltre la fine = vai all'ultima pagina (usato da
|
||||
// prevPage per non saltare la coda del capitolo)
|
||||
web.runJavaScript(
|
||||
"window.scrollTo(0,Math.max(1,"
|
||||
+ "document.documentElement.scrollHeight-window.innerHeight));true",
|
||||
function() { readScrollPosition() })
|
||||
} else {
|
||||
web.runJavaScript(page.scrollToCharJs(off), function() {
|
||||
readScrollPosition()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
readScrollPosition()
|
||||
}
|
||||
})
|
||||
page.restoring = false
|
||||
}
|
||||
}
|
||||
@@ -210,28 +235,95 @@ Page {
|
||||
return (page.currentChapter + page.chapterFraction) / book.chapterCount
|
||||
}
|
||||
|
||||
// JS che sblocca lo scroll del documento: rimuove overflow hidden / altezze
|
||||
// fisse che i CSS di alcuni EPUB impongono (sintomo: max==0, ogni tap salta
|
||||
// al capitolo successivo saltando tutto il testo)
|
||||
function unlockScrollJs() {
|
||||
return "(function(){"
|
||||
+ "var de=document.documentElement,b=document.body;"
|
||||
+ "if(!de||!b)return false;"
|
||||
+ "de.style.setProperty('overflow','visible','important');"
|
||||
+ "b.style.setProperty('overflow','visible','important');"
|
||||
+ "de.style.setProperty('height','auto','important');"
|
||||
+ "b.style.setProperty('height','auto','important');"
|
||||
+ "var after=Math.max(de.scrollHeight,b.scrollHeight);"
|
||||
+ "if(after<=window.innerHeight+1&&b.textContent.length>0){"
|
||||
+ "var all=b.querySelectorAll('div,section,article,main,td,li');"
|
||||
+ "for(var i=0;i<all.length;i++){var cs=getComputedStyle(all[i]);"
|
||||
+ "if(cs.overflowY==='hidden'||cs.overflowY==='scroll'){"
|
||||
+ "all[i].style.setProperty('overflow','visible','important');}}}"
|
||||
+ "return true;})()"
|
||||
}
|
||||
|
||||
// JS che legge la posizione: {o: caratteri di testo sopra la viewport,
|
||||
// f: frazione di scroll, p: pagina corrente, pc: pagine totali}.
|
||||
// Il conteggio cammina i text node del DOM: stabile al variare di font e
|
||||
// margini (a differenza della frazione di scroll).
|
||||
function positionJs() {
|
||||
return "(function(){"
|
||||
+ "var de=document.documentElement,b=document.body;"
|
||||
+ "var max=Math.max(1,Math.max(de.scrollHeight,b.scrollHeight)-window.innerHeight);"
|
||||
+ "var y=Math.max(0,Math.min(max,window.scrollY));"
|
||||
+ "var vh=window.innerHeight;"
|
||||
+ "var o=0;"
|
||||
+ "var it=document.createTreeWalker(b,NodeFilter.SHOW_TEXT);"
|
||||
+ "var n;"
|
||||
+ "while((n=it.nextNode())){"
|
||||
+ "var p=n.parentNode;"
|
||||
+ "if(!p||p.tagName==='SCRIPT'||p.tagName==='STYLE'||p.tagName==='NOSCRIPT')continue;"
|
||||
+ "var r=document.createRange();r.selectNodeContents(n);"
|
||||
+ "var rect=r.getBoundingClientRect();"
|
||||
+ "if(rect.bottom<=0){o+=n.data.length;continue;}"
|
||||
+ "if(rect.top>=0)break;"
|
||||
+ "var h=rect.bottom-rect.top;"
|
||||
+ "if(h>0)o+=Math.round(n.data.length*(0-rect.top)/h);"
|
||||
+ "break;}"
|
||||
+ "return JSON.stringify({o:o,f:y/max,p:Math.floor(y/vh)+1,"
|
||||
+ "pc:Math.max(1,Math.floor(max/vh)+1)});})()"
|
||||
}
|
||||
|
||||
// JS che porta in cima alla viewport il carattere 'off' del capitolo
|
||||
function scrollToCharJs(off) {
|
||||
return "(function(){"
|
||||
+ "var b=document.body,o=0;"
|
||||
+ "var it=document.createTreeWalker(b,NodeFilter.SHOW_TEXT);"
|
||||
+ "var n;"
|
||||
+ "while((n=it.nextNode())){"
|
||||
+ "var p=n.parentNode;"
|
||||
+ "if(!p||p.tagName==='SCRIPT'||p.tagName==='STYLE'||p.tagName==='NOSCRIPT')continue;"
|
||||
+ "if(o+n.data.length>=" + off + "){"
|
||||
+ "var r=document.createRange();r.setStart(n," + off + "-o);r.collapse(true);"
|
||||
+ "var rect=r.getBoundingClientRect();"
|
||||
+ "if(rect.top>0)window.scrollTo(0,rect.top+window.scrollY-8);"
|
||||
+ "return true;}"
|
||||
+ "o+=n.data.length;}"
|
||||
+ "return false;})()"
|
||||
}
|
||||
|
||||
function readScrollPosition() {
|
||||
if (!web.loaded)
|
||||
return
|
||||
web.runJavaScript(
|
||||
"window.scrollY / Math.max(1, document.documentElement.scrollHeight - window.innerHeight)",
|
||||
function(f) {
|
||||
var v = Math.max(0, Math.min(1, f))
|
||||
if (Math.abs(v - page.chapterFraction) > 0.005) {
|
||||
page.chapterFraction = v
|
||||
maybeSave(true)
|
||||
}
|
||||
})
|
||||
web.runJavaScript(page.positionJs(), function(res) {
|
||||
var r = JSON.parse(res)
|
||||
page.charOffset = r.o
|
||||
page.chapterFraction = r.f
|
||||
page.pageIndex = r.p
|
||||
page.pageCount = r.pc
|
||||
maybeSave(false)
|
||||
})
|
||||
}
|
||||
|
||||
function maybeSave(force) {
|
||||
if (bookId <= 0)
|
||||
return
|
||||
page.saveCounter++
|
||||
// salva in locale ogni ~10 s (o subito su forzatura)
|
||||
if (force || page.saveCounter % 5 === 0) {
|
||||
// posizione assoluta nel libro = capitolo + caratteri del capitolo
|
||||
var abs = book.chapterStartChar(page.currentChapter) + page.charOffset
|
||||
// salva subito su forzatura, altrimenti solo se la posizione è cambiata
|
||||
// di almeno 20 caratteri (evita scritture continue a tracker attivo)
|
||||
if (force || Math.abs(abs - page.lastSavedAbs) > 20) {
|
||||
page.lastSavedAbs = abs
|
||||
appSettings.setBookmarkKey(page.bookId, page.bookFormat,
|
||||
book.makeBookmarkKey(page.currentChapter, page.chapterFraction))
|
||||
book.makeBookmarkKey(page.currentChapter, page.charOffset))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +333,9 @@ Page {
|
||||
if (index !== page.currentChapter) {
|
||||
maybeSave(true)
|
||||
page.chapterFraction = 0
|
||||
page.charOffset = 0
|
||||
page.restoreCharOffset = -1
|
||||
page.lastSavedAbs = -1
|
||||
}
|
||||
page.currentChapter = index
|
||||
loadChapter()
|
||||
@@ -248,41 +343,73 @@ Page {
|
||||
|
||||
function changeFontScale(delta) {
|
||||
page.fontScale = Math.max(0.7, Math.min(2.5, page.fontScale + delta))
|
||||
// ricarica il capitolo con la nuova dimensione: il reflow avviene al load
|
||||
// ricarica con la nuova dimensione, mantenendo la posizione di lettura:
|
||||
// l'offset in CARATTERI è stabile col reflow (a differenza dei px scrollati)
|
||||
page.restoreCharOffset = page.charOffset
|
||||
loadChapter()
|
||||
}
|
||||
|
||||
// pagina successiva: se l'ultima pagina del capitolo, passa al capitolo dopo
|
||||
// pagina successiva: una viewport avanti; all'ultima pagina passa al capitolo
|
||||
// dopo. max è calcolato su documentElement E body (alcuni libri scrollano il body).
|
||||
function nextPage() {
|
||||
if (!web.loaded || !page.ready)
|
||||
return
|
||||
web.runJavaScript(
|
||||
"var vh=window.innerHeight;" +
|
||||
"var cur=Math.floor(window.scrollY/vh);" +
|
||||
"var max=Math.max(0,document.documentElement.scrollHeight-window.innerHeight);" +
|
||||
"var target=(cur+1)*vh;" +
|
||||
"if(target<=max){window.scrollTo(0,target);true}else{false}",
|
||||
function(hasNext) {
|
||||
if (hasNext)
|
||||
"(function(){"
|
||||
+ "var de=document.documentElement,b=document.body;"
|
||||
+ "var max=Math.max(de.scrollHeight,b.scrollHeight)-window.innerHeight;"
|
||||
+ "if(max<=1){"
|
||||
+ "de.style.setProperty('overflow','visible','important');"
|
||||
+ "b.style.setProperty('overflow','visible','important');"
|
||||
+ "de.style.setProperty('height','auto','important');"
|
||||
+ "b.style.setProperty('height','auto','important');"
|
||||
+ "max=Math.max(de.scrollHeight,b.scrollHeight)-window.innerHeight;}"
|
||||
+ "if(max<=1)return JSON.stringify({ok:false,atEnd:true});"
|
||||
+ "var vh=window.innerHeight;"
|
||||
+ "var cur=Math.max(0,Math.min(max,window.scrollY));"
|
||||
+ "var target=Math.min(max,cur+vh);"
|
||||
+ "if(target>cur+1){window.scrollTo(0,target);"
|
||||
+ "return JSON.stringify({ok:true,atEnd:target>=max-1});}"
|
||||
+ "return JSON.stringify({ok:false,atEnd:true});})()",
|
||||
function(res) {
|
||||
var r = JSON.parse(res)
|
||||
if (r.ok) {
|
||||
readScrollPosition()
|
||||
else
|
||||
} else if (r.atEnd) {
|
||||
gotoChapter(page.currentChapter + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// pagina precedente: se la prima del capitolo, torna al capitolo prima
|
||||
// pagina precedente: una viewport indietro; dalla prima pagina torna al
|
||||
// capitolo prima SULL'ULTIMA PAGINA (non salta la coda del capitolo)
|
||||
function prevPage() {
|
||||
if (!web.loaded || !page.ready)
|
||||
return
|
||||
web.runJavaScript(
|
||||
"var vh=window.innerHeight;" +
|
||||
"var cur=Math.floor(window.scrollY/vh);" +
|
||||
"if(cur>0){window.scrollTo(0,(cur-1)*vh);true}else{false}",
|
||||
function(hasPrev) {
|
||||
if (hasPrev)
|
||||
"(function(){"
|
||||
+ "var de=document.documentElement,b=document.body;"
|
||||
+ "var max=Math.max(de.scrollHeight,b.scrollHeight)-window.innerHeight;"
|
||||
+ "var vh=window.innerHeight;"
|
||||
+ "var cur=Math.max(0,Math.min(max,window.scrollY));"
|
||||
+ "if(cur>1){window.scrollTo(0,Math.max(0,cur-vh));"
|
||||
+ "return JSON.stringify({ok:true,atStart:false});}"
|
||||
+ "return JSON.stringify({ok:false,atStart:true});})()",
|
||||
function(res) {
|
||||
var r = JSON.parse(res)
|
||||
if (r.ok) {
|
||||
readScrollPosition()
|
||||
else
|
||||
gotoChapter(page.currentChapter - 1)
|
||||
} else if (r.atStart && page.currentChapter > 0) {
|
||||
maybeSave(true)
|
||||
page.currentChapter -= 1
|
||||
page.chapterFraction = 0
|
||||
page.charOffset = 0
|
||||
// sentinella "fine capitolo": va oltre la lunghezza, così
|
||||
// onLoadedChanged porta all'ultima pagina
|
||||
page.restoreCharOffset = book.chapterTextLength(page.currentChapter) + 1
|
||||
page.lastSavedAbs = -1
|
||||
loadChapter()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -353,19 +480,23 @@ Page {
|
||||
}
|
||||
|
||||
function openChapter() {
|
||||
// ripristina il segno locale
|
||||
// ripristina il segno locale: posizione per CARATTERE (v2),
|
||||
// le vecchie chiavi v1 vengono migrate da parseBookmarkKey
|
||||
var key = appSettings.bookmarkKey(page.bookId, page.bookFormat)
|
||||
var ch = 0
|
||||
var fr = 0.0
|
||||
var off = -1
|
||||
if (key.length > 0) {
|
||||
var r = book.parseBookmarkKey(key)
|
||||
if (r && r.hasOwnProperty("chapter")) {
|
||||
ch = Math.max(0, Math.min(r.chapter, book.chapterCount - 1))
|
||||
fr = r.fraction
|
||||
off = r.hasOwnProperty("charOffset") ? r.charOffset : -1
|
||||
}
|
||||
}
|
||||
page.currentChapter = ch
|
||||
page.chapterFraction = fr
|
||||
page.chapterFraction = 0
|
||||
page.charOffset = 0
|
||||
page.restoreCharOffset = off
|
||||
page.lastSavedAbs = -1
|
||||
loadChapter()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user