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: "Indice capitoli" onClicked: showChapterList() } MenuItem { text: "Margini del testo" 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: "Impostazioni" onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) } } // 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 = "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 } // ---- 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 = "Capitolo non leggibile" 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 = "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) { 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() } } }