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
+4
View File
@@ -108,6 +108,9 @@ STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "Sync Server URL"
STR_DOCUMENT_MATCHING: "Document Matching"
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_KOREADER_USERNAME: "KOReader Username"
STR_KOREADER_PASSWORD: "KOReader Password"
@@ -324,6 +327,7 @@ STR_UPLOAD_LOCAL: "Upload local progress"
STR_NO_REMOTE_MSG: "No remote progress found"
STR_UPLOAD_PROMPT: "Upload current position?"
STR_UPLOAD_SUCCESS: "Progress uploaded!"
STR_ALREADY_SYNCED: "Already synced"
STR_SYNC_FAILED_MSG: "Sync failed"
STR_SAVE_PROGRESS_FAILED: "Could not save progress"
STR_SECTION_PREFIX: "Section "
@@ -15,6 +15,7 @@ void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
doc["serverUrl"] = getServerUrl();
doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod());
doc["sendMetadata"] = getSendMetadata();
doc["syncBehavior"] = static_cast<uint8_t>(getSyncBehavior());
}
bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
@@ -35,6 +36,18 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
}
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) {
LOG_DBG("KRS", "Resaved KOReader credentials to update format");
saveToFile();
@@ -105,3 +118,11 @@ void KOReaderCredentialStore::setSendMetadata(bool enabled) {
sendMetadata = enabled;
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)
};
// 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.
* 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)
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
bool sendMetadata = false; // Send document metadata with progress sync
KOReaderSyncBehavior syncBehavior = KOReaderSyncBehavior::SMART;
// Private constructor for singleton
KOReaderCredentialStore() = default;
@@ -65,6 +72,10 @@ class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore>
// Send metadata setting
void setSendMetadata(bool enabled);
bool getSendMetadata() const { return sendMetadata; }
// Sync behavior
void setSyncBehavior(KOReaderSyncBehavior behavior);
KOReaderSyncBehavior getSyncBehavior() const { return syncBehavior; }
};
// Helper macro to access credential store