## 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>
129 lines
4.1 KiB
C++
129 lines
4.1 KiB
C++
#include "KOReaderCredentialStore.h"
|
|
|
|
#include <Logging.h>
|
|
#include <MD5Builder.h>
|
|
#include <ObfuscationUtils.h>
|
|
|
|
namespace {
|
|
// Default sync server URL
|
|
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
|
|
} // namespace
|
|
|
|
void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
|
|
doc["username"] = getUsername();
|
|
doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword());
|
|
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) {
|
|
std::string user = doc["username"] | "";
|
|
|
|
bool needsResave = false;
|
|
std::string pass = extractPassword(doc, needsResave);
|
|
|
|
setCredentials(user, pass);
|
|
setServerUrl(doc["serverUrl"] | "");
|
|
|
|
uint8_t method = doc["matchMethod"] | (uint8_t)0;
|
|
if (method <= static_cast<uint8_t>(DocumentMatchMethod::BINARY)) {
|
|
setMatchMethod(static_cast<DocumentMatchMethod>(method));
|
|
} else {
|
|
LOG_DBG("KRS", "Invalid matchMethod %u in JSON, resetting to FILENAME", method);
|
|
setMatchMethod(DocumentMatchMethod::FILENAME);
|
|
}
|
|
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();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
|
|
username = user;
|
|
password = pass;
|
|
LOG_DBG("KRS", "Set credentials for user: %s", user.c_str());
|
|
}
|
|
|
|
std::string KOReaderCredentialStore::getMd5Password() const {
|
|
if (password.empty()) {
|
|
return "";
|
|
}
|
|
|
|
// Calculate MD5 hash of password using ESP32's MD5Builder
|
|
MD5Builder md5;
|
|
md5.begin();
|
|
md5.add(password.c_str());
|
|
md5.calculate();
|
|
|
|
return md5.toString().c_str();
|
|
}
|
|
|
|
bool KOReaderCredentialStore::hasCredentials() const { return !username.empty() && !password.empty(); }
|
|
|
|
void KOReaderCredentialStore::clearCredentials() {
|
|
username.clear();
|
|
password.clear();
|
|
saveToFile();
|
|
LOG_DBG("KRS", "Cleared KOReader credentials");
|
|
}
|
|
|
|
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
|
|
serverUrl = url;
|
|
LOG_DBG("KRS", "Set server URL: %s", url.empty() ? "(default)" : url.c_str());
|
|
}
|
|
|
|
std::string KOReaderCredentialStore::getBaseUrl() const {
|
|
std::string url;
|
|
if (serverUrl.empty()) {
|
|
url = DEFAULT_SERVER_URL;
|
|
} else if (serverUrl.find("://") == std::string::npos) {
|
|
// Normalize URL: add http:// if no protocol specified (local servers typically don't have SSL)
|
|
url = "http://" + serverUrl;
|
|
} else {
|
|
url = serverUrl;
|
|
}
|
|
|
|
// Strip trailing slashes to avoid double-slash in API paths
|
|
while (!url.empty() && url.back() == '/') {
|
|
url.pop_back();
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
|
matchMethod = method;
|
|
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
|
}
|
|
|
|
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");
|
|
}
|