Files
harbour-calibreweb/qml/pages/ReaderPage.qml
T

293 lines
9.0 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick 2.0
import Sailfish.Silica 1.0
import Sailfish.WebView 1.0
import harbour.calibreweb 1.0
Page {
id: page
property string epubPath: ""
property int bookId: 0
property string bookFormat: "epub"
property int currentChapter: 0
property double chapterFraction: 0.0
property bool ready: false
property bool restoring: false
property int saveCounter: 0
property int syncCounter: 0
property string errorMessage: ""
// reflow: scala il font (il testo si ri-avvolge), non lo zoom grafico
property double fontScale: 1.0
allowedOrientations: Orientation.All
EpubBook {
id: book
}
SilicaFlickable {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: toolbar.top
contentHeight: parent.height
// il PullDownMenu va DENTRO una vista Silica, non a livello di Page
PullDownMenu {
MenuItem {
text: "Indice capitoli"
onClicked: showChapterList()
}
MenuItem {
text: "Aggiorna posizione sul server"
onClicked: syncNow()
}
}
WebView {
id: web
anchors.fill: parent
active: true
onLoadedChanged: {
if (!loaded)
return
loadWatchdog.stop()
if (page.restoring)
page.restoring = false
page.ready = true
applyFontScale()
}
}
}
BusyIndicator {
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 = "Il lettore non risponde (WebView). Il file EPUB potrebbe essere danneggiato."
page.ready = true
}
}
}
// ritardo di apertura: evita la race tra onCompleted e l'init del WebView
Timer {
id: openTimer
interval: 100
onTriggered: openChapter()
}
Label {
anchors.centerIn: parent
text: page.errorMessage
color: Theme.errorColor
visible: page.errorMessage.length > 0
}
// ---- barra di controllo ----
Rectangle {
id: toolbar
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: Theme.itemSizeMedium
color: Theme.rgba(Theme.primaryColor, 0.08)
IconButton {
id: prevBtn
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
icon.source: "image://theme/icon-m-left"
enabled: page.currentChapter > 0
onClicked: gotoChapter(page.currentChapter - 1)
}
Button {
id: fontMinus
anchors.left: prevBtn.right
anchors.verticalCenter: parent.verticalCenter
width: Theme.itemSizeSmall
height: Theme.itemSizeMedium
text: "A"
onClicked: page.changeFontScale(-0.15)
}
Button {
id: fontPlus
anchors.left: fontMinus.right
anchors.verticalCenter: parent.verticalCenter
width: Theme.itemSizeSmall
height: Theme.itemSizeMedium
text: "A+"
onClicked: page.changeFontScale(0.15)
}
Label {
id: chapterLabel
anchors.left: fontPlus.right
anchors.right: nextBtn.left
anchors.verticalCenter: parent.verticalCenter
text: (book.chapterCount > 0)
? (page.currentChapter + 1) + "/" + book.chapterCount
+ " · " + book.chapterTitle(page.currentChapter)
+ " · " + Math.round(page.overallProgress() * 100) + "%"
: "…"
truncationMode: TruncationMode.Elide
color: Theme.primaryColor
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeSmall
}
IconButton {
id: nextBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
icon.source: "image://theme/icon-m-right"
enabled: page.currentChapter < book.chapterCount - 1
onClicked: gotoChapter(page.currentChapter + 1)
}
ProgressBar {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 6
value: page.overallProgress()
minimumValue: 0
maximumValue: 1
}
}
// ---- timer di tracciamento ----
Timer {
id: trackTimer
interval: 2000
running: page.ready
repeat: true
onTriggered: readScrollPosition()
}
function overallProgress() {
if (book.chapterCount <= 0)
return 0
return (page.currentChapter + page.chapterFraction) / book.chapterCount
}
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)
}
})
}
function maybeSave(force) {
if (bookId <= 0)
return
page.saveCounter++
// salva locale ogni ~10 s, sync server ogni ~30 s
if (force || page.saveCounter % 5 === 0) {
appSettings.setBookmarkKey(page.bookId, page.bookFormat,
book.makeBookmarkKey(page.currentChapter, page.chapterFraction))
}
if (force || page.syncCounter++ % 15 === 0)
syncNow()
}
function syncNow() {
if (bookId <= 0)
return
apiClient.postBookmark(page.bookId, page.bookFormat,
book.makeBookmarkKey(page.currentChapter, page.chapterFraction))
}
function gotoChapter(index) {
if (index < 0 || index >= book.chapterCount)
return
if (index !== page.currentChapter) {
maybeSave(true)
page.chapterFraction = 0
}
page.currentChapter = index
page.restoring = true
page.ready = false
web.url = "file://" + book.chapterFilePath(index)
}
function changeFontScale(delta) {
page.fontScale = Math.max(0.7, Math.min(2.5, page.fontScale + delta))
applyFontScale()
}
// reflow: imposta il font-size del documento (il testo si ri-avvolge)
// e ripristina la posizione in frazione sull'altezza ricalcolata.
// Serve uno <style> con !important: molti EPUB impostano px su body/p
// che vincerebbero sul semplice font-size dell'html
function applyFontScale() {
if (!web.loaded)
return
var px = Math.round(16 * page.fontScale * 100) / 100
var script = "(function(){"
+ "var s=document.getElementById('cw-font');"
+ "if(!s){s=document.createElement('style');s.id='cw-font';document.head.appendChild(s);}"
+ "s.textContent='html,body{font-size:" + px + "px!important}"
+ "p,div,li,td,blockquote,section{font-size:1em!important}';"
+ "window.scrollTo(0," + page.chapterFraction
+ "*Math.max(1,document.documentElement.scrollHeight-window.innerHeight));"
+ "true})()"
web.runJavaScript(script, function() { })
}
function showChapterList() {
var dialog = pageStack.push(Qt.resolvedUrl("ChapterListDialog.qml"),
{ model: book.chapterList() })
dialog.accepted.connect(function() {
gotoChapter(dialog.selectedIndex)
})
}
Component.onCompleted: {
page.ready = false
if (!book.open(page.epubPath)) {
page.errorMessage = "Impossibile aprire il libro EPUB"
page.ready = true
return
}
openTimer.start()
}
function openChapter() {
// ripristina il segno locale
var key = appSettings.bookmarkKey(page.bookId, page.bookFormat)
var ch = 0
var fr = 0.0
if (key.length > 0 && book.parseBookmarkKey(key, ch, fr)) {
page.currentChapter = Math.max(0, Math.min(ch, book.chapterCount - 1))
page.chapterFraction = fr
}
page.ready = false
loadWatchdog.restart()
web.url = "file://" + book.chapterFilePath(page.currentChapter)
}
onStatusChanged: {
if (status === PageStatus.Inactive)
maybeSave(true)
}
}