#include "JsonSettingsIO.h" #include #include #include #include #include #include #include "CrossPointSettings.h" #include "CrossPointState.h" #include "KOReaderCredentialStore.h" #include "RecentBooksStore.h" #include "SettingsList.h" #include "WifiCredentialStore.h" // Convert legacy settings. void applyLegacyStatusBarSettings(CrossPointSettings& settings) { switch (static_cast(settings.statusBar)) { case CrossPointSettings::NONE: settings.statusBarChapterPageCount = 0; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarBattery = 0; break; case CrossPointSettings::NO_PROGRESS: settings.statusBarChapterPageCount = 0; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::BOOK_PROGRESS_BAR: settings.statusBarChapterPageCount = 1; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR: settings.statusBarChapterPageCount = 1; settings.statusBarBookProgressPercentage = 0; settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS; settings.statusBarTitle = CrossPointSettings::HIDE_TITLE; settings.statusBarBattery = 0; break; case CrossPointSettings::CHAPTER_PROGRESS_BAR: settings.statusBarChapterPageCount = 0; settings.statusBarBookProgressPercentage = 1; settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; case CrossPointSettings::FULL: default: settings.statusBarChapterPageCount = 1; settings.statusBarBookProgressPercentage = 1; settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS; settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE; settings.statusBarBattery = 1; break; } } // ---- CrossPointState ---- bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) { JsonDocument doc; doc["openEpubPath"] = s.openEpubPath; doc["lastSleepImage"] = s.lastSleepImage; doc["readerActivityLoadCount"] = s.readerActivityLoadCount; doc["lastSleepFromReader"] = s.lastSleepFromReader; String json; serializeJson(doc, json); return Storage.writeFile(path, json); } bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("CPS", "JSON parse error: %s", error.c_str()); return false; } s.openEpubPath = doc["openEpubPath"] | std::string(""); s.lastSleepImage = doc["lastSleepImage"] | (uint8_t)0; s.readerActivityLoadCount = doc["readerActivityLoadCount"] | (uint8_t)0; s.lastSleepFromReader = doc["lastSleepFromReader"] | false; return true; } // ---- CrossPointSettings ---- bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path) { JsonDocument doc; for (const auto& info : getSettingsList()) { if (!info.key) continue; // Dynamic entries (KOReader etc.) are stored in their own files — skip. if (!info.valuePtr && !info.stringOffset) continue; if (info.stringOffset) { const char* strPtr = (const char*)&s + info.stringOffset; if (info.obfuscated) { doc[std::string(info.key) + "_obf"] = obfuscation::obfuscateToBase64(strPtr); } else { doc[info.key] = strPtr; } } else { doc[info.key] = s.*(info.valuePtr); } } // Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. doc["frontButtonBack"] = s.frontButtonBack; doc["frontButtonConfirm"] = s.frontButtonConfirm; doc["frontButtonLeft"] = s.frontButtonLeft; doc["frontButtonRight"] = s.frontButtonRight; // Mod: timezone offset is int8_t, not uint8_t — handled separately doc["timezoneOffsetHours"] = s.timezoneOffsetHours; // Preferred orientations for portrait/landscape toggle (DynamicEnum, not in generic loop) doc["preferredPortrait"] = s.preferredPortrait; doc["preferredLandscape"] = s.preferredLandscape; String json; serializeJson(doc, json); return Storage.writeFile(path, json); } bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool* needsResave) { if (needsResave) *needsResave = false; JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("CPS", "JSON parse error: %s", error.c_str()); return false; } auto clamp = [](uint8_t val, uint8_t maxVal, uint8_t def) -> uint8_t { return val < maxVal ? val : def; }; // Legacy migration: if statusBarChapterPageCount is absent this is a pre-refactor settings file. // Populate s with migrated values now so the generic loop below picks them up as defaults and clamps them. if (doc["statusBarChapterPageCount"].isNull()) { applyLegacyStatusBarSettings(s); } for (const auto& info : getSettingsList()) { if (!info.key) continue; // Dynamic entries (KOReader etc.) are stored in their own files — skip. if (!info.valuePtr && !info.stringOffset) continue; if (info.stringOffset) { const char* strPtr = (const char*)&s + info.stringOffset; const std::string fieldDefault = strPtr; // current buffer = struct-initializer default std::string val; if (info.obfuscated) { bool ok = false; val = obfuscation::deobfuscateFromBase64(doc[std::string(info.key) + "_obf"] | "", &ok); if (!ok || val.empty()) { val = doc[info.key] | fieldDefault; if (val != fieldDefault && needsResave) *needsResave = true; } } else { val = doc[info.key] | fieldDefault; } char* destPtr = (char*)&s + info.stringOffset; if (info.stringMaxLen == 0) { LOG_ERR("CPS", "Misconfigured SettingInfo: stringMaxLen is 0 for key '%s'", info.key); destPtr[0] = '\0'; if (needsResave) *needsResave = true; continue; } strncpy(destPtr, val.c_str(), info.stringMaxLen - 1); destPtr[info.stringMaxLen - 1] = '\0'; } else { const uint8_t fieldDefault = s.*(info.valuePtr); // struct-initializer default, read before we overwrite it uint8_t v = doc[info.key] | fieldDefault; if (info.type == SettingType::ENUM) { v = clamp(v, (uint8_t)info.enumValues.size(), fieldDefault); } else if (info.type == SettingType::TOGGLE) { v = clamp(v, (uint8_t)2, fieldDefault); } else if (info.type == SettingType::VALUE) { if (v < info.valueRange.min) v = info.valueRange.min; else if (v > info.valueRange.max) v = info.valueRange.max; } s.*(info.valuePtr) = v; } } // Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. using S = CrossPointSettings; s.frontButtonBack = clamp(doc["frontButtonBack"] | (uint8_t)S::FRONT_HW_BACK, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_BACK); s.frontButtonConfirm = clamp(doc["frontButtonConfirm"] | (uint8_t)S::FRONT_HW_CONFIRM, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_CONFIRM); s.frontButtonLeft = clamp(doc["frontButtonLeft"] | (uint8_t)S::FRONT_HW_LEFT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_LEFT); s.frontButtonRight = clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT); CrossPointSettings::validateFrontButtonMapping(s); // Mod: timezone offset is int8_t, not uint8_t s.timezoneOffsetHours = doc["timezoneOffsetHours"] | (int8_t)0; if (s.timezoneOffsetHours < -12) s.timezoneOffsetHours = -12; if (s.timezoneOffsetHours > 14) s.timezoneOffsetHours = 14; // Preferred orientations for portrait/landscape toggle (DynamicEnum, not in generic loop) auto isValidPortrait = [](uint8_t v) { return v == S::PORTRAIT || v == S::INVERTED; }; auto isValidLandscape = [](uint8_t v) { return v == S::LANDSCAPE_CW || v == S::LANDSCAPE_CCW; }; uint8_t pp = doc["preferredPortrait"] | s.preferredPortrait; s.preferredPortrait = isValidPortrait(pp) ? pp : S::PORTRAIT; uint8_t pl = doc["preferredLandscape"] | s.preferredLandscape; s.preferredLandscape = isValidLandscape(pl) ? pl : S::LANDSCAPE_CW; LOG_DBG("CPS", "Settings loaded from file"); return true; } // ---- WifiCredentialStore ---- bool JsonSettingsIO::saveWifi(const WifiCredentialStore& store, const char* path) { JsonDocument doc; doc["lastConnectedSsid"] = store.getLastConnectedSsid(); JsonArray arr = doc["credentials"].to(); for (const auto& cred : store.getCredentials()) { JsonObject obj = arr.add(); obj["ssid"] = cred.ssid; obj["password_obf"] = obfuscation::obfuscateToBase64(cred.password); } String json; serializeJson(doc, json); return Storage.writeFile(path, json); } bool JsonSettingsIO::loadWifi(WifiCredentialStore& store, const char* json, bool* needsResave) { if (needsResave) *needsResave = false; JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("WCS", "JSON parse error: %s", error.c_str()); return false; } store.lastConnectedSsid = doc["lastConnectedSsid"] | std::string(""); store.credentials.clear(); JsonArray arr = doc["credentials"].as(); for (JsonObject obj : arr) { if (store.credentials.size() >= store.MAX_NETWORKS) break; WifiCredential cred; cred.ssid = obj["ssid"] | std::string(""); bool ok = false; cred.password = obfuscation::deobfuscateFromBase64(obj["password_obf"] | "", &ok); if (!ok || cred.password.empty()) { cred.password = obj["password"] | std::string(""); if (!cred.password.empty() && needsResave) *needsResave = true; } store.credentials.push_back(cred); } LOG_DBG("WCS", "Loaded %zu WiFi credentials from file", store.credentials.size()); return true; } // ---- RecentBooksStore ---- bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char* path) { JsonDocument doc; JsonArray arr = doc["books"].to(); for (const auto& book : store.getBooks()) { JsonObject obj = arr.add(); obj["path"] = book.path; obj["title"] = book.title; obj["author"] = book.author; obj["coverBmpPath"] = book.coverBmpPath; } String json; serializeJson(doc, json); return Storage.writeFile(path, json); } bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json) { JsonDocument doc; auto error = deserializeJson(doc, json); if (error) { LOG_ERR("RBS", "JSON parse error: %s", error.c_str()); return false; } store.recentBooks.clear(); JsonArray arr = doc["books"].as(); for (JsonObject obj : arr) { if (store.getCount() >= 10) break; RecentBook book; book.path = obj["path"] | std::string(""); book.title = obj["title"] | std::string(""); book.author = obj["author"] | std::string(""); book.coverBmpPath = obj["coverBmpPath"] | std::string(""); store.recentBooks.push_back(book); } LOG_DBG("RBS", "Recent books loaded from file (%d entries)", store.getCount()); return true; }