First attempt

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
jpirnay
2026-04-27 15:30:43 +02:00
co-authored by Copilot
parent 291898bb7f
commit aed297b2b1
11 changed files with 213 additions and 17 deletions
@@ -294,6 +294,7 @@ void EpubReaderActivity::loop() {
// Long press BACK (1s+) goes to home screen
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
ReaderUtils::enforceExitFullRefresh(renderer);
if (tryAutoPushOnClose()) return;
onGoHome();
return;
}
@@ -306,6 +307,7 @@ void EpubReaderActivity::loop() {
return;
}
ReaderUtils::enforceExitFullRefresh(renderer);
if (tryAutoPushOnClose()) return;
finish();
return;
}
@@ -318,6 +320,7 @@ void EpubReaderActivity::loop() {
// At end of the book, forward button returns to caller and back button returns to last page
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
if (nextTriggered) {
if (tryAutoPushOnClose()) return;
finish();
} else {
currentSpineIndex = epub->getSpineItemsCount() - 1;
@@ -568,6 +571,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
break;
}
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
if (tryAutoPushOnClose()) return;
onGoHome();
return;
}
@@ -831,6 +835,8 @@ void EpubReaderActivity::launchKOReaderSync(const SyncLaunchMode mode) {
syncIntent = KOReaderSyncIntentState::PULL_REMOTE;
} else if (mode == SyncLaunchMode::PUSH_LOCAL) {
syncIntent = KOReaderSyncIntentState::PUSH_LOCAL;
} else if (mode == SyncLaunchMode::AUTO_PUSH) {
syncIntent = KOReaderSyncIntentState::AUTO_PUSH;
}
auto& sync = APP_STATE.koReaderSyncSession;
@@ -865,6 +871,10 @@ void EpubReaderActivity::launchKOReaderSync(const SyncLaunchMode mode) {
sync.resultPage = 0;
sync.resultParagraphIndex = 0;
sync.resultHasParagraphIndex = false;
// Only auto-push-on-close should bypass the reader on resume; explicit syncs from the
// reader menu always come back to the reader. Reset here so a stale flag from a prior
// run cannot steal the user back to home.
sync.exitToHomeAfterSync = (mode == SyncLaunchMode::AUTO_PUSH);
APP_STATE.saveToFile();
LOG_DBG("ERS", "Standalone sync handoff: spine=%d page=%d/%d", currentSpineIndex, currentPage, totalPages);
@@ -872,6 +882,27 @@ void EpubReaderActivity::launchKOReaderSync(const SyncLaunchMode mode) {
activityManager.goToKOReaderSync();
}
bool EpubReaderActivity::tryAutoPushOnClose() {
// Three-page minimum filters out brief inspections — opening to check the cover or
// skim the TOC shouldn't burn a network round-trip. Counter is per-activity-instance.
constexpr int MIN_SESSION_PAGES = 3;
if (!SETTINGS.koSyncOnBookClose) {
return false;
}
if (!KOREADER_STORE.hasCredentials()) {
return false;
}
if (sessionPagesAdvanced < MIN_SESSION_PAGES) {
return false;
}
if (!epub) {
return false;
}
// exitToHomeAfterSync flag is set inside launchKOReaderSync for AUTO_PUSH mode.
launchKOReaderSync(SyncLaunchMode::AUTO_PUSH);
return true;
}
void EpubReaderActivity::applyPendingSyncSession() {
auto& sync = APP_STATE.koReaderSyncSession;
if (!sync.active || !epub || sync.epubPath != epub->getPath()) {
@@ -891,6 +922,17 @@ void EpubReaderActivity::applyPendingSyncSession() {
return;
}
// AUTO_PULL handed off zeroed local state (the reader was not yet running when sync started),
// so on cancel/fail we must NOT restore those zeros to progress.bin — they would clobber the
// user's real local progress. Just clear the session and let the normal startup load progress.bin.
if (sync.intent == KOReaderSyncIntentState::AUTO_PULL && sync.outcome != KOReaderSyncOutcomeState::APPLIED_REMOTE) {
LOG_DBG("ERS", "AUTO_PULL non-success outcome=%d: leaving progress.bin untouched", static_cast<int>(sync.outcome));
sync.clear();
APP_STATE.saveToFile();
logReaderMemSnapshot("after_apply_pending_sync_session");
return;
}
int restoreSpineIndex = sync.spineIndex;
int restorePage = sync.page;
pendingParagraphLookup = sync.hasParagraphIndex;
@@ -1142,6 +1184,9 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn) {
if (!stepPageState(isForwardTurn)) {
return;
}
// Track real progress within this session so auto-push-on-close can ignore brief
// book inspections. Counts both directions — the user is engaging with the book either way.
sessionPagesAdvanced++;
requestUpdate();
}
@@ -1888,6 +1933,7 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
}
case BA::BTN_EXIT_READER:
ReaderUtils::enforceExitFullRefresh(renderer);
if (tryAutoPushOnClose()) break;
finish();
break;
case BA::BTN_READER_MENU:
+12 -1
View File
@@ -11,15 +11,17 @@
#include "activities/Activity.h"
class EpubReaderActivity final : public Activity {
// Reader can launch sync in three UX modes:
// Reader can launch sync in several UX modes:
// - COMPARE: legacy chooser (apply/upload) for power users.
// - PULL_REMOTE / PUSH_LOCAL: direct one-step actions from menu entries.
// - AUTO_PUSH: silent push on reader exit; skips when remote is already ahead.
// Keeping this split in the caller avoids branching on menu semantics deep
// inside generic reader state handling.
enum class SyncLaunchMode {
COMPARE,
PULL_REMOTE,
PUSH_LOCAL,
AUTO_PUSH,
};
std::shared_ptr<Epub> epub;
@@ -124,6 +126,10 @@ class EpubReaderActivity final : public Activity {
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
ReaderUtils::InputDrainGuard inputDrainGuard;
bool automaticPageTurnActive = false;
// Pages turned in the current reader session. Used to gate auto-push-on-close: a brief
// inspection of a book should not trigger a network round-trip. Reset on every reader
// entry; not persisted, since "session" means the lifetime of this activity instance.
int sessionPagesAdvanced = 0;
// -1 means use global SETTINGS value.
int8_t bookEmbeddedStyleOverride = -1;
int8_t bookImageRenderingOverride = -1;
@@ -152,6 +158,11 @@ class EpubReaderActivity final : public Activity {
void jumpToPercent(int percent);
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
void launchKOReaderSync(SyncLaunchMode mode = SyncLaunchMode::COMPARE);
// Reader-close auto-push gate. Returns true if AUTO_PUSH was launched (the caller
// must not perform its own exit — the sync activity will route to home on completion).
// Returns false when any of the gates fails (setting off, no credentials, < 3 pages),
// letting the caller take its normal exit path.
bool tryAutoPushOnClose();
// Consume a persisted standalone KOReader sync session for this EPUB. Remote
// apply writes the mapped reopen position into progress.bin before the normal
// reader startup path reads it. Upload-complete leaves the existing local
+60 -4
View File
@@ -115,8 +115,8 @@ void KOReaderSyncActivity::performSync() {
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
// Local mapping is only needed for compare/upload paths.
// Pull-only mode can skip this expensive step and go straight to remote fetch.
if (syncIntent != KOReaderSyncIntentState::PULL_REMOTE) {
// Pull-only modes can skip this expensive step and go straight to remote fetch.
if (syncIntent != KOReaderSyncIntentState::PULL_REMOTE && syncIntent != KOReaderSyncIntentState::AUTO_PULL) {
// Precompute local mapping before first network request so the expensive
// inflate/index work happens before TLS. This avoids a second local mapping
// pass later and keeps the upload path lightweight.
@@ -141,7 +141,7 @@ void KOReaderSyncActivity::performSync() {
// Push intent skips comparison UI but still warms an HTTP/TLS session first
// so PUT can reuse the connection instead of forcing a fresh handshake.
if (syncIntent == KOReaderSyncIntentState::PUSH_LOCAL) {
if (syncIntent == KOReaderSyncIntentState::PUSH_LOCAL || syncIntent == KOReaderSyncIntentState::AUTO_PUSH) {
// Direct push previously started with no reusable HTTP/TLS session, forcing
// a fresh handshake in updateProgress. Compare flow often succeeds because
// upload reuses the GET session. Warm the session here so push can take the
@@ -164,6 +164,22 @@ void KOReaderSyncActivity::performSync() {
requestUpdate(true);
return;
}
// Auto-push must not overwrite progress that is already further along on the server.
// Compare percentages from the warmup GET; users opted into automatic sync, so a
// remote-ahead state is treated as "nothing to do" and falls straight back to home.
if (syncIntent == KOReaderSyncIntentState::AUTO_PUSH && warmupResult == KOReaderSyncClient::OK &&
warmupProgress.percentage > localProgress.percentage) {
LOG_DBG("KOSync", "AUTO_PUSH skipped: remote %.4f >= local %.4f", warmupProgress.percentage,
localProgress.percentage);
KOReaderSyncClient::endPersistentSession();
HalClock::wifiOff(true);
// Reuse UPLOAD_COMPLETE outcome so the resume path is identical to a successful push;
// there is nothing to apply to the reader and progress.bin already reflects local state.
APP_STATE.koReaderSyncSession.outcome = KOReaderSyncOutcomeState::UPLOAD_COMPLETE;
APP_STATE.saveToFile();
resumeReader(KOReaderSyncOutcomeState::UPLOAD_COMPLETE);
return;
}
performUpload();
return;
}
@@ -195,6 +211,15 @@ void KOReaderSyncActivity::performSync() {
return;
}
if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
// Auto-pull at book open: nothing to apply, just open the book with local progress.
// No user-visible failure since the user opted into "open with sync, if available".
KOReaderSyncClient::endPersistentSession();
HalClock::wifiOff(true);
resumeReader(KOReaderSyncOutcomeState::CANCELLED);
return;
}
// Keep session open so an immediate upload can reuse the same connection.
// No remote progress - offer to upload
{
@@ -234,10 +259,16 @@ void KOReaderSyncActivity::performSync() {
remotePosition.hasParagraphIndex = false;
remoteChapterLabel.clear();
if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE) {
if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE || syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
// Pull intent applies immediately and exits. We bypass chooser UI to keep
// reader menu actions deterministic ("pull" always means apply remote).
if (!ensureRemotePositionMapped()) {
if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
// Auto-pull was best-effort. Fail silently and just open the book.
HalClock::wifiOff(true);
resumeReader(KOReaderSyncOutcomeState::CANCELLED);
return;
}
{
RenderLock lock(*this);
state = SYNC_FAILED;
@@ -256,6 +287,14 @@ void KOReaderSyncActivity::performSync() {
sync.resultParagraphIndex = remotePosition.paragraphIndex;
sync.resultHasParagraphIndex = remotePosition.hasParagraphIndex;
APP_STATE.saveToFile();
if (syncIntent == KOReaderSyncIntentState::AUTO_PULL) {
// Auto-pull skips the success-screen dwell — the reader will render the new
// position immediately, which is the only visible feedback the user needs.
HalClock::wifiOff(true);
resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE);
return;
}
{
RenderLock lock(*this);
state = APPLY_COMPLETE;
@@ -390,6 +429,12 @@ void KOReaderSyncActivity::performUpload() {
HalClock::wifiOff(true);
APP_STATE.koReaderSyncSession.outcome = KOReaderSyncOutcomeState::UPLOAD_COMPLETE;
APP_STATE.saveToFile();
if (syncIntent == KOReaderSyncIntentState::AUTO_PUSH) {
// Auto-push doesn't need user acknowledgement on success; resume immediately
// back to the calling activity (RecentBooks / FileBrowser via reader).
resumeReader(KOReaderSyncOutcomeState::UPLOAD_COMPLETE);
return;
}
{
RenderLock lock(*this);
state = UPLOAD_COMPLETE;
@@ -464,8 +509,19 @@ void KOReaderSyncActivity::resumeReader(const KOReaderSyncOutcomeState outcome,
sync.resultParagraphIndex = 0;
sync.resultHasParagraphIndex = false;
}
// Honor exit-to-home flag set by reader-close auto-sync — bouncing back into the reader
// the user just left would be jarring. The session state is consumed and cleared by the
// home destination's normal flow (no reader to apply it to in this case).
const bool exitToHome = sync.exitToHomeAfterSync;
if (exitToHome) {
sync.clear();
}
APP_STATE.saveToFile();
logSyncMemSnapshot("before_resume_reader");
if (exitToHome) {
activityManager.goHome();
return;
}
activityManager.goToReader(epubPath);
}
+33
View File
@@ -11,6 +11,7 @@
#include "CrossPointState.h"
#include "Epub.h"
#include "EpubReaderActivity.h"
#include "KOReaderCredentialStore.h"
#include "MdReaderActivity.h"
#include "Txt.h"
#include "TxtReaderActivity.h"
@@ -110,6 +111,38 @@ void ReaderActivity::goToLibrary(const std::string& fromBookPath) {
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
const auto epubPath = epub->getPath();
currentBookPath = epubPath;
// Long-press Confirm on RecentBooks/FileBrowser sets autoPullOnOpen so the user can ask
// for KOReader sync at open time. Route into the sync activity instead of creating the
// reader; sync's resumeReader() will create the reader once the remote position is applied.
// Pull-only mode does not need accurate local reader state, so we hand off zeros for spine/page.
auto& sync = APP_STATE.koReaderSyncSession;
if (sync.autoPullOnOpen && KOREADER_STORE.hasCredentials()) {
LOG_DBG("READER", "AUTO_PULL on open: %s", epubPath.c_str());
sync.autoPullOnOpen = false; // consume the flag
sync.active = true;
sync.epubPath = epubPath;
sync.spineIndex = 0;
sync.page = 0;
sync.totalPagesInSpine = 0;
sync.paragraphIndex = 0;
sync.hasParagraphIndex = false;
sync.xhtmlSeekHint = 0;
sync.intent = KOReaderSyncIntentState::AUTO_PULL;
sync.outcome = KOReaderSyncOutcomeState::PENDING;
sync.resultSpineIndex = 0;
sync.resultPage = 0;
sync.resultParagraphIndex = 0;
sync.resultHasParagraphIndex = false;
sync.exitToHomeAfterSync = false;
APP_STATE.saveToFile();
// Drop the loaded Epub before TLS — sync activity will reload it for remote-position
// mapping. Holding it here would needlessly inflate the heap during WiFi/TLS work.
epub.reset();
activityManager.goToKOReaderSync();
return;
}
logReaderLaunchMemSnapshot("before_replace_epub_reader");
activityManager.replaceActivity(std::make_unique<EpubReaderActivity>(renderer, mappedInput, std::move(epub)));
}