diff --git a/qml/pages/ReaderPage.qml b/qml/pages/ReaderPage.qml
index fca4b98..0b8bf85 100644
--- a/qml/pages/ReaderPage.qml
+++ b/qml/pages/ReaderPage.qml
@@ -1,8 +1,13 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
-import Sailfish.WebView 1.0
import harbour.calibreweb 1.0
+// Lettore senza WebView: il capitolo è renderizzato come rich text Qt in un
+// Text QML dentro un SilicaFlickable. Lo scroll (contentY/contentHeight) è
+// nativo QML e sempre affidabile — sul WebView di Sailfish lo scroll
+// programmatico e la lettura della posizione non funzionano (window.scrollY
+// stale, contentHeight/scrollPosition non scrivibili): con il WebView il
+// cambio capitolo non scattava mai.
Page {
id: page
@@ -12,13 +17,10 @@ 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)
+ // posizione di lettura nel capitolo corrente, stimata in caratteri di testo
+ // dalla frazione di scroll (stabile col reflow del font)
property int charOffset: 0
- // caratteri di testo per pagina (stimati dalla viewport corrente): usato
- // per il passo di pagina e per la label "pagina corrente/totale"
- property int charsPerPage: 1
- // offset da ripristinare al load del capitolo (-1 = nessuno)
+ // offset da ripristinare al cambio capitolo (-1 = nessuno)
property int restoreCharOffset: -1
// ultimo offset assoluto salvato (evita scritture continue)
property int lastSavedAbs: -1
@@ -26,10 +28,15 @@ Page {
property int pageIndex: 1
property int pageCount: 1
property bool ready: false
- property bool restoring: false
property string errorMessage: ""
// reflow: scala il font (il testo si ri-avvolge), non lo zoom grafico
property double fontScale: 1.0
+ // restore in sospeso: frazione da applicare quando il testo si ri-impagina
+ property bool pendingRestore: false
+ property double restoreFrac: 0.0
+ // anteprima margini dal dialog (valori temporanei, -1 = usa le impostazioni)
+ property int previewMarginL: -1
+ property int previewMarginT: -1
allowedOrientations: Orientation.All
@@ -38,8 +45,9 @@ Page {
}
SilicaFlickable {
+ id: readerFlick
anchors.fill: parent
- contentHeight: parent.height
+ contentHeight: readerContent.height
// il PullDownMenu va DENTRO una vista Silica, non a livello di Page
PullDownMenu {
@@ -56,8 +64,9 @@ Page {
previewHandler: page.previewMargins
})
dlg.accepted.connect(function() {
- // i valori sono già persistiti da onAccepted; rinfresca subito
- applyMarginsJS()
+ // i valori sono già persistiti da onAccepted; esci dal preview
+ page.previewMarginL = -1
+ page.previewMarginT = -1
})
}
}
@@ -130,49 +139,43 @@ Page {
}
}
- WebView {
- id: web
- anchors.fill: parent
- active: true
- onLoadedChanged: {
- if (!loaded)
- return
- loadWatchdog.stop()
- page.ready = true
- // 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):
- // scrollTo oltre il limite porta in fondo al documento
- web.runJavaScript(
- "window.scrollTo(0,9999999);true",
- function() { readScrollPosition() })
- } else {
- web.runJavaScript(page.scrollToCharJs(off), function() {
- readScrollPosition()
- })
- }
- } else {
- readScrollPosition()
- }
- })
- page.restoring = false
+ // contenuto del capitolo: rich text Qt in un Text QML. Il cambio di
+ // currentChapter aggiorna il binding; il reflow (font/margini) è nativo.
+ Item {
+ id: readerContent
+ width: parent.width
+ height: Math.max(parent.height,
+ textItem.height + 2 * (page.previewMarginT >= 0
+ ? page.previewMarginT
+ : appSettings.readerMarginV))
+
+ Text {
+ id: textItem
+ x: page.previewMarginL >= 0 ? page.previewMarginL : appSettings.readerMargin
+ y: page.previewMarginT >= 0 ? page.previewMarginT : appSettings.readerMarginV
+ width: parent.width - 2 * (page.previewMarginL >= 0
+ ? page.previewMarginL
+ : appSettings.readerMargin)
+ text: book.chapterRichText(page.currentChapter)
+ textFormat: Text.RichText
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ font.pixelSize: page.fontPx()
+ color: Theme.primaryColor
+ onContentHeightChanged: page.onTextReflowed()
}
}
// Paginazione a tap: niente scroll libero. La meta' destra va avanti di
// una pagina, la sinistra indietro. Le fasce superiore e inferiore sono
// lasciate ai menu (PullDown in alto, PushUp in basso).
+ // preventStealing: i drag nella zona centrale NON scrollano (paginazione
+ // a tap come richiesto), i pulley menu restano raggiungibili dai bordi.
MouseArea {
id: tapZone
anchors.fill: parent
anchors.topMargin: Theme.itemSizeLarge
anchors.bottomMargin: Theme.itemSizeLarge + Theme.paddingLarge
+ preventStealing: true
onClicked: {
if (mouse.x > width / 2)
page.nextPage()
@@ -182,29 +185,11 @@ Page {
}
}
- BusyIndicator {
+ Label {
anchors.centerIn: parent
- running: !page.ready
- size: BusyIndicatorSize.Large
- }
-
- // watchdog: se il capitolo non si carica, niente spinner infinito
- Timer {
- id: loadWatchdog
- interval: 15000
- onTriggered: {
- if (!web.loaded && !page.ready) {
- page.errorMessage = qsTr("The reader is not responding (WebView). The EPUB file may be damaged.")
- page.ready = true
- }
- }
- }
-
- // ritardo di apertura: evita la race tra onCompleted e l'init del WebView
- Timer {
- id: openTimer
- interval: 100
- onTriggered: openChapter()
+ text: page.errorMessage
+ color: Theme.errorColor
+ visible: page.errorMessage.length > 0
}
// chiusura libro differita (pop dalla MainPage, non dall'handler del ReaderPage)
@@ -214,19 +199,23 @@ Page {
onTriggered: pageStack.pop(null)
}
- Label {
- anchors.centerIn: parent
- text: page.errorMessage
- color: Theme.errorColor
- visible: page.errorMessage.length > 0
+ // rete di sicurezza per il restore: se il reflow non ha ancora cambiato
+ // contentHeight (capitoli con altezza identica), applica comunque
+ Timer {
+ id: restoreTimer
+ interval: 80
+ onTriggered: {
+ if (page.pendingRestore) {
+ page.pendingRestore = false
+ applyRestore()
+ }
+ }
}
- // ---- controlli spostati nel PushUpMenu (trascina in alto) ----
-
- // ---- timer di tracciamento ----
+ // tracciamento periodico (aggiorna label e bookmark anche senza tap)
Timer {
id: trackTimer
- interval: 2000
+ interval: 3000
running: page.ready
repeat: true
onTriggered: readScrollPosition()
@@ -235,110 +224,51 @@ Page {
function overallProgress() {
if (book.chapterCount <= 0)
return 0
- return (page.currentChapter + page.chapterFraction) / book.chapterCount
+ var total = book.chapterStartChar(book.chapterCount)
+ if (total <= 0)
+ return 0
+ var start = book.chapterStartChar(page.currentChapter)
+ return (start + page.charOffset) / total
}
- // 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
");
+ stack.append(QString());
+ continue;
+ }
+ if (name == QLatin1String("img") || name == QLatin1String("hr")
+ || name == QLatin1String("input")) {
+ stack.append(QString());
+ continue;
+ }
+ QString tag;
+ if (name == QLatin1String("b") || name == QLatin1String("strong"))
+ tag = QStringLiteral("b");
+ else if (name == QLatin1String("i") || name == QLatin1String("em"))
+ tag = QStringLiteral("i");
+ else if (name == QLatin1String("u"))
+ tag = QStringLiteral("u");
+ else if (name == QLatin1String("s") || name == QLatin1String("strike")
+ || name == QLatin1String("del"))
+ tag = QStringLiteral("s");
+ else if (name == QLatin1String("ul"))
+ tag = QStringLiteral("ul");
+ else if (name == QLatin1String("ol"))
+ tag = QStringLiteral("ol");
+ else if (name == QLatin1String("li"))
+ tag = QStringLiteral("li");
+ else if (name == QLatin1String("table"))
+ tag = QStringLiteral("table border=\"1\" cellspacing=\"0\" cellpadding=\"4\"");
+ else if (name == QLatin1String("tr"))
+ tag = QStringLiteral("tr");
+ else if (name == QLatin1String("td") || name == QLatin1String("th"))
+ tag = QStringLiteral("td");
+ else if (name == QLatin1String("p") || name == QLatin1String("blockquote"))
+ tag = QStringLiteral("p");
+ else if (name == QLatin1String("h1") || name == QLatin1String("h2")
+ || name == QLatin1String("h3") || name == QLatin1String("h4")
+ || name == QLatin1String("h5") || name == QLatin1String("h6"))
+ tag = name;
+ // div/span/a/section/... -> trasparenti (tag vuoto)
+ if (!tag.isEmpty()) {
+ out += QLatin1Char('<') + tag + QLatin1Char('>');
+ stack.append(tag);
+ } else {
+ stack.append(QString());
+ }
+ } else if (t == QXmlStreamReader::EndElement) {
+ if (!stack.isEmpty()) {
+ const QString tag = stack.takeLast();
+ if (!tag.isEmpty()) {
+ const int sp = tag.indexOf(QLatin1Char(' '));
+ out += QStringLiteral("") + (sp > 0 ? tag.left(sp) : tag) + QLatin1Char('>');
+ }
+ }
+ } else if (t == QXmlStreamReader::Characters) {
+ out += r.text();
+ }
+ }
+
+ // HTML non ben formato (parse fallito): fallback grezzo — testo con
+ // sui newline e paragrafi sui doppi newline
+ if (r.hasError()) {
+ out.clear();
+ int i = 0;
+ const int n = scope.size();
+ while (i < n) {
+ const QChar c = scope.at(i);
+ if (c == QLatin1Char('<')) {
+ const int close = scope.indexOf(QLatin1Char('>'), i + 1);
+ i = (close < 0) ? n : close + 1;
+ continue;
+ }
+ if (c == QLatin1Char('&')) {
+ const int semi = scope.indexOf(QLatin1Char(';'), i + 1);
+ if (semi > i && semi - i <= 10) {
+ const QString ent = scope.mid(i + 1, semi - i - 1);
+ if (ent == QLatin1String("amp")) out += QLatin1Char('&');
+ else if (ent == QLatin1String("lt")) out += QLatin1Char('<');
+ else if (ent == QLatin1String("gt")) out += QLatin1Char('>');
+ else if (ent == QLatin1String("nbsp")) out += QLatin1Char(' ');
+ else if (ent.startsWith(QLatin1String("#"))) {
+ bool ok = false;
+ const int cp = ent.mid(1).toInt(&ok);
+ if (ok && cp > 0 && cp < 0x10FFFF)
+ out += QChar(cp);
+ } else {
+ out += c;
+ out += ent;
+ out += QLatin1Char(';');
+ }
+ i = semi + 1;
+ continue;
+ }
+ }
+ if (c == QLatin1Char('\n')) {
+ // doppio newline = paragrafo, singolo = a capo
+ if (i + 1 < n && scope.at(i + 1) == QLatin1Char('\n')) {
+ out += QStringLiteral("
");
+ ++i;
+ } else {
+ out += QStringLiteral("
");
+ }
+ ++i;
+ continue;
+ }
+ out += c;
+ ++i;
+ }
+ }
+ return out;
+}
+
} // namespace
// ---------------------------------------------------------------------------
@@ -524,6 +752,17 @@ QString EpubBook::chapterBaseUrl(int index) const
.toString();
}
+QString EpubBook::chapterRichText(int index) const
+{
+ if (index < 0 || index >= m_chapters.size())
+ return QString();
+ QFile f(m_chapters.at(index).path);
+ if (!f.open(QIODevice::ReadOnly))
+ return QString();
+ const QString html = QString::fromUtf8(f.readAll());
+ return htmlToRichText(html);
+}
+
void EpubBook::computeTextLengths()
{
m_textLengths.clear();
diff --git a/src/epub.h b/src/epub.h
index 0565fd2..fef98c9 100644
--- a/src/epub.h
+++ b/src/epub.h
@@ -45,6 +45,9 @@ public:
Q_INVOKABLE QString chapterHtml(int index, int fontSizePx, int marginPx, int marginVerticalPx) const;
// base URL (file://) per risolvere immagini/CSS relativi del capitolo
Q_INVOKABLE QString chapterBaseUrl(int index) const;
+ // rich text Qt del capitolo (per Text QML: niente WebView, scroll affidabile).
+ // Converte i tag HTML di base, salta script/style/head/immagini.
+ Q_INVOKABLE QString chapterRichText(int index) const;
// chiave segno locale v2, es. "cw:v2:12345" (posizione = carattere assoluto
// nel libro, indipendente da font, margini e reflow)
diff --git a/src/main.cpp b/src/main.cpp
index 18213c2..385e824 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -22,7 +22,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
app->setOrganizationName(QStringLiteral("harbour"));
app->setApplicationName(QStringLiteral("calibreweb"));
// versione mostrata nelle impostazioni (tenere allineata allo spec)
- app->setApplicationVersion(QStringLiteral("0.3.4"));
+ app->setApplicationVersion(QStringLiteral("0.4.0"));
Settings settings;
@@ -56,7 +56,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
QQuickView *view = SailfishApp::createView();
- qDebug() << "harbour-calibreweb v0.3.4 build" << __DATE__ << __TIME__;
+ qDebug() << "harbour-calibreweb v0.4.0 build" << __DATE__ << __TIME__;
qmlRegisterType