Lettore EPUB nativo: ReaderPage (WebView+capitoli+progresso), segno locale in QSettings, sync POST /ajax/bookmark con fallback CSRF, pulsante Leggi nel dettaglio
This commit is contained in:
@@ -8,6 +8,8 @@ Page {
|
||||
property string coverSource: ""
|
||||
property bool downloading: false
|
||||
property string downloadError: ""
|
||||
property bool readingNow: false
|
||||
property string readingUrl: ""
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
@@ -29,6 +31,14 @@ Page {
|
||||
visible: source.length > 0
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Leggi (EPUB)"
|
||||
visible: page.hasEpubFormat()
|
||||
enabled: !page.downloading
|
||||
onClicked: startReading()
|
||||
}
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
@@ -157,6 +167,50 @@ Page {
|
||||
return out.trim()
|
||||
}
|
||||
|
||||
function hasEpubFormat() {
|
||||
var fmts = bookData.formats
|
||||
if (!fmts)
|
||||
return false
|
||||
for (var i = 0; i < fmts.length; i++)
|
||||
if (String(fmts[i].format).toLowerCase() === "epub")
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
function firstEpubFormat() {
|
||||
var fmts = bookData.formats
|
||||
if (!fmts)
|
||||
return null
|
||||
for (var i = 0; i < fmts.length; i++)
|
||||
if (String(fmts[i].format).toLowerCase() === "epub")
|
||||
return fmts[i]
|
||||
return null
|
||||
}
|
||||
|
||||
// estrae l'id numerico calibre da un url di download: /opds/download/<id>/<formato>/
|
||||
function extractBookId(u) {
|
||||
var parts = u.split("/")
|
||||
for (var i = 0; i < parts.length - 1; i++)
|
||||
if (parts[i] === "download" && parts[i + 1].length > 0
|
||||
&& !isNaN(parseInt(parts[i + 1])))
|
||||
return parseInt(parts[i + 1])
|
||||
return 0
|
||||
}
|
||||
|
||||
function startReading() {
|
||||
var f = page.firstEpubFormat()
|
||||
if (!f)
|
||||
return
|
||||
var id = page.extractBookId(f.url)
|
||||
if (id <= 0)
|
||||
return
|
||||
page.readingNow = true
|
||||
page.readingUrl = f.url
|
||||
page.downloading = true
|
||||
page.downloadError = ""
|
||||
downloader.download(resolveUrl(f.url), appSettings.bookFilePath(id, "epub"))
|
||||
}
|
||||
|
||||
function startDownload(format) {
|
||||
if (page.downloading)
|
||||
return
|
||||
@@ -196,11 +250,19 @@ Page {
|
||||
onFinished: {
|
||||
page.downloading = false
|
||||
if (ok) {
|
||||
if (page.readingNow) {
|
||||
page.readingNow = false
|
||||
var id = page.extractBookId(page.readingUrl)
|
||||
pageStack.push(Qt.resolvedUrl("ReaderPage.qml"),
|
||||
{ epubPath: path, bookId: id, bookFormat: "epub" })
|
||||
return
|
||||
}
|
||||
page.downloadError = ""
|
||||
page.downloadNotice = "Salvato in " + path
|
||||
hideNoticeTimer.restart()
|
||||
} else {
|
||||
page.downloadNotice = ""
|
||||
page.readingNow = false
|
||||
page.downloadError = "Errore: " + error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
Dialog {
|
||||
id: dialog
|
||||
|
||||
property var model: []
|
||||
property int selectedIndex: 0
|
||||
|
||||
SilicaListView {
|
||||
anchors.fill: parent
|
||||
header: DialogHeader { title: "Indice" }
|
||||
model: dialog.model
|
||||
|
||||
delegate: ListItem {
|
||||
id: item
|
||||
contentHeight: Theme.itemSizeMedium
|
||||
|
||||
Label {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: (modelData.index + 1) + ". " + modelData.title
|
||||
color: item.highlighted ? Theme.highlightColor : Theme.primaryColor
|
||||
truncationMode: TruncationMode.Elide
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
dialog.selectedIndex = modelData.index
|
||||
dialog.accept()
|
||||
}
|
||||
}
|
||||
|
||||
ViewPlaceholder {
|
||||
enabled: dialog.model.length === 0
|
||||
text: "Nessun capitolo"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
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 restoring: false
|
||||
property int saveCounter: 0
|
||||
property int syncCounter: 0
|
||||
property string errorMessage: ""
|
||||
|
||||
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
|
||||
WebView {
|
||||
id: web
|
||||
anchors.fill: parent
|
||||
active: true
|
||||
onLoadedChanged: {
|
||||
if (!loaded)
|
||||
return
|
||||
if (page.restoring)
|
||||
page.restoring = false
|
||||
else
|
||||
restorePosition()
|
||||
page.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: !page.ready
|
||||
size: BusyIndicatorSize.Large
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Label {
|
||||
id: chapterLabel
|
||||
anchors.left: prevBtn.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
|
||||
}
|
||||
}
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: "Indice capitoli"
|
||||
onClicked: showChapterList()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Aggiorna posizione sul server"
|
||||
onClicked: syncNow()
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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 restorePosition() {
|
||||
if (page.chapterFraction > 0.01) {
|
||||
web.runJavaScript(
|
||||
"window.scrollTo(0, " + page.chapterFraction
|
||||
+ " * Math.max(1, document.documentElement.scrollHeight - window.innerHeight)); true",
|
||||
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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
web.url = "file://" + book.chapterFilePath(page.currentChapter)
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === PageStatus.Inactive)
|
||||
maybeSave(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user