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

377 lines
13 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 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.fill: parent
contentHeight: parent.height
// il PullDownMenu va DENTRO una vista Silica, non a livello di Page
PullDownMenu {
MenuItem {
text: qsTr("Table of contents")
onClicked: showChapterList()
}
MenuItem {
text: qsTr("Text margins")
onClicked: {
var dlg = pageStack.push(Qt.resolvedUrl("MarginDialog.qml"), {
marginLr: appSettings.readerMargin,
marginTb: appSettings.readerMarginV,
previewHandler: page.previewMargins
})
dlg.accepted.connect(function() {
// i valori sono già persistiti da onAccepted; rinfresca subito
applyMarginsJS()
})
}
}
MenuItem {
text: qsTr("Settings")
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
}
MenuItem {
text: qsTr("Close book")
onClicked: {
page.maybeSave(true)
// torna alla prima pagina dello stack (MainPage):
// popTo(null) non è affidabile su questa Silica
while (pageStack.depth > 1)
pageStack.pop()
}
}
}
// controlli del lettore: un solo trascinamento in alto li apre
PushUpMenu {
id: readerMenu
Label {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * Theme.horizontalPageMargin
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
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.paddingSmall
IconButton {
icon.source: "image://theme/icon-m-left"
enabled: page.currentChapter > 0
onClicked: gotoChapter(page.currentChapter - 1)
}
Button {
text: "A"
width: Theme.itemSizeSmall
height: Theme.itemSizeMedium
onClicked: page.changeFontScale(-0.15)
}
Button {
text: "A+"
width: Theme.itemSizeSmall
height: Theme.itemSizeMedium
onClicked: page.changeFontScale(0.15)
}
IconButton {
icon.source: "image://theme/icon-m-right"
enabled: page.currentChapter < book.chapterCount - 1
onClicked: gotoChapter(page.currentChapter + 1)
}
}
ProgressBar {
width: parent.width
value: page.overallProgress()
minimumValue: 0
maximumValue: 1
}
}
WebView {
id: web
anchors.fill: parent
active: true
onLoadedChanged: {
if (!loaded)
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() { })
}
page.restoring = false
}
}
// 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).
MouseArea {
id: tapZone
anchors.fill: parent
anchors.topMargin: Theme.itemSizeLarge
anchors.bottomMargin: Theme.itemSizeLarge + Theme.paddingLarge
onClicked: {
if (mouse.x > width / 2)
page.nextPage()
else
page.prevPage()
}
}
}
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 = 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()
}
Label {
anchors.centerIn: parent
text: page.errorMessage
color: Theme.errorColor
visible: page.errorMessage.length > 0
}
// ---- controlli spostati nel PushUpMenu (trascina in alto) ----
// ---- 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 in locale ogni ~10 s (o subito su forzatura)
if (force || page.saveCounter % 5 === 0) {
appSettings.setBookmarkKey(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
loadChapter()
}
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
loadChapter()
}
// pagina successiva: se l'ultima pagina del capitolo, passa al capitolo dopo
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)
readScrollPosition()
else
gotoChapter(page.currentChapter + 1)
})
}
// pagina precedente: se la prima del capitolo, torna al capitolo prima
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)
readScrollPosition()
else
gotoChapter(page.currentChapter - 1)
})
}
function fontPx() {
return Math.round(16 * page.fontScale * 100) / 100
}
// carica il capitolo corrente via loadHtml: lo stile di reflow (font-size
// e margini) è iniettato nell'HTML dal parser, quindi niente JS post-load
function loadChapter() {
var html = book.chapterHtml(page.currentChapter, page.fontPx(),
appSettings.readerMargin, appSettings.readerMarginV)
if (html.length === 0) {
page.errorMessage = qsTr("Chapter unreadable")
page.ready = true
return
}
page.ready = false
page.restoring = true
loadWatchdog.restart()
web.loadHtml(html, book.chapterBaseUrl(page.currentChapter))
}
function showChapterList() {
var dialog = pageStack.push(Qt.resolvedUrl("ChapterListDialog.qml"),
{ model: book.chapterList() })
dialog.accepted.connect(function() {
gotoChapter(dialog.selectedIndex)
})
}
// JS che sovrascrive i margini sul documento aperto: stile inline con
// 'important' batte lo stylesheet iniettato. Effetto immediato, niente reload.
function marginJs(lr, tb) {
return "var h=document.documentElement,b=document.body;"
+ "h.style.setProperty('margin-left','" + lr + "px','important');"
+ "h.style.setProperty('margin-right','" + lr + "px','important');"
+ "b.style.setProperty('margin-left','" + lr + "px','important');"
+ "b.style.setProperty('margin-right','" + lr + "px','important');"
+ "b.style.setProperty('margin-top','" + tb + "px','important');"
+ "b.style.setProperty('margin-bottom','" + tb + "px','important');"
+ "true"
}
// riapplica i margini salvati nelle impostazioni (senza ricaricare il libro)
function applyMarginsJS() {
if (!web.loaded || !page.ready)
return
web.runJavaScript(marginJs(appSettings.readerMargin, appSettings.readerMarginV),
function() { })
}
// anteprima live dal dialogo margini: valori non ancora persistiti
function previewMargins(lr, tb) {
if (!web.loaded || !page.ready)
return
web.runJavaScript(marginJs(lr, tb), function() { })
}
Component.onCompleted: {
page.ready = false
if (!book.open(page.epubPath)) {
page.errorMessage = qsTr("Unable to open the EPUB file")
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) {
var r = book.parseBookmarkKey(key)
if (r && r.hasOwnProperty("chapter")) {
ch = Math.max(0, Math.min(r.chapter, book.chapterCount - 1))
fr = r.fraction
}
}
page.currentChapter = ch
page.chapterFraction = fr
loadChapter()
}
onStatusChanged: {
if (status === PageStatus.Inactive) {
maybeSave(true)
} else if (status === PageStatus.Active) {
// riapplica i margini se cambiati nelle impostazioni:
// via JS sul documento aperto, senza ricaricare il capitolo
if (page.ready && book.chapterCount() > 0)
applyMarginsJS()
}
}
}