From 041b9b9567084330d4db0a649922e67133313785 Mon Sep 17 00:00:00 2001 From: Carlo Baratto Date: Thu, 3 Sep 2026 14:16:18 +0200 Subject: [PATCH] Audit best practice 0.4.8: orientazioni per pagina (reader in landscape), licenza nel pacchetto, EPUB lazy lengths, cover dinamica (ReaderState), flush config senza sync per tasto, pulizia chapterFraction/commenti --- harbour-calibreweb.pro | 11 +++++++---- qml/cover/CoverPage.qml | 29 +++++++++++++++++++++++++++ qml/harbour-calibreweb.qml | 5 +++-- qml/pages/BookDetailPage.qml | 2 ++ qml/pages/FeedPage.qml | 2 ++ qml/pages/MainPage.qml | 2 ++ qml/pages/ReaderPage.qml | 26 ++++++++++++++++++------ qml/pages/SearchPage.qml | 2 ++ qml/pages/SettingsPage.qml | 9 +++++++++ rpm/harbour-calibreweb.spec | 30 +++++++++++++++++++++++++++- src/epub.cpp | 36 ++++++++++++++++++++++------------ src/epub.h | 8 +++++--- src/main.cpp | 8 ++++++-- src/readerstate.cpp | 27 +++++++++++++++++++++++++ src/readerstate.h | 38 ++++++++++++++++++++++++++++++++++++ src/settings.cpp | 12 +++++++----- src/settings.h | 23 +++++++++++++--------- 17 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 src/readerstate.cpp create mode 100644 src/readerstate.h diff --git a/harbour-calibreweb.pro b/harbour-calibreweb.pro index ee02cf3..8abe925 100644 --- a/harbour-calibreweb.pro +++ b/harbour-calibreweb.pro @@ -10,14 +10,16 @@ SOURCES += \ src/apiclient.cpp \ src/downloader.cpp \ src/opdsparser.cpp \ - src/epub.cpp + src/epub.cpp \ + src/readerstate.cpp HEADERS += \ src/settings.h \ src/apiclient.h \ src/downloader.h \ src/opdsparser.h \ - src/epub.h + src/epub.h \ + src/readerstate.h LIBS += -lz @@ -33,8 +35,9 @@ DISTFILES += $$OTHER_FILES # Translations are NOT handled by the sailfishapp_i18n template on the 5.1 kit # (the .qm is never generated/installed reliably). The spec builds the .qm -# with lrelease and installs it into /usr/share/translations directly; -# libsailfishapp loads harbour-calibreweb_.qm automatically. +# with lrelease and installs it into the app data dir +# (/usr/share/harbour-calibreweb/translations — the RPM validator rejects +# /usr/share/translations); main.cpp loads it explicitly (see language block). # Source strings are ENGLISH (default language); harbour-calibreweb_it.ts # provides the Italian catalogue. diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index 7fb6f0f..27bcc61 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -1,10 +1,14 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +// Cover: se un libro è aperto (readerState attivo dal ReaderPage) mostra +// titolo e posizione di lettura, altrimenti icona e nome dell'app. CoverBackground { Column { anchors.centerIn: parent + width: parent.width - 2 * Theme.paddingLarge spacing: Theme.paddingMedium + visible: !readerState.active Image { anchors.horizontalCenter: parent.horizontalCenter @@ -20,4 +24,29 @@ CoverBackground { font.pixelSize: Theme.fontSizeSmall } } + + Column { + anchors.centerIn: parent + width: parent.width - 2 * Theme.paddingLarge + spacing: Theme.paddingSmall + visible: readerState.active + + Label { + width: parent.width + text: readerState.title + color: Theme.primaryColor + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignHCenter + truncationMode: TruncationMode.Elide + } + + Label { + width: parent.width + text: readerState.subtitle + color: Theme.secondaryColor + font.pixelSize: Theme.fontSizeExtraSmall + horizontalAlignment: Text.AlignHCenter + truncationMode: TruncationMode.Elide + } + } } diff --git a/qml/harbour-calibreweb.qml b/qml/harbour-calibreweb.qml index 3be7e99..a8f4c5c 100644 --- a/qml/harbour-calibreweb.qml +++ b/qml/harbour-calibreweb.qml @@ -9,6 +9,7 @@ ApplicationWindow { initialPage: Component { MainPage {} } cover: Component { CoverPage {} } - allowedOrientations: Orientation.Portrait - _defaultPageOrientations: Orientation.Portrait + // Niente vincoli a livello window: ogni pagina dichiara le proprie + // orientazioni (il lettore ruota in landscape, le pagine di lista + // restano in portrait). _defaultPageOrientations è API privata. } diff --git a/qml/pages/BookDetailPage.qml b/qml/pages/BookDetailPage.qml index 6f253af..b99e312 100644 --- a/qml/pages/BookDetailPage.qml +++ b/qml/pages/BookDetailPage.qml @@ -4,6 +4,8 @@ import Sailfish.Silica 1.0 Page { id: page + allowedOrientations: Orientation.Portrait + property var bookData: ({}) property string coverSource: "" property bool downloading: false diff --git a/qml/pages/FeedPage.qml b/qml/pages/FeedPage.qml index af008d3..8893691 100644 --- a/qml/pages/FeedPage.qml +++ b/qml/pages/FeedPage.qml @@ -4,6 +4,8 @@ import Sailfish.Silica 1.0 Page { id: page + allowedOrientations: Orientation.Portrait + property string feedUrl property string pageTitle property string nextUrl: "" diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index d138a41..778d33c 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -4,6 +4,8 @@ import Sailfish.Silica 1.0 Page { id: page + allowedOrientations: Orientation.Portrait + property bool loading: false property string errorMessage: "" diff --git a/qml/pages/ReaderPage.qml b/qml/pages/ReaderPage.qml index ce48e04..237fe10 100644 --- a/qml/pages/ReaderPage.qml +++ b/qml/pages/ReaderPage.qml @@ -18,7 +18,6 @@ Page { property string bookFormat: "epub" property int currentChapter: 0 - property double chapterFraction: 0.0 // posizione di lettura nel capitolo corrente, stimata in caratteri di testo // dalla frazione di scroll (stabile col reflow del font) property int charOffset: 0 @@ -276,11 +275,11 @@ Page { var max = Math.max(0, innerFlick.contentHeight - innerFlick.height) var y = Math.max(0, Math.min(max, innerFlick.contentY)) var len = book.chapterTextLength(page.currentChapter) - page.chapterFraction = max > 0 ? y / max : 0 // posizione per caratteri (stima dalla frazione: stabile col reflow) - page.charOffset = Math.round(page.chapterFraction * len) + page.charOffset = Math.round((max > 0 ? y / max : 0) * len) page.pageIndex = Math.floor(y / step) + 1 page.pageCount = Math.max(1, Math.ceil(max / step) + 1) + updateCoverState() maybeSave(false) } @@ -308,7 +307,6 @@ Page { return if (index !== page.currentChapter) { maybeSave(true) - page.chapterFraction = 0 page.charOffset = 0 page.lastSavedAbs = -1 } @@ -368,7 +366,6 @@ Page { } else if (page.currentChapter > 0) { maybeSave(true) page.currentChapter -= 1 - page.chapterFraction = 0 page.charOffset = 0 page.lastSavedAbs = -1 // sentinella "fine capitolo": va oltre la lunghezza, così @@ -383,6 +380,17 @@ Page { return Math.round(16 * page.fontScale * 100) / 100 } + // aggiorna lo stato per la cover (titolo + capitolo + percentuale) + function updateCoverState() { + if (book.chapterCount <= 0) + return + var pct = Math.round(page.overallProgress() * 100) + readerState.setReading(book.title(), + (page.currentChapter + 1) + "/" + book.chapterCount + + " · " + book.chapterTitle(page.currentChapter) + + " · " + pct + "%") + } + function showChapterList() { var dialog = pageStack.push(Qt.resolvedUrl("ChapterListDialog.qml"), { model: book.chapterList() }) @@ -424,11 +432,17 @@ Page { page.currentChapter = ch page.pendingRestore = true page.restoreFrac = off >= 0 ? offsetToFrac(off, ch) : 0.0 + updateCoverState() restoreTimer.start() } + Component.onDestruction: readerState.clearReading() + onStatusChanged: { - if (status === PageStatus.Inactive) { + if (status === PageStatus.Active) { + if (page.ready && book.chapterCount > 0) + updateCoverState() + } else if (status === PageStatus.Inactive) { maybeSave(true) } } diff --git a/qml/pages/SearchPage.qml b/qml/pages/SearchPage.qml index f36ab50..f320bba 100644 --- a/qml/pages/SearchPage.qml +++ b/qml/pages/SearchPage.qml @@ -4,6 +4,8 @@ import Sailfish.Silica 1.0 Page { id: page + allowedOrientations: Orientation.Portrait + SilicaFlickable { anchors.fill: parent contentHeight: column.height diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index 3ee118c..c0eef69 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -4,6 +4,8 @@ import Sailfish.Silica 1.0 Page { id: page + allowedOrientations: Orientation.Portrait + property string testResult: "" property bool testOk: false property bool testing: false @@ -171,11 +173,18 @@ Page { if (base.indexOf("http://") !== 0 && base.indexOf("https://") !== 0) base = "https://" + base appSettings.serverUrl = base + appSettings.flush() // i campi di testo scrivono senza sync a ogni tasto page.testing = true page.testResult = qsTr("Testing…") apiClient.getFeed(base + "/opds") } + // uscita dalla pagina: scrivi su disco le modifiche in sospeso + onStatusChanged: { + if (status === PageStatus.Inactive) + appSettings.flush() + } + Connections { target: apiClient onFeedReady: { diff --git a/rpm/harbour-calibreweb.spec b/rpm/harbour-calibreweb.spec index 57ca39d..2bced7d 100644 --- a/rpm/harbour-calibreweb.spec +++ b/rpm/harbour-calibreweb.spec @@ -1,6 +1,6 @@ Name: harbour-calibreweb Summary: Client OPDS per Calibre Web -Version: 0.4.7 +Version: 0.4.8 Release: 1 Group: Qt/Qt License: MIT @@ -24,6 +24,20 @@ reflow del testo, margini configurabili e posizione salvata in locale. %changelog +* Fri Aug 21 2026 Carlo Baratto - 0.4.8-1 +- Best practice audit: orientazioni per pagina (finestra senza vincoli, il + lettore ora ruota davvero in landscape; pagine di lista in portrait; + rimossa la proprieta' privata _defaultPageOrientations) +- Licenza MIT installata nel pacchetto (/usr/share/licenses/harbour-calibreweb) +- EPUB: lunghezze testo LAZY per capitolo (l'apertura non parse piu' tutto il + libro: niente freeze sui libri grossi; cache al primo accesso) +- Cover dinamica: con un libro aperto mostra titolo e posizione di lettura + (ReaderState esposto come context property, aggiornato dal ReaderPage) +- Impostazioni: niente sync su disco a ogni tasto nei campi di testo + (flush() all'uscita della pagina e prima del test connessione) +- Pulizia: rimossa proprieta' morta chapterFraction, commenti obsoleti + aggiornati (settings.h, .pro) + * Fri Aug 21 2026 Carlo Baratto - 0.4.7-1 - Fix margini lettore: il passo di pagina era la viewport intera, quindi il testo scorreva a filo dei bordi e i margini verticali si vedevano solo @@ -199,6 +213,19 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop || tr mkdir -p %{buildroot}%{_datadir}/%{name}/translations install -m 0644 %{_builddir}/%{name}_it.qm %{buildroot}%{_datadir}/%{name}/translations/ +# MIT license: installed in the package (Harbour best practice). +mkdir -p %{buildroot}%{_datadir}/licenses/%{name} +if [ -f %{_builddir}/../%{name}-%{version}/LICENSE ]; then + install -m 0644 %{_builddir}/../%{name}-%{version}/LICENSE %{buildroot}%{_datadir}/licenses/%{name}/ +elif [ -f %{_builddir}/%{name}-%{version}/LICENSE ]; then + install -m 0644 %{_builddir}/%{name}-%{version}/LICENSE %{buildroot}%{_datadir}/licenses/%{name}/ +elif [ -f %{_builddir}/LICENSE ]; then + install -m 0644 %{_builddir}/LICENSE %{buildroot}%{_datadir}/licenses/%{name}/ +else + echo "ERROR: LICENSE not found" >&2 + exit 1 +fi + %files %defattr(-,root,root,-) %{_bindir}/%{name} @@ -209,3 +236,4 @@ install -m 0644 %{_builddir}/%{name}_it.qm %{buildroot}%{_datadir}/%{name}/trans %{_datadir}/icons/hicolor/172x172/apps/%{name}.png %{_datadir}/%{name}/qml %{_datadir}/%{name}/translations/%{name}*.qm +%{_datadir}/licenses/%{name}/LICENSE diff --git a/src/epub.cpp b/src/epub.cpp index 4582d7b..5249500 100644 --- a/src/epub.cpp +++ b/src/epub.cpp @@ -490,8 +490,14 @@ bool EpubBook::open(const QString &epubPath) if (!parseOpf()) return false; const bool ok = !m_chapters.isEmpty(); - if (ok) - computeTextLengths(); + if (ok) { + // lunghezze testo LAZY: -1 = da calcolare al primo accesso. L'apertura + // resta veloce anche per libri grossi (niente parse di tutti i capitoli). + m_textLengths.clear(); + m_textLengths.reserve(m_chapters.size()); + for (int i = 0; i < m_chapters.size(); ++i) + m_textLengths.append(-1); + } emit changed(); return ok; } @@ -763,24 +769,26 @@ QString EpubBook::chapterRichText(int index) const return htmlToRichText(html); } -void EpubBook::computeTextLengths() +// calcola (e cache) la lunghezza del capitolo index se non ancora nota +void EpubBook::ensureTextLength(int index) const { - m_textLengths.clear(); - for (const EpubChapter &c : m_chapters) { - QFile f(c.path); - if (!f.open(QIODevice::ReadOnly)) { - m_textLengths.append(0); - continue; - } - const QString html = QString::fromUtf8(f.readAll()); - m_textLengths.append(plainTextLength(html)); + if (index < 0 || index >= m_textLengths.size() || m_textLengths.at(index) >= 0) + return; + const EpubChapter &c = m_chapters.at(index); + QFile f(c.path); + if (!f.open(QIODevice::ReadOnly)) { + m_textLengths[index] = 0; + return; } + const QString html = QString::fromUtf8(f.readAll()); + m_textLengths[index] = plainTextLength(html); } int EpubBook::chapterTextLength(int index) const { if (index < 0 || index >= m_textLengths.size()) return 0; + ensureTextLength(index); return m_textLengths.at(index); } @@ -788,8 +796,10 @@ qint64 EpubBook::chapterStartChar(int index) const { qint64 start = 0; const int end = qMin(index, m_textLengths.size()); - for (int i = 0; i < end; ++i) + for (int i = 0; i < end; ++i) { + ensureTextLength(i); start += m_textLengths.at(i); + } return start; } diff --git a/src/epub.h b/src/epub.h index fef98c9..9c95ce7 100644 --- a/src/epub.h +++ b/src/epub.h @@ -66,15 +66,17 @@ private: void parseTocEpub3(const QString &navPath); void parseTocEpub2(const QString &ncxPath); QString absolutePath(const QString &relativeToOpfDir) const; - // lunghezze in caratteri del testo visibile, calcolate all'open - void computeTextLengths(); + // calcola (e cache) la lunghezza in caratteri del capitolo al primo accesso + void ensureTextLength(int index) const; QString m_baseDir; QString m_opfDir; QString m_title; QString m_author; QList m_chapters; - QList m_textLengths; + // lunghezze testo per capitolo, calcolate LAZY al primo accesso + // (-1 = non ancora calcolata); mutable per la cache da metodi const + mutable QList m_textLengths; }; #endif // EPUB_H diff --git a/src/main.cpp b/src/main.cpp index cff12ce..d6abab0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "apiclient.h" #include "downloader.h" #include "epub.h" +#include "readerstate.h" #include "settings.h" Q_DECL_EXPORT int main(int argc, char *argv[]) @@ -22,7 +23,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) app->setOrganizationName(QStringLiteral("harbour")); app->setApplicationName(QStringLiteral("calibreweb")); // versione mostrata nelle impostazioni (tenere allineata allo spec) - app->setApplicationVersion(QStringLiteral("0.4.7")); + app->setApplicationVersion(QStringLiteral("0.4.8")); Settings settings; @@ -56,16 +57,19 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) QQuickView *view = SailfishApp::createView(); - qDebug() << "harbour-calibreweb v0.4.7 build" << __DATE__ << __TIME__; + qDebug() << "harbour-calibreweb v0.4.8 build" << __DATE__ << __TIME__; qmlRegisterType("harbour.calibreweb", 1, 0, "EpubBook"); ApiClient apiClient(&settings); Downloader downloader(&settings); + // stato di lettura corrente (per la cover quando l'app va in background) + ReaderState readerState(&settings); view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("apiClient"), &apiClient); view->rootContext()->setContextProperty(QStringLiteral("downloader"), &downloader); + view->rootContext()->setContextProperty(QStringLiteral("readerState"), &readerState); view->rootContext()->setContextProperty(QStringLiteral("appVersion"), QCoreApplication::applicationVersion()); diff --git a/src/readerstate.cpp b/src/readerstate.cpp new file mode 100644 index 0000000..111fe55 --- /dev/null +++ b/src/readerstate.cpp @@ -0,0 +1,27 @@ +#include "readerstate.h" + +ReaderState::ReaderState(QObject *parent) + : QObject(parent) + , m_active(false) +{ +} + +void ReaderState::setReading(const QString &title, const QString &subtitle) +{ + if (m_active && m_title == title && m_subtitle == subtitle) + return; + m_active = true; + m_title = title; + m_subtitle = subtitle; + emit changed(); +} + +void ReaderState::clearReading() +{ + if (!m_active) + return; + m_active = false; + m_title.clear(); + m_subtitle.clear(); + emit changed(); +} diff --git a/src/readerstate.h b/src/readerstate.h new file mode 100644 index 0000000..e5afffb --- /dev/null +++ b/src/readerstate.h @@ -0,0 +1,38 @@ +#ifndef READERSTATE_H +#define READERSTATE_H + +#include +#include + +// Stato di lettura corrente, esposto come context property "readerState": +// la cover mostra il libro aperto (titolo + posizione) invece dell'icona fissa. +class ReaderState : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool active READ active NOTIFY changed) + Q_PROPERTY(QString title READ title NOTIFY changed) + Q_PROPERTY(QString subtitle READ subtitle NOTIFY changed) + +public: + explicit ReaderState(QObject *parent = 0); + + bool active() const { return m_active; } + QString title() const { return m_title; } + QString subtitle() const { return m_subtitle; } + + // aggiorna lo stato quando il lettore mostra un libro (chiamata dal QML) + Q_INVOKABLE void setReading(const QString &title, const QString &subtitle); + // il libro è stato chiuso: la cover torna all'icona predefinita + Q_INVOKABLE void clearReading(); + +signals: + void changed(); + +private: + bool m_active; + QString m_title; + QString m_subtitle; +}; + +#endif // READERSTATE_H diff --git a/src/settings.cpp b/src/settings.cpp index 4bd0a67..07c7d35 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -115,9 +115,9 @@ void Settings::setServerUrl(const QString &value) { if (value == serverUrl()) return; + // niente sync a ogni tasto (flash wear): il flush avviene all'uscita + // dalle impostazioni o prima delle richieste di rete (vedi flush()) m_settings.setValue(QStringLiteral("serverUrl"), value); - m_settings.sync(); // scrivi subito su disco: l'app può crashare alla chiusura (WebView) - openPermissions(); emit serverUrlChanged(); emit baseUrlChanged(); } @@ -132,8 +132,6 @@ void Settings::setUsername(const QString &value) if (value == username()) return; m_settings.setValue(QStringLiteral("username"), value); - m_settings.sync(); - openPermissions(); emit usernameChanged(); } @@ -147,9 +145,13 @@ void Settings::setPassword(const QString &value) if (value == password()) return; m_settings.setValue(QStringLiteral("password"), value); + emit passwordChanged(); +} + +void Settings::flush() +{ m_settings.sync(); openPermissions(); - emit passwordChanged(); } bool Settings::ignoreSslErrors() const diff --git a/src/settings.h b/src/settings.h index d923110..c768b82 100644 --- a/src/settings.h +++ b/src/settings.h @@ -55,6 +55,10 @@ public: QString downloadDir() const; + // scrive subito su disco le modifiche in sospeso (i setter di testo non + // fanno sync a ogni tasto: chiamare questo all'uscita dalle impostazioni) + Q_INVOKABLE void flush(); + // Header "Authorization: Basic ..." pronto da usare, vuoto se utente non impostato QString authHeader() const; @@ -80,17 +84,18 @@ signals: void baseUrlChanged(); private: - // Path fisso condiviso: l'app deve leggere/scrivere SEMPRE lo stesso file, - // sia avviata dal launcher (utente nemo) sia da SSH di debug (defaultuser). - // Con QSettings("harbour","calibreweb") il file dipende dalla HOME - // dell'utente che avvia -> configurazione invisibile dall'altro utente. + // Path canonico per la sandbox SailJail (Sailfish 4.4+/5.x): con org/app + // impostati (harbour/calibreweb, vedi main.cpp e X-Sailjail nel desktop + // file), AppConfigLocation = ~/.config/harbour/calibreweb, l'unica zona + // config PERSISTENTE della sandbox (senza profilo la sandbox la nasconde). static QString configPath(); - // Importa i valori dalla vecchia config per-utente (~/.config/harbour/...) se - // il file condiviso e' vuoto: copre la configurazione fatta prima del fix. + // Importa i valori dal vecchio file per-utente (~/.config/harbour/...) se + // il file canonico e' vuoto: copre la configurazione fatta prima del fix. void migrateIfNeeded(); - // Apre i permessi della catena /home/nemo/.config/harbour e del file: - // il launcher gira come nemo (home 700), il debug SSH come defaultuser — - // entrambi devono poter leggere/scrivere lo stesso file di configurazione. + // Apre i permessi della catena di directory del file di configurazione: + // l'avvio dal launcher e quello da SSH (defaultuser) devono poter + // leggere/scrivere lo stesso file. Efficace quando l'app gira come + // owner della home (dal launcher). void openPermissions(); QSettings m_settings;