Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/readest-sync/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/book_status.cpp src/operation_runner.cpp src/device_adapter.cpp src/cover_loader.cpp src/thumbnail_cache.cpp)
src/book_status.cpp src/book_actions.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)
Expand Down
4 changes: 4 additions & 0 deletions apps/readest-sync/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ entry in Readest Sync.
The displayed summary of a book's availability and relationship to Readest,
including removal from Readest or a pending upload.

**Book action**:
An operation offered for a book in its current state, such as opening a local
copy, downloading an EPUB or choosing which reading position to use.

**PocketBook reading position**:
The reading location saved by the PocketBook native reader for a device book.

Expand Down
3 changes: 3 additions & 0 deletions apps/readest-sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ Matches and candidate-index updates are each committed as a batch.

The library snapshot loads saved sync metadata in one query and native percentages
in one grouped query. Cover checks read headers rather than whole image files.
Sync, Open and Upload read the PocketBook reading position directly in a short,
read-only transaction that keeps identity and settings consistent, including WAL
changes. They no longer copy the native database to a temporary capture file.
The scale regression uses 500 valid EPUBs totaling roughly 500 MiB and checks
content-read/validation counts, cache persistence, path invalidation and new cloud
matches. It prints host timings as diagnostics; these are not device timings.
Expand Down
32 changes: 6 additions & 26 deletions apps/readest-sync/qml/BookDetailsPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,15 @@ Item {
objectName: "nativeBookDetailsPage"
required property var shell
property var details: shell.view.book || ({})
property var availableActions: shell.view.actions || []
readonly property var actionPresentation: shell.view.actionPresentation || ({})
property bool menuOpen: false
property bool activating: false
readonly property bool choosing: availableActions.length > 0 && availableActions[0].command.indexOf("copy:") === 0
readonly property bool conflict: shell.view.decision
readonly property var primaryAction: choosing || conflict ? null : firstContentAction()
readonly property var secondaryActions: contentActions(false)
readonly property var menuActions: contentActions(true)
readonly property bool choosing: actionPresentation.choosing || false
readonly property var primaryAction: actionPresentation.primary || null
readonly property var secondaryActions: actionPresentation.secondary || []
readonly property var menuActions: actionPresentation.menu || []
readonly property bool hasMenu: menuActions.length > 0

function firstContentAction() {
for (let action of availableActions)
if (action.command !== "back" && action.command !== "uploadCover") return action
return null
}
function contentActions(menu) {
let result = []
if (conflict) return result
for (let action of availableActions) {
if (action.command === "back") continue
if (menu) {
if (action.command === "uploadCover") result.push(action)
} else if (choosing || action.command !== (primaryAction ? primaryAction.command : "")) {
if (action.command !== "uploadCover") result.push(action)
}
}
return result
}
function run(command) {
if (activating) return
activating = true
Expand Down Expand Up @@ -144,8 +125,7 @@ Item {
width: parent.width - 2 * shell.metrics.largeGap
x: shell.metrics.largeGap
height: shell.metrics.menuRow
text: root.availableActions[0] && root.availableActions[0].command.indexOf("copy:") === 0 ?
"CHOOSE LOCAL COPY" : "CHOOSE READING POSITION"
text: "CHOOSE LOCAL COPY"
verticalAlignment: Text.AlignVCenter
font.pixelSize: shell.metrics.smallFont
font.bold: true
Expand Down
10 changes: 1 addition & 9 deletions apps/readest-sync/qml/DecisionDialog.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@ Item {
objectName: "nativeDecisionDialog"
required property var shell
z: 60
property var choices: decisionChoices()

function decisionChoices() {
if (!shell.view.decision) return []
let result = []
for (let action of shell.view.actions || [])
if (action.command !== "back") result.push(action)
return result
}
readonly property var choices: shell.view.actionPresentation ? shell.view.actionPresentation.choices : []

Rectangle { anchors.fill: parent; color: "#dddddd"; opacity: 0.92 }
Rectangle {
Expand Down
2 changes: 1 addition & 1 deletion apps/readest-sync/qml/LibraryPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Item {
}

function openBook(account, hash) {
if (activating) return
if (activating || shell.view.busy) return
activating = true
menuOpen = false
filterMenuOpen = false
Expand Down
15 changes: 10 additions & 5 deletions apps/readest-sync/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ Window {

NativeMetrics { id: nativeMetrics; viewportWidth: window.width; viewportHeight: window.height }

function toggleMenu() {
if (!view.busy && !view.decision && !view.blocking &&
pageLoader.item && pageLoader.item.toggleMenu &&
(window.libraryActive || pageLoader.item.hasMenu))
pageLoader.item.toggleMenu()
}

function handleHardwareButton(key) {
const normalizedKey = Number(key)
if (normalizedKey === Qt.Key_Menu) {
if (!view.busy && !view.decision && !view.blocking &&
pageLoader.item && pageLoader.item.toggleMenu &&
(window.libraryActive || pageLoader.item.hasMenu))
pageLoader.item.toggleMenu()
window.toggleMenu()
return
}
if (pageLoader.item && pageLoader.item.handleKey && pageLoader.item.handleKey(normalizedKey)) return
Expand All @@ -40,13 +44,14 @@ Window {

NativeHeader {
id: header
objectName: "nativeHeader"
width: parent.width
height: window.metrics.chrome
title: window.view.detail ? "BOOK INFO / " + (window.view.title || "Untitled") : window.view.title || "Readest Sync"
showBack: window.view.detail || window.view.signingIn
actionText: window.libraryActive || (pageLoader.item && pageLoader.item.hasMenu) ? "Menu" : ""
onBack: window.handleHardwareButton(Qt.Key_Back)
onAction: if (pageLoader.item && pageLoader.item.toggleMenu) pageLoader.item.toggleMenu()
onAction: window.toggleMenu()
onClose: window.view.close()
}
FocusScope {
Expand Down
11 changes: 2 additions & 9 deletions apps/readest-sync/src/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,6 @@ ManagedBook ApplicationService::resolve(const BookId& id) {
}
throw std::runtime_error("This book is no longer in the library.");
}
NativePosition ApplicationService::capture(const std::string& path) {
const auto file=config_.root+"/capture-"+std::to_string(getpid())+"-"+std::to_string(++sequence_)+".db";
try {
backup_native_database(config_.database,file);
auto result=native_position(file,path); unlink(file.c_str()); return result;
} catch(...) { unlink(file.c_str()); throw; }
}
LibrarySnapshot ApplicationService::snapshot() {
trace("snapshot.begin");
LibrarySnapshot result; result.initialized=bool(state_ && cloud_);
Expand Down Expand Up @@ -123,7 +116,7 @@ void ApplicationService::synchronize(const Request& request, OperationResult& re
if(book.book.deleted) throw std::runtime_error("Removed from Readest. Re-upload this book or read offline.");
if(book.local_only) throw std::runtime_error("Upload this book to Readest before synchronizing.");
state_->remember_integrity(book);
const auto native=capture(book.path);
const auto native=native_position(config_.database,book.path);
const bool open=request.command==Command::Open;
const auto audit=config_.root+"/native-"+std::to_string(time(nullptr))+"-"+std::to_string(getpid())+"-"+std::to_string(++sequence_);
const auto transition=transition_progress(*cloud_,*state_,verified,native,time(nullptr),request.choice,request.revision,
Expand Down Expand Up @@ -232,7 +225,7 @@ OperationResult ApplicationService::execute(const Request& request,const std::at
const VerifiedManagedBook verified(resolve(request.book));
state_->remember_integrity(verified.book());
UploadPosition position;
try {position.native=capture(verified.book().path);}
try {position.native=native_position(config_.database,verified.book().path);}
catch(const UnsupportedNativePosition& e) {position.error=e.what();position.unsupported=true;}
catch(const std::exception& e) {position.error=e.what();}
const auto uploaded=upload_book(*cloud_,*state_,verified,position,config_.transport,config_.ca,config_.root,cancel);
Expand Down
1 change: 0 additions & 1 deletion apps/readest-sync/src/application.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ class ApplicationService {
void check_cancel(const std::atomic<bool>& cancel) const;
size_t scan(const std::atomic<bool>& cancel);
LibrarySnapshot snapshot();
NativePosition capture(const std::string& path);
ManagedBook resolve(const BookId& id);
void synchronize(const Request& request, OperationResult& result, const std::atomic<bool>& cancel);
};
Expand Down
68 changes: 68 additions & 0 deletions apps/readest-sync/src/book_actions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include "book_actions.h"
#include "library_model.h"

QVariantMap BookActions::presentation() const {
return {{"primary",primary.isEmpty()?QVariant():QVariant(primary)},{"secondary",secondary},{"menu",menu},
{"choices",choices},{"choosing",choosing}};
}

BookActions bookActions(const readest::LibraryEntry& entry,bool signedIn,
BookChoice choice,BookBlock block) {
using readest::Availability;
BookActions result;
const bool conflict=choice==BookChoice::OpenConflict || choice==BookChoice::SyncConflict;
enum class Place { Content, Menu, Navigation };
auto add=[&](const QString& command,const QString& text,Place place=Place::Content) {
const QVariantMap action{{"command",command},{"text",text}};
result.allowed.append(action);
if(place==Place::Navigation) return;
if(conflict) result.choices.append(action);
else if(place==Place::Menu) result.menu.append(action);
else if(result.choosing || !result.primary.isEmpty()) result.secondary.append(action);
else result.primary=action;
};
auto back=[&] { add("back","Back to library",Place::Navigation); };
if(block!=BookBlock::None) {
if(block==BookBlock::Pending) return result;
add(entry.availability==Availability::OnDevice?"sync":"refresh",
entry.availability==Availability::OnDevice?"Sync now":"Check availability");
back(); return result;
}
if(entry.book.needs_copy_choice || choice==BookChoice::LocalCopy) {
result.choosing=!entry.book.copies.empty();
for(size_t i=0;i<entry.book.copies.size();++i) {
const auto& copy=entry.book.copies[i];
add("copy:"+QString::number(i),QString::fromStdString(copy.path)+" · "+percentageLabel(copy.percentage));
}
back(); return result;
}
if(choice==BookChoice::OpenConflict) {
add("openPocketBook","Open at PocketBook position"); add("openReadest","Open at Readest position");
back(); return result;
}
if(choice==BookChoice::SyncConflict) {
add("usePocketBook","Use PocketBook position"); add("useReadest","Use Readest position");
back(); return result;
}
if(entry.upload_pending && signedIn) {
add("upload","Retry upload / reading position"); back(); return result;
}
if(entry.book.local_only || entry.book.book.deleted || !signedIn) {
add("offline","Open at PocketBook position");
if(signedIn) add("upload",entry.book.book.deleted?"Re-upload to Readest":entry.upload_pending?"Retry upload":"Upload to Readest");
else add("signin","Sign in to upload");
if(entry.book.copies.size()>1) add("copies","Choose local copy");
back(); return result;
}
if(entry.book.copies.size()>1) add("copies","Choose local copy");
if(entry.availability==Availability::OnDevice) {
add("open",entry.sync.pending_remote.empty()?"Open":"Open at Readest position");
add("sync","Sync now"); add("offline","Read offline");
} else if(entry.availability==Availability::Downloadable) add("download","Download EPUB");
else add("refresh","Check availability");
if(entry.availability==Availability::OnDevice && entry.book.epubs==0)
add("upload","Upload EPUB to Readest");
if(entry.availability==Availability::OnDevice && entry.book.epubs>0)
add("uploadCover","Upload cover to Readest",Place::Menu);
back(); return result;
}
17 changes: 17 additions & 0 deletions apps/readest-sync/src/book_actions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once
#include <QVariantMap>
#include <QVariantList>

namespace readest { struct LibraryEntry; }
enum class BookChoice { None, SyncConflict, OpenConflict, LocalCopy };
enum class BookBlock { None, Pending, Acknowledged };

struct BookActions {
QVariantList allowed,secondary,menu,choices;
QVariantMap primary;
bool choosing=false;
QVariantMap presentation() const;
};

BookActions bookActions(const readest::LibraryEntry& entry,bool signedIn,
BookChoice choice,BookBlock block);
56 changes: 8 additions & 48 deletions apps/readest-sync/src/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,55 +121,15 @@ QVariantMap AppController::book() const {
{"pocketBookProgress",percentageLabel(entry->local_percentage)},
{"readestProgress",percentageLabel(entry->remote_percentage)}};
}
QVariantList AppController::actions() const {
QVariantList result;
if(busy()) return result;
auto add=[&](const char* command,const QString& text) { result.append(QVariantMap{{"command",command},{"text",text}}); };
const auto* entry=library_.find(selected_); if(!entry) return result;
if(blocking_state_!=BlockingState::None && selected_==blocking_book_) {
if(!blocking_acknowledged_) return result;
add(entry->availability==Availability::OnDevice?"sync":"refresh",
entry->availability==Availability::OnDevice?"Sync now":"Check availability");
add("back","Back to library"); return result;
}
if(entry->book.needs_copy_choice || choice_==ChoiceState::LocalCopy) {
for(size_t i=0;i<entry->book.copies.size();++i) {
const auto& copy=entry->book.copies[i];
const auto command="copy:"+std::to_string(i);
add(command.c_str(),QString::fromStdString(copy.path)+" · "+percentageLabel(copy.percentage));
}
add("back","Back to library");return result;
}
if(choice_==ChoiceState::OpenConflict) {
add("openPocketBook","Open at PocketBook position"); add("openReadest","Open at Readest position");
add("back","Back to library"); return result;
}
if(choice_==ChoiceState::SyncConflict) {
add("usePocketBook","Use PocketBook position"); add("useReadest","Use Readest position");
add("back","Back to library"); return result;
}
if(entry->upload_pending && signed_in_) {
add("upload","Retry upload / reading position"); add("back","Back to library"); return result;
}
if(entry->book.local_only || entry->book.book.deleted || !signed_in_) {
add("offline","Open at PocketBook position");
if(signed_in_) add("upload",entry->book.book.deleted?"Re-upload to Readest":entry->upload_pending?"Retry upload":"Upload to Readest");
else add("signin","Sign in to upload");
if(entry->book.copies.size()>1) add("copies","Choose local copy");
add("back","Back to library");return result;
}
if(entry->book.copies.size()>1) add("copies","Choose local copy");
if(entry->availability==Availability::OnDevice) {
add("open",entry->sync.pending_remote.empty()?"Open":"Open at Readest position");
add("sync","Sync now"); add("offline","Read offline");
} else if(entry->availability==Availability::Downloadable) add("download","Download EPUB");
else add("refresh","Check availability");
if(entry->availability==Availability::OnDevice && entry->book.epubs==0)
add("upload","Upload EPUB to Readest");
if(entry->availability==Availability::OnDevice && entry->book.epubs>0)
add("uploadCover","Upload cover to Readest");
add("back","Back to library"); return result;
BookActions AppController::bookActions() const {
const auto* entry=library_.find(selected_);
if(busy() || !entry) return {};
const auto block=blocking_state_!=BlockingState::None && selected_==blocking_book_
?(blocking_acknowledged_?BookBlock::Acknowledged:BookBlock::Pending):BookBlock::None;
return ::bookActions(*entry,signed_in_,choice_,block);
}
QVariantList AppController::actions() const { return bookActions().allowed; }
QVariantMap AppController::actionPresentation() const { return bookActions().presentation(); }
void AppController::submit(Request request) {
if(runner_.busy()) {
if(covers_.active()) { pending_request_=std::make_unique<Request>(std::move(request)); covers_.show({}); emit changed(); }
Expand Down
6 changes: 5 additions & 1 deletion apps/readest-sync/src/controller.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include "application.h"
#include "book_actions.h"
#include "cover_loader.h"
#include "device_adapter.h"
#include "library_model.h"
Expand All @@ -20,6 +21,7 @@ class AppController : public QObject {
Q_PROPERTY(QString hint READ hint NOTIFY changed)
Q_PROPERTY(QVariantMap book READ book NOTIFY changed)
Q_PROPERTY(QVariantList actions READ actions NOTIFY changed)
Q_PROPERTY(QVariantMap actionPresentation READ actionPresentation NOTIFY changed)
Q_PROPERTY(bool decision READ decision NOTIFY changed)
Q_PROPERTY(bool blocking READ blocking NOTIFY changed)
Q_PROPERTY(QString blockingMessage READ blockingMessage NOTIFY changed)
Expand All @@ -39,6 +41,7 @@ class AppController : public QObject {
QString hint() const;
QVariantMap book() const;
QVariantList actions() const;
QVariantMap actionPresentation() const;
bool decision() const { return choice_==ChoiceState::SyncConflict || choice_==ChoiceState::OpenConflict; }
bool blocking() const;
QString blockingMessage() const;
Expand Down Expand Up @@ -69,7 +72,7 @@ class AppController : public QObject {
OperationRunner runner_; // Destroy/join the runner before the service.
VisibleCoverLoader covers_; // Invalidates callbacks before runner destruction.
readest::BookId selected_;
enum class ChoiceState { None, SyncConflict, OpenConflict, LocalCopy };
using ChoiceState=BookChoice;
ChoiceState choice_=ChoiceState::None;
enum class BlockingState { None, AppliedUnrecorded, NativeCommitUncertain };
BlockingState blocking_state_=BlockingState::None;
Expand All @@ -82,4 +85,5 @@ class AppController : public QObject {
void submit(readest::Request request);
void complete(const readest::Request& request,readest::OperationResult result);
void prepareCovers();
BookActions bookActions() const;
};
Loading