feat: add smart KOReader progress sync (#2192)

## Summary

Make KOReader progress sync a one-click flow for common cases while
keeping the existing manual mode available.

### Changes

- Add a **Sync Behavior** setting:
  - **Smart sync** for new configurations
  - **Ask every time** for manual control
- Preserve **Ask every time** when migrating an existing credential file
that predates this setting.
- In Smart mode:
  - upload local progress when no remote record exists;
  - show a short confirmation when already synced;
  - upload when local progress is further ahead;
  - apply remote progress when remote progress is further ahead.
- Probe both KOReader document-matching hashes before making the Smart
decision, while keeping uploads on the user's configured matching
method.
- Auto-return after successful Smart terminal states, with Back/Confirm
still available.
- Persist the setting alongside the current credential, matching-method,
and send-metadata fields.
- Document both behaviors in the user guide.

## Additional context

This pairs well with #2189 (smart Wi-Fi auto-connect), but does not
depend on it. It does not add background Wi-Fi or passive network
detection; sync is still triggered by the user from the reader menu.

Smart sync uses the furthest progress because CrossPoint does not
currently persist local progress timestamps. Users who prefer explicit
conflict resolution can select **Ask every time**.

## Verification

- `git diff --check`
- `platformio check --fail-on-defect low --fail-on-defect medium
--fail-on-defect high` — no defects
- `platformio run -e default` — firmware build successful
- The original implementation was manually smoke-tested on an X4 device.

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_

AI tools were used to inspect the codebase, draft and review the
implementation, resolve the rebase against current `develop`, and
prepare the PR text. The resulting firmware was built locally.

---------

Co-authored-by: Alexander Hoffer <git@alexanderhoffer.com>
Co-authored-by: Alexander Hoffer <contact@alexanderhoffer.com>
This commit is contained in:
Alexander Hoff ❍
2026-07-15 14:59:15 -04:00
committed by GitHub
co-authored by Alexander Hoffer Alexander Hoffer
parent 35a45f9c1e
commit a4ac3b2788
8 changed files with 177 additions and 26 deletions
+7 -9
View File
@@ -297,7 +297,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates. - **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress. - **KOReader Sync**: Options for setting up KOReader for syncing book progress. **Smart sync** is the default for new configurations and auto-resolves simple push/pull decisions. Existing credential files retain **Ask every time** when migrated; you can switch Sync Behavior at any time if you prefer manual confirmation.
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below. - **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
@@ -389,11 +389,10 @@ When this returns `HTTP 402` with `{"code":2002,"message":"Username is already r
- Run **Authenticate**. - Run **Authenticate**.
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. 3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
- Choose **Apply Remote** to jump to remote progress. - With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
- Choose **Upload Local** to push current progress.
##### Option B: Self-Hosted Server (Docker Compose) ##### Option B: Self-Hosted Server (Docker Compose)
@@ -468,11 +467,10 @@ If this returns `HTTP 402` with `{"code":2002,"message":"Username is already reg
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing). If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. 5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
- Choose **Apply Remote** to jump to remote progress. - With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
- Choose **Upload Local** to push current progress.
### 3.7 Sleep Screen ### 3.7 Sleep Screen
+4
View File
@@ -108,6 +108,9 @@ STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "Sync Server URL" STR_SYNC_SERVER_URL: "Sync Server URL"
STR_DOCUMENT_MATCHING: "Document Matching" STR_DOCUMENT_MATCHING: "Document Matching"
STR_SEND_METADATA: "Send Document Metadata" STR_SEND_METADATA: "Send Document Metadata"
STR_SYNC_BEHAVIOR: "Sync Behavior"
STR_ASK_EVERY_TIME: "Ask every time"
STR_SMART_SYNC: "Smart sync"
STR_AUTHENTICATE: "Authenticate" STR_AUTHENTICATE: "Authenticate"
STR_KOREADER_USERNAME: "KOReader Username" STR_KOREADER_USERNAME: "KOReader Username"
STR_KOREADER_PASSWORD: "KOReader Password" STR_KOREADER_PASSWORD: "KOReader Password"
@@ -324,6 +327,7 @@ STR_UPLOAD_LOCAL: "Upload local progress"
STR_NO_REMOTE_MSG: "No remote progress found" STR_NO_REMOTE_MSG: "No remote progress found"
STR_UPLOAD_PROMPT: "Upload current position?" STR_UPLOAD_PROMPT: "Upload current position?"
STR_UPLOAD_SUCCESS: "Progress uploaded!" STR_UPLOAD_SUCCESS: "Progress uploaded!"
STR_ALREADY_SYNCED: "Already synced"
STR_SYNC_FAILED_MSG: "Sync failed" STR_SYNC_FAILED_MSG: "Sync failed"
STR_SAVE_PROGRESS_FAILED: "Could not save progress" STR_SAVE_PROGRESS_FAILED: "Could not save progress"
STR_SECTION_PREFIX: "Section " STR_SECTION_PREFIX: "Section "
@@ -15,6 +15,7 @@ void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
doc["serverUrl"] = getServerUrl(); doc["serverUrl"] = getServerUrl();
doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod()); doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod());
doc["sendMetadata"] = getSendMetadata(); doc["sendMetadata"] = getSendMetadata();
doc["syncBehavior"] = static_cast<uint8_t>(getSyncBehavior());
} }
bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) { bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
@@ -35,6 +36,18 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
} }
setSendMetadata(doc["sendMetadata"] | false); setSendMetadata(doc["sendMetadata"] | false);
const JsonVariantConst behaviorValue = doc["syncBehavior"];
const bool missingBehavior = behaviorValue.isNull();
uint8_t behavior = behaviorValue | static_cast<uint8_t>(KOReaderSyncBehavior::ASK_EVERY_TIME);
if (behavior <= static_cast<uint8_t>(KOReaderSyncBehavior::SMART)) {
setSyncBehavior(static_cast<KOReaderSyncBehavior>(behavior));
needsResave = needsResave || missingBehavior;
} else {
LOG_DBG("KRS", "Invalid syncBehavior %u in JSON, resetting to ASK_EVERY_TIME", behavior);
setSyncBehavior(KOReaderSyncBehavior::ASK_EVERY_TIME);
needsResave = true;
}
if (needsResave) { if (needsResave) {
LOG_DBG("KRS", "Resaved KOReader credentials to update format"); LOG_DBG("KRS", "Resaved KOReader credentials to update format");
saveToFile(); saveToFile();
@@ -105,3 +118,11 @@ void KOReaderCredentialStore::setSendMetadata(bool enabled) {
sendMetadata = enabled; sendMetadata = enabled;
LOG_DBG("KRS", "Set send metadata: %s", enabled ? "true" : "false"); LOG_DBG("KRS", "Set send metadata: %s", enabled ? "true" : "false");
} }
void KOReaderCredentialStore::setSyncBehavior(KOReaderSyncBehavior behavior) {
if (static_cast<uint8_t>(behavior) > static_cast<uint8_t>(KOReaderSyncBehavior::SMART)) {
behavior = KOReaderSyncBehavior::ASK_EVERY_TIME;
}
syncBehavior = behavior;
LOG_DBG("KRS", "Set sync behavior: %s", behavior == KOReaderSyncBehavior::SMART ? "Smart" : "Ask");
}
@@ -11,6 +11,12 @@ enum class DocumentMatchMethod : uint8_t {
BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical) BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical)
}; };
// How manual "Sync Progress" resolves differences after fetching remote progress.
enum class KOReaderSyncBehavior : uint8_t {
ASK_EVERY_TIME = 0, // Preserve legacy behavior: always show Apply/Upload choices.
SMART = 1, // Auto-resolve simple cases using furthest progress.
};
/** /**
* Singleton class for storing KOReader sync credentials on the SD card. * Singleton class for storing KOReader sync credentials on the SD card.
* Passwords are XOR-obfuscated with the device's unique hardware MAC address * Passwords are XOR-obfuscated with the device's unique hardware MAC address
@@ -25,6 +31,7 @@ class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore>
std::string serverUrl; // Custom sync server URL (empty = default) std::string serverUrl; // Custom sync server URL (empty = default)
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
bool sendMetadata = false; // Send document metadata with progress sync bool sendMetadata = false; // Send document metadata with progress sync
KOReaderSyncBehavior syncBehavior = KOReaderSyncBehavior::SMART;
// Private constructor for singleton // Private constructor for singleton
KOReaderCredentialStore() = default; KOReaderCredentialStore() = default;
@@ -65,6 +72,10 @@ class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore>
// Send metadata setting // Send metadata setting
void setSendMetadata(bool enabled); void setSendMetadata(bool enabled);
bool getSendMetadata() const { return sendMetadata; } bool getSendMetadata() const { return sendMetadata; }
// Sync behavior
void setSyncBehavior(KOReaderSyncBehavior behavior);
KOReaderSyncBehavior getSyncBehavior() const { return syncBehavior; }
}; };
// Helper macro to access credential store // Helper macro to access credential store
+8
View File
@@ -242,6 +242,14 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koSendMetadata", StrId::STR_KOREADER_SYNC), "koSendMetadata", StrId::STR_KOREADER_SYNC),
SettingInfo::DynamicEnum(
StrId::STR_SYNC_BEHAVIOR, {StrId::STR_ASK_EVERY_TIME, StrId::STR_SMART_SYNC},
[] { return static_cast<uint8_t>(KOREADER_STORE.getSyncBehavior()); },
[](uint8_t v) {
KOREADER_STORE.setSyncBehavior(static_cast<KOReaderSyncBehavior>(v));
KOREADER_STORE.saveToFile();
},
"koSyncBehavior", StrId::STR_KOREADER_SYNC),
// --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) ---
SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount,
"statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR),
+103 -14
View File
@@ -10,6 +10,7 @@
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
#include <cmath>
#include "Epub/Section.h" #include "Epub/Section.h"
#include "EpubReaderUtils.h" #include "EpubReaderUtils.h"
@@ -24,6 +25,19 @@
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
std::string calculateDocumentHashForMethod(const std::string& path, const DocumentMatchMethod method) {
return method == DocumentMatchMethod::FILENAME ? KOReaderDocumentId::calculateFromFilename(path)
: KOReaderDocumentId::calculate(path);
}
DocumentMatchMethod alternateMatchMethod(const DocumentMatchMethod method) {
return method == DocumentMatchMethod::FILENAME ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME;
}
const char* matchMethodName(const DocumentMatchMethod method) {
return method == DocumentMatchMethod::FILENAME ? "filename" : "binary";
}
void syncTimeWithNTP() { void syncTimeWithNTP() {
// Stop SNTP if already running (can't reconfigure while running) // Stop SNTP if already running (can't reconfigure while running)
if (esp_sntp_enabled()) { if (esp_sntp_enabled()) {
@@ -84,6 +98,21 @@ void KOReaderSyncActivity::saveProgressAndReturn(int spineIndex, int page) {
void KOReaderSyncActivity::returnToReader() { activityManager.goToReader(epubPath); } void KOReaderSyncActivity::returnToReader() { activityManager.goToReader(epubPath); }
bool KOReaderSyncActivity::smartSyncEnabled() const {
return KOREADER_STORE.getSyncBehavior() == KOReaderSyncBehavior::SMART;
}
void KOReaderSyncActivity::markAutoReturn() { autoReturnAt = millis() + AUTO_RETURN_DELAY_MS; }
void KOReaderSyncActivity::completeAlreadySynced() {
{
RenderLock lock(*this);
state = SYNC_COMPLETE;
}
markAutoReturn();
requestUpdate(true);
}
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
if (!success) { if (!success) {
LOG_DBG("KOSync", "WiFi connection failed, exiting"); LOG_DBG("KOSync", "WiFi connection failed, exiting");
@@ -113,12 +142,8 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
} }
void KOReaderSyncActivity::performSync() { void KOReaderSyncActivity::performSync() {
// Calculate document hash based on user's preferred method const DocumentMatchMethod primaryMethod = KOREADER_STORE.getMatchMethod();
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { documentHash = calculateDocumentHashForMethod(epubPath, primaryMethod);
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
} else {
documentHash = KOReaderDocumentId::calculate(epubPath);
}
if (documentHash.empty()) { if (documentHash.empty()) {
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -128,8 +153,9 @@ void KOReaderSyncActivity::performSync() {
requestUpdate(true); requestUpdate(true);
return; return;
} }
const std::string primaryHash = documentHash;
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str()); LOG_DBG("KOSync", "Document hash (%s): %s", matchMethodName(primaryMethod), documentHash.c_str());
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -137,10 +163,42 @@ void KOReaderSyncActivity::performSync() {
} }
requestUpdateAndWait(); requestUpdateAndWait();
// Fetch remote progress // Fetch remote progress. In smart mode, also probe the alternate document-id
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); // method and use the furthest remote state we can find. This avoids a stale
// local upload when another KOReader device synced the same book with a
// different document matching method.
auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
LOG_DBG("KOSync", "Primary remote (%s): result=%d http=%d doc=%s local=%.6f remote=%.6f xpath=%s",
matchMethodName(primaryMethod), result, KOReaderSyncClient::lastHttpCode, documentHash.c_str(),
localProgress.percentage, remoteProgress.percentage, remoteProgress.progress.c_str());
if (smartSyncEnabled()) {
const DocumentMatchMethod altMethod = alternateMatchMethod(primaryMethod);
const std::string altHash = calculateDocumentHashForMethod(epubPath, altMethod);
if (!altHash.empty() && altHash != documentHash) {
KOReaderProgress altProgress;
const auto altResult = KOReaderSyncClient::getProgress(altHash, altProgress);
LOG_DBG("KOSync", "Alternate remote (%s): result=%d http=%d doc=%s local=%.6f remote=%.6f xpath=%s",
matchMethodName(altMethod), altResult, KOReaderSyncClient::lastHttpCode, altHash.c_str(),
localProgress.percentage, altProgress.percentage, altProgress.progress.c_str());
if (altResult == KOReaderSyncClient::OK &&
(result == KOReaderSyncClient::NOT_FOUND || altProgress.percentage > remoteProgress.percentage)) {
documentHash = altHash;
remoteProgress = std::move(altProgress);
result = KOReaderSyncClient::OK;
}
}
}
if (result == KOReaderSyncClient::NOT_FOUND) { if (result == KOReaderSyncClient::NOT_FOUND) {
if (smartSyncEnabled()) {
LOG_DBG("KOSync", "Smart sync: no remote progress found for known document hashes; uploading local %.6f",
localProgress.percentage);
performUpload();
return;
}
// No remote progress - offer to upload // No remote progress - offer to upload
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -177,6 +235,29 @@ void KOReaderSyncActivity::performSync() {
SavedProgressPosition koPos = {remoteProgress.progress, remoteProgress.percentage}; SavedProgressPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, renderer, currentSpineIndex, totalPagesInSpine); remotePosition = ProgressMapper::toCrossPoint(epub, koPos, renderer, currentSpineIndex, totalPagesInSpine);
if (smartSyncEnabled()) {
static constexpr float SAME_PROGRESS_EPSILON = 0.001f; // 0.1 percentage points
const float delta = localProgress.percentage - remoteProgress.percentage;
LOG_DBG("KOSync", "Smart decision: doc=%s local=%.6f remote=%.6f delta=%.6f remoteXpath=%s mapped=%d/%d",
documentHash.c_str(), localProgress.percentage, remoteProgress.percentage, delta,
remoteProgress.progress.c_str(), remotePosition.spineIndex, remotePosition.pageNumber);
if (std::fabs(delta) <= SAME_PROGRESS_EPSILON) {
completeAlreadySynced();
return;
}
if (delta > 0) {
// Alternate hashes are only probes for newer remote state. Keep uploads
// on the user's configured matching method so its primary record heals.
documentHash = primaryHash;
performUpload();
return;
}
saveProgressAndReturn(remotePosition.spineIndex, remotePosition.pageNumber);
return;
}
// localProgress was pre-computed in EpubReaderActivity before the Epub was released. // localProgress was pre-computed in EpubReaderActivity before the Epub was released.
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -248,6 +329,7 @@ void KOReaderSyncActivity::performUpload() {
RenderLock lock(*this); RenderLock lock(*this);
state = UPLOAD_COMPLETE; state = UPLOAD_COMPLETE;
} }
markAutoReturn();
requestUpdate(true); requestUpdate(true);
} }
@@ -391,10 +473,12 @@ void KOReaderSyncActivity::render(RenderLock&&) {
return; return;
} }
if (state == UPLOAD_COMPLETE) { if (state == UPLOAD_COMPLETE || state == SYNC_COMPLETE) {
UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD); UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top,
state == UPLOAD_COMPLETE ? tr(STR_UPLOAD_SUCCESS) : tr(STR_ALREADY_SYNCED), true,
EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DONE), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -412,8 +496,13 @@ void KOReaderSyncActivity::render(RenderLock&&) {
} }
void KOReaderSyncActivity::loop() { void KOReaderSyncActivity::loop() {
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) { if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE || state == SYNC_COMPLETE) {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (autoReturnAt != 0 && millis() >= autoReturnAt) {
returnToReader();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
returnToReader(); returnToReader();
} }
return; return;
+9 -1
View File
@@ -40,7 +40,7 @@ class KOReaderSyncActivity final : public Activity {
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING || state == UPLOADING; }
private: private:
enum State { enum State {
@@ -50,6 +50,7 @@ class KOReaderSyncActivity final : public Activity {
SHOWING_RESULT, SHOWING_RESULT,
UPLOADING, UPLOADING,
UPLOAD_COMPLETE, UPLOAD_COMPLETE,
SYNC_COMPLETE,
NO_REMOTE_PROGRESS, NO_REMOTE_PROGRESS,
SYNC_FAILED, SYNC_FAILED,
NO_CREDENTIALS NO_CREDENTIALS
@@ -78,6 +79,10 @@ class KOReaderSyncActivity final : public Activity {
// Selection in result screen (0=Apply, 1=Upload) // Selection in result screen (0=Apply, 1=Upload)
int selectedOption = 0; int selectedOption = 0;
// Timed return for successful smart-sync terminal states.
unsigned long autoReturnAt = 0;
static constexpr unsigned long AUTO_RETURN_DELAY_MS = 1200;
// Tracks whether this session activated WiFi. Set in onEnter past the credentials // Tracks whether this session activated WiFi. Set in onEnter past the credentials
// check; checked in onExit to decide whether to silent-reboot. Can't rely on // check; checked in onExit to decide whether to silent-reboot. Can't rely on
// WiFi.getMode() because performUpload() calls esp_wifi_stop() on the way out, // WiFi.getMode() because performUpload() calls esp_wifi_stop() on the way out,
@@ -87,6 +92,9 @@ class KOReaderSyncActivity final : public Activity {
void onWifiSelectionComplete(bool success); void onWifiSelectionComplete(bool success);
void performSync(); void performSync();
void performUpload(); void performUpload();
bool smartSyncEnabled() const;
void markAutoReturn();
void completeAlreadySynced();
void ensureEpubLoaded(); void ensureEpubLoaded();
void saveProgressAndReturn(int spineIndex, int page); void saveProgressAndReturn(int spineIndex, int page);
void returnToReader(); void returnToReader();
@@ -13,9 +13,10 @@
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
constexpr int MENU_ITEMS = 6; constexpr int MENU_ITEMS = 7;
const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL,
StrId::STR_DOCUMENT_MATCHING, StrId::STR_SEND_METADATA, StrId::STR_AUTHENTICATE}; StrId::STR_DOCUMENT_MATCHING, StrId::STR_SEND_METADATA, StrId::STR_SYNC_BEHAVIOR,
StrId::STR_AUTHENTICATE};
} // namespace } // namespace
void KOReaderSettingsActivity::onEnter() { void KOReaderSettingsActivity::onEnter() {
@@ -103,6 +104,14 @@ void KOReaderSettingsActivity::handleSelection() {
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
requestUpdate(); requestUpdate();
} else if (selectedIndex == 5) { } else if (selectedIndex == 5) {
// Sync behavior - toggle between Ask and Smart
const auto current = KOREADER_STORE.getSyncBehavior();
const auto newBehavior = (current == KOReaderSyncBehavior::ASK_EVERY_TIME) ? KOReaderSyncBehavior::SMART
: KOReaderSyncBehavior::ASK_EVERY_TIME;
KOREADER_STORE.setSyncBehavior(newBehavior);
KOREADER_STORE.saveToFile();
requestUpdate();
} else if (selectedIndex == 6) {
// Authenticate // Authenticate
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
// Can't authenticate without credentials - just show message briefly // Can't authenticate without credentials - just show message briefly
@@ -143,6 +152,9 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
} else if (index == 4) { } else if (index == 4) {
return KOREADER_STORE.getSendMetadata() ? std::string(tr(STR_STATE_ON)) : std::string(tr(STR_STATE_OFF)); return KOREADER_STORE.getSendMetadata() ? std::string(tr(STR_STATE_ON)) : std::string(tr(STR_STATE_OFF));
} else if (index == 5) { } else if (index == 5) {
return KOREADER_STORE.getSyncBehavior() == KOReaderSyncBehavior::SMART ? std::string(tr(STR_SMART_SYNC))
: std::string(tr(STR_ASK_EVERY_TIME));
} else if (index == 6) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
} }
return std::string(tr(STR_NOT_SET)); return std::string(tr(STR_NOT_SET));