From b79d97f9851725622a19af8eae1b6e9d335e0b77 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Sat, 26 Sep 2026 13:11:44 +0200 Subject: [PATCH] Unify displayed book status across library and details --- apps/readest-sync/CMakeLists.txt | 2 +- apps/readest-sync/CONTEXT.md | 4 +++ apps/readest-sync/src/book_status.cpp | 21 +++++++++++ apps/readest-sync/src/book_status.h | 6 ++++ apps/readest-sync/src/controller.cpp | 10 ++---- apps/readest-sync/src/library_model.cpp | 17 ++------- apps/readest-sync/src/library_model.h | 1 - apps/readest-sync/tests/qt_ui_test.cpp | 46 ++++++++++++++++++++++++- 8 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 apps/readest-sync/src/book_status.cpp create mode 100644 apps/readest-sync/src/book_status.h diff --git a/apps/readest-sync/CMakeLists.txt b/apps/readest-sync/CMakeLists.txt index 8ecf9dd..d07e8d3 100644 --- a/apps/readest-sync/CMakeLists.txt +++ b/apps/readest-sync/CMakeLists.txt @@ -67,7 +67,7 @@ if(READEST_DESKTOP OR READEST_HOST_TESTS) add_subdirectory(tests) endif() set(PRESENTATION_SOURCES src/controller.cpp src/controller.h src/library_model.cpp src/library_model.h - src/operation_runner.cpp src/device_adapter.cpp src/cover_loader.cpp src/thumbnail_cache.cpp) + src/book_status.cpp src/operation_runner.cpp src/device_adapter.cpp src/cover_loader.cpp src/thumbnail_cache.cpp) if(NOT READEST_HOST_TESTS OR READEST_DESKTOP) add_library(readest-presentation STATIC ${PRESENTATION_SOURCES}) set_target_properties(readest-presentation PROPERTIES AUTOMOC ON) diff --git a/apps/readest-sync/CONTEXT.md b/apps/readest-sync/CONTEXT.md index 2e49101..ad5273e 100644 --- a/apps/readest-sync/CONTEXT.md +++ b/apps/readest-sync/CONTEXT.md @@ -13,6 +13,10 @@ Readest library. A book represented in both the PocketBook and Readest libraries, shown as one entry in Readest Sync. +**Book status**: +The displayed summary of a book's availability and relationship to Readest, +including removal from Readest or a pending upload. + **PocketBook reading position**: The reading location saved by the PocketBook native reader for a device book. diff --git a/apps/readest-sync/src/book_status.cpp b/apps/readest-sync/src/book_status.cpp new file mode 100644 index 0000000..e925a67 --- /dev/null +++ b/apps/readest-sync/src/book_status.cpp @@ -0,0 +1,21 @@ +#include "book_status.h" +#include "application.h" + +QString bookStatusLabel(const readest::LibraryEntry& entry) { + using readest::Availability; + if(entry.book.book.deleted) return "Removed from Readest"; + if(entry.upload_pending) return "Upload pending"; + if(entry.book.local_only) return "PocketBook only"; + if(entry.availability==Availability::OnDevice && entry.book.epubs==0) + return "On device · Readest progress only"; + switch(entry.availability) { + case Availability::OnDevice: return "On device"; + case Availability::Downloadable: return "Available to download"; + case Availability::ProgressOnly: return "Progress only"; + case Availability::Unknown: return "Not checked"; + case Availability::Unavailable: return "EPUB unavailable"; + case Availability::Multiple: return "Multiple EPUBs"; + case Availability::Removed: return "Removed from cloud"; + } + return {}; +} diff --git a/apps/readest-sync/src/book_status.h b/apps/readest-sync/src/book_status.h new file mode 100644 index 0000000..bf03dd4 --- /dev/null +++ b/apps/readest-sync/src/book_status.h @@ -0,0 +1,6 @@ +#pragma once +#include + +namespace readest { struct LibraryEntry; } + +QString bookStatusLabel(const readest::LibraryEntry& entry); diff --git a/apps/readest-sync/src/controller.cpp b/apps/readest-sync/src/controller.cpp index 28d4fa4..539917c 100644 --- a/apps/readest-sync/src/controller.cpp +++ b/apps/readest-sync/src/controller.cpp @@ -1,4 +1,5 @@ #include "controller.h" +#include "book_status.h" #include #include using namespace readest; @@ -106,7 +107,7 @@ QString AppController::blockingMessage() const { } QString AppController::hint() const { const auto* entry=library_.find(selected_); if(!entry) return {}; - auto text=(entry->book.book.deleted?QStringLiteral("Removed from Readest"):entry->book.local_only?QStringLiteral("PocketBook only"):availabilityLabel(entry->availability))+". "+QString::fromStdString(entry->book.book.author); + auto text=bookStatusLabel(*entry)+". "+QString::fromStdString(entry->book.book.author); if(!entry->book.local_only && !entry->book.book.deleted && entry->book.epubs==0) text+=entry->availability==Availability::OnDevice?"\nReadest has progress only. You can upload this local EPUB.":"\nReadest has progress only. No local EPUB is available to upload."; text+="\nPocketBook: "+percentageLabel(entry->local_percentage)+" · Readest: "+percentageLabel(entry->remote_percentage); @@ -114,14 +115,9 @@ QString AppController::hint() const { } QVariantMap AppController::book() const { const auto* entry=library_.find(selected_); if(!entry) return {}; - auto availability=entry->book.book.deleted?QStringLiteral("Removed from Readest"): - entry->upload_pending?QStringLiteral("Upload pending"): - entry->book.local_only?QStringLiteral("PocketBook only"):availabilityLabel(entry->availability); - if(entry->availability==Availability::OnDevice && entry->book.epubs==0) - availability=QStringLiteral("On device · Readest progress only"); return {{"title",QString::fromStdString(entry->book.book.title.empty()?"Untitled":entry->book.book.title)}, {"author",QString::fromStdString(entry->book.book.author)}, - {"availability",availability},{"coverPath",QString::fromStdString(entry->cover)}, + {"availability",bookStatusLabel(*entry)},{"coverPath",QString::fromStdString(entry->cover)}, {"pocketBookProgress",percentageLabel(entry->local_percentage)}, {"readestProgress",percentageLabel(entry->remote_percentage)}}; } diff --git a/apps/readest-sync/src/library_model.cpp b/apps/readest-sync/src/library_model.cpp index 337bad9..08194a3 100644 --- a/apps/readest-sync/src/library_model.cpp +++ b/apps/readest-sync/src/library_model.cpp @@ -1,19 +1,7 @@ #include "library_model.h" +#include "book_status.h" #include QString percentageLabel(double value) { return value<0?QStringLiteral("—"):QString::number(value,'f',1)+"%"; } -QString availabilityLabel(readest::Availability value) { - using readest::Availability; - switch(value) { - case Availability::OnDevice: return "On device"; - case Availability::Downloadable: return "Available to download"; - case Availability::ProgressOnly: return "Progress only"; - case Availability::Unknown: return "Not checked"; - case Availability::Unavailable: return "EPUB unavailable"; - case Availability::Multiple: return "Multiple EPUBs"; - case Availability::Removed: return "Removed from cloud"; - } - return {}; -} int LibraryModel::rowCount(const QModelIndex& parent) const { return parent.isValid()?0:std::max(0,std::min(capacity_,count()-page_*capacity_)); } @@ -27,8 +15,7 @@ QVariant LibraryModel::data(const QModelIndex& index,int role) const { case BookHash: return QString::fromStdString(entry.id.hash); case Title: return QString::fromStdString(book.title.empty()?"Untitled":book.title); case Author: return QString::fromStdString(book.author); - case Availability: return book.deleted?QStringLiteral("Removed from Readest"):entry.upload_pending?QStringLiteral("Upload pending"):entry.book.local_only?QStringLiteral("PocketBook only"): - entry.availability==readest::Availability::OnDevice && entry.book.epubs==0?QStringLiteral("On device · Readest progress only"):availabilityLabel(entry.availability); + case Availability: return bookStatusLabel(entry); case Cover: return QString::fromStdString(entry.cover); case LocalPath: return QString::fromStdString(entry.book.path); case LocalProgress: return percentageLabel(entry.local_percentage); diff --git a/apps/readest-sync/src/library_model.h b/apps/readest-sync/src/library_model.h index fe337c3..6d469b8 100644 --- a/apps/readest-sync/src/library_model.h +++ b/apps/readest-sync/src/library_model.h @@ -2,7 +2,6 @@ #include "application.h" #include -QString availabilityLabel(readest::Availability value); QString percentageLabel(double value); class LibraryModel : public QAbstractListModel { Q_OBJECT diff --git a/apps/readest-sync/tests/qt_ui_test.cpp b/apps/readest-sync/tests/qt_ui_test.cpp index 5eeb5f6..85a7dd3 100644 --- a/apps/readest-sync/tests/qt_ui_test.cpp +++ b/apps/readest-sync/tests/qt_ui_test.cpp @@ -1,5 +1,6 @@ // Link the production controller/model/runner normally; test public contracts. #include "controller.h" +#include "book_status.h" #include "cover_provider.h" #include "network_route.h" #include @@ -55,6 +56,49 @@ static ApplicationConfig configAt(const QString& base) { assert(QDir().mkpath(QString::fromStdString(config.root))); return config; } +static void bookStatusChecks() { + struct Scenario { + Availability availability; + int epubs; + bool deleted,upload_pending,local_only; + const char* expected; + }; + const Scenario scenarios[]={ + {Availability::OnDevice,0,true,true,false,"Removed from Readest"}, + {Availability::OnDevice,0,true,false,false,"Removed from Readest"}, + {Availability::OnDevice,0,false,true,false,"Upload pending"}, + {Availability::OnDevice,0,false,true,true,"Upload pending"}, + {Availability::OnDevice,0,false,false,true,"PocketBook only"}, + {Availability::OnDevice,0,false,false,false,"On device · Readest progress only"}, + {Availability::OnDevice,1,false,false,false,"On device"}, + {Availability::Downloadable,1,false,false,false,"Available to download"}, + {Availability::ProgressOnly,0,false,false,false,"Progress only"}, + {Availability::Unknown,0,false,false,false,"Not checked"}, + {Availability::Unavailable,0,false,false,false,"EPUB unavailable"}, + {Availability::Multiple,2,false,false,false,"Multiple EPUBs"}, + {Availability::Removed,0,false,false,false,"Removed from cloud"}, + {Availability::Removed,0,true,false,false,"Removed from Readest"} + }; + QTemporaryDir temp; assert(temp.isValid()); + AppController control(configAt(temp.path()),DeviceAccess{}); + assert(control.book().isEmpty() && control.hint().isEmpty()); + for(const auto& scenario:scenarios) { + LibraryEntry entry; + entry.id={"fixture-user",std::string(32,'a')}; + entry.book.book.author="Fixture author"; + entry.availability=scenario.availability;entry.book.epubs=scenario.epubs; + entry.book.book.deleted=scenario.deleted;entry.upload_pending=scenario.upload_pending; + entry.book.local_only=scenario.local_only; + const auto expected=QString::fromUtf8(scenario.expected); + assert(bookStatusLabel(entry)==expected); + auto* model=control.library(); + model->replace({entry}); + control.selectBook(QString::fromStdString(entry.id.account),QString::fromStdString(entry.id.hash)); + assert(model->data(model->index(0),LibraryModel::Availability).toString()==expected); + assert(control.book().value("availability").toString()==expected); + assert(control.hint().startsWith(expected+". Fixture author\n")); + } +} static void runnerChecks() { { // An online operation must hold standby off before waking Wi-Fi, @@ -219,7 +263,7 @@ static void runnerChecks() { } int main(int argc,char** argv) { QQuickWindow::setGraphicsApi(QSGRendererInterface::Software); - QGuiApplication app(argc,argv); runnerChecks(); + QGuiApplication app(argc,argv); bookStatusChecks(); runnerChecks(); QTemporaryDir temp; assert(temp.isValid()); auto config=configAt(temp.path()); std::atomic cover_requests{0};