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 string coverSource: ""
|
||||||
property bool downloading: false
|
property bool downloading: false
|
||||||
property string downloadError: ""
|
property string downloadError: ""
|
||||||
|
property bool readingNow: false
|
||||||
|
property string readingUrl: ""
|
||||||
|
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -29,6 +31,14 @@ Page {
|
|||||||
visible: source.length > 0
|
visible: source.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "Leggi (EPUB)"
|
||||||
|
visible: page.hasEpubFormat()
|
||||||
|
enabled: !page.downloading
|
||||||
|
onClicked: startReading()
|
||||||
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
x: Theme.horizontalPageMargin
|
x: Theme.horizontalPageMargin
|
||||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||||
@@ -157,6 +167,50 @@ Page {
|
|||||||
return out.trim()
|
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) {
|
function startDownload(format) {
|
||||||
if (page.downloading)
|
if (page.downloading)
|
||||||
return
|
return
|
||||||
@@ -196,11 +250,19 @@ Page {
|
|||||||
onFinished: {
|
onFinished: {
|
||||||
page.downloading = false
|
page.downloading = false
|
||||||
if (ok) {
|
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.downloadError = ""
|
||||||
page.downloadNotice = "Salvato in " + path
|
page.downloadNotice = "Salvato in " + path
|
||||||
hideNoticeTimer.restart()
|
hideNoticeTimer.restart()
|
||||||
} else {
|
} else {
|
||||||
page.downloadNotice = ""
|
page.downloadNotice = ""
|
||||||
|
page.readingNow = false
|
||||||
page.downloadError = "Errore: " + error
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QSslError>
|
#include <QSslError>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -134,6 +135,90 @@ static bool looksLikeImage(const QByteArray &data)
|
|||||||
|| data.startsWith("GIF8") || data.startsWith("RIFF");
|
|| data.startsWith("GIF8") || data.startsWith("RIFF");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ApiClient::postBookmark(int bookId, const QString &format, const QString &key)
|
||||||
|
{
|
||||||
|
const QString url = m_settings->baseUrl() + QStringLiteral("/ajax/bookmark/")
|
||||||
|
+ QString::number(bookId) + QLatin1Char('/') + format;
|
||||||
|
qDebug() << "postBookmark:" << url << "key=" << key;
|
||||||
|
|
||||||
|
const QUrl requestUrl(url);
|
||||||
|
QNetworkRequest request(requestUrl);
|
||||||
|
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||||
|
QLatin1String("application/x-www-form-urlencoded"));
|
||||||
|
applyAuth(request);
|
||||||
|
const QByteArray body = "bookmark=" + QUrl::toPercentEncoding(key);
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_nam.post(request, body);
|
||||||
|
if (m_settings->ignoreSslErrors()) {
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||||
|
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||||
|
&QNetworkReply::ignoreSslErrors));
|
||||||
|
}
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, url, key]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
qDebug() << "postBookmark status:" << status;
|
||||||
|
// 400/403 = probabile CSRF attivo: recupera il token dalla pagina di login e riprova
|
||||||
|
if ((status == 400 || status == 403) && !m_csrfRetried) {
|
||||||
|
m_csrfRetried = true;
|
||||||
|
fetchCsrfTokenAndRetry(url, key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiClient::fetchCsrfTokenAndRetry(const QString &url, const QString &key)
|
||||||
|
{
|
||||||
|
const QUrl loginUrl(m_settings->baseUrl() + QStringLiteral("/login"));
|
||||||
|
QNetworkRequest request(loginUrl);
|
||||||
|
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||||
|
applyAuth(request);
|
||||||
|
QNetworkReply *reply = m_nam.get(request);
|
||||||
|
if (m_settings->ignoreSslErrors()) {
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||||
|
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||||
|
&QNetworkReply::ignoreSslErrors));
|
||||||
|
}
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, url, key]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
const QByteArray body = reply->readAll();
|
||||||
|
// <input id="csrf_token" name="csrf_token" type="hidden" value="...">
|
||||||
|
QRegularExpression re(QStringLiteral(
|
||||||
|
"name=[\"']csrf_token[\"'][^>]*value=[\"']([^\"']+)[\"']"));
|
||||||
|
const QRegularExpressionMatch match = re.match(QString::fromUtf8(body));
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
postBookmarkWithCsrf(url, key, match.captured(1));
|
||||||
|
} else {
|
||||||
|
qDebug() << "postBookmark: token CSRF non trovato nella pagina di login";
|
||||||
|
}
|
||||||
|
m_csrfRetried = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiClient::postBookmarkWithCsrf(const QString &url, const QString &key,
|
||||||
|
const QString &csrfToken)
|
||||||
|
{
|
||||||
|
const QUrl requestUrl(url);
|
||||||
|
QNetworkRequest request(requestUrl);
|
||||||
|
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||||
|
QLatin1String("application/x-www-form-urlencoded"));
|
||||||
|
request.setRawHeader("X-CSRFToken", csrfToken.toUtf8());
|
||||||
|
applyAuth(request);
|
||||||
|
const QByteArray body = "bookmark=" + QUrl::toPercentEncoding(key);
|
||||||
|
QNetworkReply *reply = m_nam.post(request, body);
|
||||||
|
if (m_settings->ignoreSslErrors()) {
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||||
|
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||||
|
&QNetworkReply::ignoreSslErrors));
|
||||||
|
}
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [reply]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
qDebug() << "postBookmark (con CSRF) status:" << status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void ApiClient::fetchImage(const QString &url, const QString &id)
|
void ApiClient::fetchImage(const QString &url, const QString &id)
|
||||||
{
|
{
|
||||||
if (url.isEmpty()) {
|
if (url.isEmpty()) {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void getFeed(const QString &url);
|
Q_INVOKABLE void getFeed(const QString &url);
|
||||||
Q_INVOKABLE void fetchImage(const QString &url, const QString &id);
|
Q_INVOKABLE void fetchImage(const QString &url, const QString &id);
|
||||||
|
// sincronizza la posizione di lettura su calibre-web (POST /ajax/bookmark)
|
||||||
|
Q_INVOKABLE void postBookmark(int bookId, const QString &format, const QString &key);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void feedReady(const QVariantList &entries, const QString &nextUrl, const QString &feedTitle);
|
void feedReady(const QVariantList &entries, const QString &nextUrl, const QString &feedTitle);
|
||||||
@@ -31,10 +33,14 @@ private:
|
|||||||
QNetworkReply *startGet(const QUrl &url);
|
QNetworkReply *startGet(const QUrl &url);
|
||||||
void applyAuth(QNetworkRequest &request);
|
void applyAuth(QNetworkRequest &request);
|
||||||
QString resolveUrl(const QString &relative) const;
|
QString resolveUrl(const QString &relative) const;
|
||||||
|
void postBookmarkWithCsrf(const QString &url, const QString &key,
|
||||||
|
const QString &csrfToken);
|
||||||
|
void fetchCsrfTokenAndRetry(const QString &url, const QString &key);
|
||||||
|
|
||||||
Settings *m_settings;
|
Settings *m_settings;
|
||||||
QNetworkAccessManager m_nam;
|
QNetworkAccessManager m_nam;
|
||||||
QString m_cacheDir;
|
QString m_cacheDir;
|
||||||
|
bool m_csrfRetried = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // APICLIENT_H
|
#endif // APICLIENT_H
|
||||||
|
|||||||
+17
-2
@@ -129,7 +129,8 @@ QString readTextFile(const QString &path)
|
|||||||
// EpubBook
|
// EpubBook
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
EpubBook::EpubBook()
|
EpubBook::EpubBook(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +190,9 @@ bool EpubBook::open(const QString &epubPath)
|
|||||||
return false;
|
return false;
|
||||||
if (!parseOpf())
|
if (!parseOpf())
|
||||||
return false;
|
return false;
|
||||||
return !m_chapters.isEmpty();
|
const bool ok = !m_chapters.isEmpty();
|
||||||
|
emit changed();
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EpubBook::parseContainer()
|
bool EpubBook::parseContainer()
|
||||||
@@ -397,6 +400,18 @@ QString EpubBook::chapterTitle(int index) const
|
|||||||
return m_chapters.at(index).title;
|
return m_chapters.at(index).title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantList EpubBook::chapterList() const
|
||||||
|
{
|
||||||
|
QVariantList list;
|
||||||
|
for (int i = 0; i < m_chapters.size(); ++i) {
|
||||||
|
QVariantMap m;
|
||||||
|
m.insert(QStringLiteral("index"), i);
|
||||||
|
m.insert(QStringLiteral("title"), m_chapters.at(i).title);
|
||||||
|
list.append(m);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
QString EpubBook::makeBookmarkKey(int chapterIndex, double fraction) const
|
QString EpubBook::makeBookmarkKey(int chapterIndex, double fraction) const
|
||||||
{
|
{
|
||||||
return QStringLiteral("cw:v1:%1:%2").arg(chapterIndex).arg(fraction, 0, 'f', 4);
|
return QStringLiteral("cw:v1:%1:%2").arg(chapterIndex).arg(fraction, 0, 'f', 4);
|
||||||
|
|||||||
+21
-10
@@ -2,7 +2,9 @@
|
|||||||
#define EPUB_H
|
#define EPUB_H
|
||||||
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
struct EpubChapter
|
struct EpubChapter
|
||||||
{
|
{
|
||||||
@@ -11,27 +13,36 @@ struct EpubChapter
|
|||||||
QString title;
|
QString title;
|
||||||
};
|
};
|
||||||
|
|
||||||
class EpubBook
|
class EpubBook : public QObject
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(QString title READ title NOTIFY changed)
|
||||||
|
Q_PROPERTY(QString author READ author NOTIFY changed)
|
||||||
|
Q_PROPERTY(int chapterCount READ chapterCount NOTIFY changed)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
EpubBook();
|
explicit EpubBook(QObject *parent = 0);
|
||||||
~EpubBook();
|
~EpubBook();
|
||||||
|
|
||||||
bool open(const QString &epubPath);
|
Q_INVOKABLE bool open(const QString &epubPath);
|
||||||
void close();
|
Q_INVOKABLE void close();
|
||||||
|
|
||||||
QString title() const { return m_title; }
|
QString title() const { return m_title; }
|
||||||
QString author() const { return m_author; }
|
QString author() const { return m_author; }
|
||||||
QList<EpubChapter> chapters() const { return m_chapters; }
|
|
||||||
QString baseDir() const { return m_baseDir; }
|
|
||||||
int chapterCount() const { return m_chapters.size(); }
|
int chapterCount() const { return m_chapters.size(); }
|
||||||
|
|
||||||
QString chapterFilePath(int index) const;
|
Q_INVOKABLE QString chapterFilePath(int index) const;
|
||||||
QString chapterTitle(int index) const;
|
Q_INVOKABLE QString chapterTitle(int index) const;
|
||||||
|
// elenco [{index, title}] per la lista capitoli
|
||||||
|
Q_INVOKABLE QVariantList chapterList() const;
|
||||||
|
|
||||||
// chiave segno locale, es. "cw:v1:3:0.42" (capitolo 3, 42% del capitolo)
|
// chiave segno locale, es. "cw:v1:3:0.42" (capitolo 3, 42% del capitolo)
|
||||||
QString makeBookmarkKey(int chapterIndex, double fraction) const;
|
Q_INVOKABLE QString makeBookmarkKey(int chapterIndex, double fraction) const;
|
||||||
bool parseBookmarkKey(const QString &key, int *chapterIndex, double *fraction) const;
|
Q_INVOKABLE bool parseBookmarkKey(const QString &key, int *chapterIndex, double *fraction) const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void changed();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool extractZip(const QString &epubPath);
|
bool extractZip(const QString &epubPath);
|
||||||
|
|||||||
+4
-1
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "apiclient.h"
|
#include "apiclient.h"
|
||||||
#include "downloader.h"
|
#include "downloader.h"
|
||||||
|
#include "epub.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
|
||||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||||
@@ -13,7 +14,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
|||||||
QGuiApplication *app = SailfishApp::application(argc, argv);
|
QGuiApplication *app = SailfishApp::application(argc, argv);
|
||||||
QQuickView *view = SailfishApp::createView();
|
QQuickView *view = SailfishApp::createView();
|
||||||
|
|
||||||
qDebug() << "harbour-calibreweb v0.1.0 build" << __DATE__ << __TIME__;
|
qDebug() << "harbour-calibreweb v0.2.0 build" << __DATE__ << __TIME__;
|
||||||
|
|
||||||
|
qmlRegisterType<EpubBook>("harbour.calibreweb", 1, 0, "EpubBook");
|
||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
ApiClient apiClient(&settings);
|
ApiClient apiClient(&settings);
|
||||||
|
|||||||
@@ -87,3 +87,22 @@ QString Settings::baseUrl() const
|
|||||||
url.chop(1);
|
url.chop(1);
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Settings::bookmarkKey(int bookId, const QString &format) const
|
||||||
|
{
|
||||||
|
return m_settings.value(QStringLiteral("bookmarks/%1/%2").arg(bookId).arg(format))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setBookmarkKey(int bookId, const QString &format, const QString &key)
|
||||||
|
{
|
||||||
|
m_settings.setValue(QStringLiteral("bookmarks/%1/%2").arg(bookId).arg(format), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Settings::bookFilePath(int bookId, const QString &format) const
|
||||||
|
{
|
||||||
|
const QString dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||||
|
+ QStringLiteral("/books");
|
||||||
|
QDir().mkpath(dir);
|
||||||
|
return dir + QStringLiteral("/%1.%2").arg(bookId).arg(format);
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ public:
|
|||||||
// URL del server senza slash finale
|
// URL del server senza slash finale
|
||||||
QString baseUrl() const;
|
QString baseUrl() const;
|
||||||
|
|
||||||
|
// --- segnalibri (posizione di lettura, per libro+formato) ---
|
||||||
|
Q_INVOKABLE QString bookmarkKey(int bookId, const QString &format) const;
|
||||||
|
Q_INVOKABLE void setBookmarkKey(int bookId, const QString &format, const QString &key);
|
||||||
|
|
||||||
|
// path del file libro scaricato in cache (per il lettore)
|
||||||
|
Q_INVOKABLE QString bookFilePath(int bookId, const QString &format) const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void serverUrlChanged();
|
void serverUrlChanged();
|
||||||
void usernameChanged();
|
void usernameChanged();
|
||||||
|
|||||||
Reference in New Issue
Block a user