@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user