feat: port upstream KOReader sync PRs (#1185, #1217, #1090)

Port three unmerged upstream PRs with adaptations for the fork's
callback-based ActivityWithSubactivity architecture:

- PR #1185: Cache KOReader document hash using mtime fingerprint +
  file size validation to avoid repeated MD5 computation on sync.
- PR #1217: Proper KOReader XPath synchronisation via new
  ChapterXPathIndexer (Expat-based on-demand XHTML parsing) with
  XPath-first mapping and percentage fallback in ProgressMapper.
- PR #1090: Push Progress & Sleep menu option with PUSH_ONLY sync
  mode. Adapted to fork's callback pattern with deferFinish() for
  thread-safe completion. Modified to sleep silently on any failure
  (hash, upload, no credentials) rather than returning to reader.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-02 05:19:14 -05:00
parent 42011d5977
commit 3628d8eb37
14 changed files with 1031 additions and 44 deletions

View File

@@ -24,6 +24,8 @@
#include "util/BookmarkStore.h"
#include "util/Dictionary.h"
extern void enterDeepSleep();
namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
constexpr unsigned long skipChapterMs = 700;
@@ -210,6 +212,18 @@ void EpubReaderActivity::loop() {
}
return; // Don't access 'this' after callback
}
if (pendingSleep) {
pendingSleep = false;
exitActivity();
enterDeepSleep();
return;
}
return;
}
if (pendingSleep) {
pendingSleep = false;
enterDeepSleep();
return;
}
@@ -814,6 +828,28 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
}
break;
}
case EpubReaderMenuActivity::MenuAction::PUSH_AND_SLEEP: {
if (KOREADER_STORE.hasCredentials()) {
const int cp = section ? section->currentPage : 0;
const int tp = section ? section->pageCount : 0;
exitActivity();
enterNewActivity(new KOReaderSyncActivity(
renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, cp, tp,
[this]() {
// Push failed -- sleep anyway (silent failure)
pendingSleep = true;
},
[this](int, int) {
// Push succeeded -- sleep
pendingSleep = true;
},
KOReaderSyncActivity::SyncMode::PUSH_ONLY));
} else {
// No credentials -- just sleep
pendingSleep = true;
}
break;
}
// Handled locally in the menu activity (cycle on Confirm, never dispatched here)
case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN:
case EpubReaderMenuActivity::MenuAction::TOGGLE_FONT_SIZE:

View File

@@ -22,6 +22,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
float pendingSpineProgress = 0.0f;
bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free
bool pendingGoHome = false; // Defer go home to avoid race condition with display task
bool pendingSleep = false; // Defer deep sleep until after push-and-sleep completes
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
bool ignoreNextConfirmRelease = false; // Suppress short-press after long-press Confirm
volatile bool loadingSection = false; // True during the entire !section block (read from main loop)

View File

@@ -27,6 +27,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
GO_TO_PERCENT,
GO_HOME,
SYNC,
PUSH_AND_SLEEP,
DELETE_CACHE,
MANAGE_BOOK,
ARCHIVE_BOOK,
@@ -140,6 +141,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK});
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
items.push_back({MenuAction::PUSH_AND_SLEEP, StrId::STR_PUSH_AND_SLEEP});
items.push_back({MenuAction::MANAGE_BOOK, StrId::STR_MANAGE_BOOK});
return items;
}

View File

@@ -44,6 +44,12 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
performSync();
}
void KOReaderSyncActivity::deferFinish(bool success) {
RenderLock lock(*this);
pendingFinishSuccess = success;
pendingFinish = true;
}
void KOReaderSyncActivity::performSync() {
// Calculate document hash based on user's preferred method
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
@@ -52,6 +58,10 @@ void KOReaderSyncActivity::performSync() {
documentHash = KOReaderDocumentId::calculate(epubPath);
}
if (documentHash.empty()) {
if (syncMode == SyncMode::PUSH_ONLY) {
deferFinish(false);
return;
}
{
RenderLock lock(*this);
state = SYNC_FAILED;
@@ -63,6 +73,11 @@ void KOReaderSyncActivity::performSync() {
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
if (syncMode == SyncMode::PUSH_ONLY) {
performUpload();
return;
}
{
RenderLock lock(*this);
statusMessage = tr(STR_FETCH_PROGRESS);
@@ -137,6 +152,10 @@ void KOReaderSyncActivity::performUpload() {
const auto result = KOReaderSyncClient::updateProgress(progress);
if (result != KOReaderSyncClient::OK) {
if (syncMode == SyncMode::PUSH_ONLY) {
deferFinish(false);
return;
}
{
RenderLock lock(*this);
state = SYNC_FAILED;
@@ -146,6 +165,11 @@ void KOReaderSyncActivity::performUpload() {
return;
}
if (syncMode == SyncMode::PUSH_ONLY) {
deferFinish(true);
return;
}
{
RenderLock lock(*this);
state = UPLOAD_COMPLETE;
@@ -334,6 +358,27 @@ void KOReaderSyncActivity::loop() {
return;
}
if (syncMode == SyncMode::PUSH_ONLY) {
bool ready = false;
bool success = false;
{
RenderLock lock(*this);
if (pendingFinish) {
pendingFinish = false;
ready = true;
success = pendingFinishSuccess;
}
}
if (ready) {
if (success) {
onSyncComplete(currentSpineIndex, currentPage);
} else {
onCancel();
}
return;
}
}
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onCancel();

View File

@@ -15,18 +15,21 @@
* 1. Connect to WiFi (if not connected)
* 2. Calculate document hash
* 3. Fetch remote progress
* 4. Show comparison and options (Apply/Upload)
* 4. Show comparison and options (Apply/Upload) or skip when SyncMode::PUSH_ONLY
* 5. Apply or upload progress
*/
class KOReaderSyncActivity final : public ActivityWithSubactivity {
public:
enum class SyncMode { INTERACTIVE, PUSH_ONLY };
using OnCancelCallback = std::function<void()>;
using OnSyncCompleteCallback = std::function<void(int newSpineIndex, int newPageNumber)>;
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
int currentPage, int totalPagesInSpine, OnCancelCallback onCancel,
OnSyncCompleteCallback onSyncComplete)
OnSyncCompleteCallback onSyncComplete,
SyncMode syncMode = SyncMode::INTERACTIVE)
: ActivityWithSubactivity("KOReaderSync", renderer, mappedInput),
epub(epub),
epubPath(epubPath),
@@ -37,7 +40,8 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
remotePosition{},
localProgress{},
onCancel(std::move(onCancel)),
onSyncComplete(std::move(onSyncComplete)) {}
onSyncComplete(std::move(onSyncComplete)),
syncMode(syncMode) {}
void onEnter() override;
void onExit() override;
@@ -82,6 +86,11 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
OnCancelCallback onCancel;
OnSyncCompleteCallback onSyncComplete;
SyncMode syncMode;
bool pendingFinish = false;
bool pendingFinishSuccess = false;
void deferFinish(bool success);
void onWifiSelectionComplete(bool success);
void performSync();
void performUpload();