519 lines
20 KiB
C++
519 lines
20 KiB
C++
#include "JsonSettingsIO.h"
|
|
|
|
#include <ArduinoJson.h>
|
|
#include <HalStorage.h>
|
|
#include <Logging.h>
|
|
#include <ObfuscationUtils.h>
|
|
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "CrossPointState.h"
|
|
#include "KOReaderCredentialStore.h"
|
|
#include "OpdsServerStore.h"
|
|
#include "RecentBooksStore.h"
|
|
#include "SettingsList.h"
|
|
#include "WifiCredentialStore.h"
|
|
|
|
// Convert legacy settings.
|
|
void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
|
|
switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(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;
|
|
// Information about a pending KOReader sync session
|
|
JsonObject sync = doc["koReaderSyncSession"].to<JsonObject>();
|
|
sync["active"] = s.koReaderSyncSession.active;
|
|
sync["epubPath"] = s.koReaderSyncSession.epubPath;
|
|
sync["spineIndex"] = s.koReaderSyncSession.spineIndex;
|
|
sync["page"] = s.koReaderSyncSession.page;
|
|
sync["totalPagesInSpine"] = s.koReaderSyncSession.totalPagesInSpine;
|
|
sync["paragraphIndex"] = s.koReaderSyncSession.paragraphIndex;
|
|
sync["hasParagraphIndex"] = s.koReaderSyncSession.hasParagraphIndex;
|
|
sync["xhtmlSeekHint"] = s.koReaderSyncSession.xhtmlSeekHint;
|
|
sync["intent"] = static_cast<uint8_t>(s.koReaderSyncSession.intent);
|
|
sync["outcome"] = static_cast<uint8_t>(s.koReaderSyncSession.outcome);
|
|
sync["resultSpineIndex"] = s.koReaderSyncSession.resultSpineIndex;
|
|
sync["resultPage"] = s.koReaderSyncSession.resultPage;
|
|
sync["resultParagraphIndex"] = s.koReaderSyncSession.resultParagraphIndex;
|
|
sync["resultHasParagraphIndex"] = s.koReaderSyncSession.resultHasParagraphIndex;
|
|
sync["exitToHomeAfterSync"] = s.koReaderSyncSession.exitToHomeAfterSync;
|
|
sync["autoPullEpubPath"] = s.koReaderSyncSession.autoPullEpubPath;
|
|
// Information about a pending bookmark jump
|
|
JsonObject jump = doc["pendingBookmarkJump"].to<JsonObject>();
|
|
jump["active"] = s.pendingBookmarkJump.active;
|
|
jump["bookPath"] = s.pendingBookmarkJump.bookPath;
|
|
jump["spineIndex"] = s.pendingBookmarkJump.spineIndex;
|
|
jump["pageNumber"] = s.pendingBookmarkJump.pageNumber;
|
|
|
|
if (doc.overflowed()) {
|
|
LOG_ERR("CPS", "JSON document overflowed while building state");
|
|
return false;
|
|
}
|
|
|
|
FsFile file;
|
|
if (!Storage.openFileForWrite("CPS", path, file)) {
|
|
return false;
|
|
}
|
|
|
|
const size_t expected = measureJson(doc);
|
|
const size_t written = serializeJson(doc, file);
|
|
file.flush();
|
|
if (!file.close()) {
|
|
return false;
|
|
}
|
|
return written == expected;
|
|
}
|
|
|
|
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"] | SIZE_MAX;
|
|
s.readerActivityLoadCount = doc["readerActivityLoadCount"] | (uint8_t)0;
|
|
s.lastSleepFromReader = doc["lastSleepFromReader"] | false;
|
|
JsonObject sync = doc["koReaderSyncSession"].as<JsonObject>();
|
|
s.koReaderSyncSession.active = sync["active"] | false;
|
|
s.koReaderSyncSession.epubPath = sync["epubPath"] | std::string("");
|
|
s.koReaderSyncSession.spineIndex = sync["spineIndex"] | 0;
|
|
s.koReaderSyncSession.page = sync["page"] | 0;
|
|
s.koReaderSyncSession.totalPagesInSpine = sync["totalPagesInSpine"] | 0;
|
|
s.koReaderSyncSession.paragraphIndex = sync["paragraphIndex"] | (uint16_t)0;
|
|
s.koReaderSyncSession.hasParagraphIndex = sync["hasParagraphIndex"] | false;
|
|
s.koReaderSyncSession.xhtmlSeekHint = sync["xhtmlSeekHint"] | (uint32_t)0;
|
|
s.koReaderSyncSession.intent =
|
|
static_cast<KOReaderSyncIntentState>(sync["intent"] | static_cast<uint8_t>(KOReaderSyncIntentState::COMPARE));
|
|
s.koReaderSyncSession.outcome =
|
|
static_cast<KOReaderSyncOutcomeState>(sync["outcome"] | static_cast<uint8_t>(KOReaderSyncOutcomeState::NONE));
|
|
s.koReaderSyncSession.resultSpineIndex = sync["resultSpineIndex"] | 0;
|
|
s.koReaderSyncSession.resultPage = sync["resultPage"] | 0;
|
|
s.koReaderSyncSession.resultParagraphIndex = sync["resultParagraphIndex"] | (uint16_t)0;
|
|
s.koReaderSyncSession.resultHasParagraphIndex = sync["resultHasParagraphIndex"] | false;
|
|
s.koReaderSyncSession.exitToHomeAfterSync = sync["exitToHomeAfterSync"] | false;
|
|
s.koReaderSyncSession.autoPullEpubPath = sync["autoPullEpubPath"] | std::string("");
|
|
if (s.koReaderSyncSession.autoPullEpubPath.empty() && (sync["autoPullOnOpen"] | false)) {
|
|
LOG_DBG("CPS", "Legacy autoPullOnOpen state found without epubPath - ignoring");
|
|
}
|
|
|
|
JsonObject jump = doc["pendingBookmarkJump"].as<JsonObject>();
|
|
s.pendingBookmarkJump.active = jump["active"] | false;
|
|
s.pendingBookmarkJump.bookPath = jump["bookPath"] | std::string("");
|
|
s.pendingBookmarkJump.spineIndex = jump["spineIndex"] | (uint16_t)0;
|
|
s.pendingBookmarkJump.pageNumber = jump["pageNumber"] | (uint16_t)0;
|
|
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;
|
|
|
|
// Font family uses a DynamicEnumCtx in SettingsList (no valuePtr) so the generic
|
|
// loop above skips it. Save manually.
|
|
doc["fontFamily"] = s.fontFamily;
|
|
if (s.sdFontFamilyName[0] != '\0') {
|
|
doc["sdFontFamilyName"] = s.sdFontFamilyName;
|
|
}
|
|
|
|
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);
|
|
|
|
// Font family uses a DynamicEnumCtx in SettingsList (no valuePtr) so the generic
|
|
// loop above skips it. Load manually.
|
|
s.fontFamily = clamp(doc["fontFamily"] | (uint8_t)CrossPointSettings::BOOKERLY,
|
|
CrossPointSettings::BUILTIN_FONT_COUNT, CrossPointSettings::BOOKERLY);
|
|
const char* sfn = doc["sdFontFamilyName"] | "";
|
|
strncpy(s.sdFontFamilyName, sfn, sizeof(s.sdFontFamilyName) - 1);
|
|
s.sdFontFamilyName[sizeof(s.sdFontFamilyName) - 1] = '\0';
|
|
|
|
LOG_DBG("CPS", "Settings loaded from file");
|
|
|
|
return true;
|
|
}
|
|
|
|
// ---- KOReaderCredentialStore ----
|
|
|
|
bool JsonSettingsIO::saveKOReader(const KOReaderCredentialStore& store, const char* path) {
|
|
JsonDocument doc;
|
|
doc["username"] = store.getUsername();
|
|
doc["password_obf"] = obfuscation::obfuscateToBase64(store.getPassword());
|
|
doc["serverUrl"] = store.getServerUrl();
|
|
doc["matchMethod"] = static_cast<uint8_t>(store.getMatchMethod());
|
|
|
|
String json;
|
|
serializeJson(doc, json);
|
|
return Storage.writeFile(path, json);
|
|
}
|
|
|
|
bool JsonSettingsIO::loadKOReader(KOReaderCredentialStore& store, const char* json, bool* needsResave) {
|
|
if (needsResave) *needsResave = false;
|
|
JsonDocument doc;
|
|
auto error = deserializeJson(doc, json);
|
|
if (error) {
|
|
LOG_ERR("KRS", "JSON parse error: %s", error.c_str());
|
|
return false;
|
|
}
|
|
|
|
store.username = doc["username"] | std::string("");
|
|
bool ok = false;
|
|
store.password = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok);
|
|
if (!ok || store.password.empty()) {
|
|
store.password = doc["password"] | std::string("");
|
|
if (!store.password.empty() && needsResave) *needsResave = true;
|
|
}
|
|
store.serverUrl = doc["serverUrl"] | std::string("");
|
|
uint8_t method = doc["matchMethod"] | (uint8_t)0;
|
|
store.matchMethod = static_cast<DocumentMatchMethod>(method);
|
|
|
|
LOG_DBG("KRS", "Loaded KOReader credentials for user: %s", store.username.c_str());
|
|
return true;
|
|
}
|
|
|
|
// ---- WifiCredentialStore ----
|
|
|
|
bool JsonSettingsIO::saveWifi(const WifiCredentialStore& store, const char* path) {
|
|
JsonDocument doc;
|
|
doc["lastConnectedSsid"] = store.getLastConnectedSsid();
|
|
doc["lastKnownMacAddress"] = store.getLastKnownMacAddress();
|
|
|
|
JsonArray arr = doc["credentials"].to<JsonArray>();
|
|
for (const auto& cred : store.getCredentials()) {
|
|
JsonObject obj = arr.add<JsonObject>();
|
|
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("");
|
|
|
|
const auto isValidDashedMac = [](const std::string& value) -> bool {
|
|
if (value.empty()) {
|
|
return true;
|
|
}
|
|
if (value.size() != 17) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < value.size(); i++) {
|
|
if (i == 2 || i == 5 || i == 8 || i == 11 || i == 14) {
|
|
if (value[i] != '-') {
|
|
return false;
|
|
}
|
|
} else if (!std::isxdigit(static_cast<unsigned char>(value[i]))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
store.lastKnownMacAddress = doc["lastKnownMacAddress"] | std::string("");
|
|
if (!isValidDashedMac(store.lastKnownMacAddress)) {
|
|
store.lastKnownMacAddress.clear();
|
|
if (needsResave) {
|
|
*needsResave = true;
|
|
}
|
|
}
|
|
|
|
store.credentials.clear();
|
|
JsonArray arr = doc["credentials"].as<JsonArray>();
|
|
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<JsonArray>();
|
|
for (const auto& book : store.getBooks()) {
|
|
JsonObject obj = arr.add<JsonObject>();
|
|
obj["path"] = book.path;
|
|
obj["title"] = book.title;
|
|
obj["author"] = book.author;
|
|
obj["series"] = book.series;
|
|
obj["coverBmpPath"] = book.coverBmpPath;
|
|
obj["embeddedStyleOverride"] = book.embeddedStyleOverride;
|
|
obj["imageRenderingOverride"] = book.imageRenderingOverride;
|
|
obj["fontFamilyOverride"] = book.fontFamilyOverride;
|
|
obj["fontSizeOverride"] = book.fontSizeOverride;
|
|
obj["bionicReadingOverride"] = book.bionicReadingOverride;
|
|
}
|
|
|
|
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<JsonArray>();
|
|
auto clampInt8 = [](int value, int minValue, int maxValue, int8_t fallback) -> int8_t {
|
|
if (value < minValue || value > maxValue) {
|
|
return fallback;
|
|
}
|
|
return static_cast<int8_t>(value);
|
|
};
|
|
|
|
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.series = obj["series"] | std::string("");
|
|
book.coverBmpPath = obj["coverBmpPath"] | std::string("");
|
|
book.embeddedStyleOverride = clampInt8(obj["embeddedStyleOverride"] | -1, -1, 1, -1);
|
|
book.imageRenderingOverride = clampInt8(obj["imageRenderingOverride"] | -1, -1, 2, -1);
|
|
book.fontFamilyOverride =
|
|
clampInt8(obj["fontFamilyOverride"] | -1, -1, CrossPointSettings::FONT_FAMILY_COUNT - 1, -1);
|
|
book.fontSizeOverride = clampInt8(obj["fontSizeOverride"] | -1, -1, CrossPointSettings::FONT_SIZE_COUNT - 1, -1);
|
|
book.bionicReadingOverride = clampInt8(obj["bionicReadingOverride"] | -1, -1, 1, -1);
|
|
store.recentBooks.push_back(book);
|
|
}
|
|
|
|
LOG_DBG("RBS", "Recent books loaded from file (%d entries)", store.getCount());
|
|
return true;
|
|
}
|
|
|
|
// ---- OpdsServerStore ----
|
|
// Follows the same save/load pattern as WifiCredentialStore above.
|
|
// Passwords are XOR-obfuscated with the device MAC and base64-encoded ("password_obf" key).
|
|
|
|
bool JsonSettingsIO::saveOpds(const OpdsServerStore& store, const char* path) {
|
|
JsonDocument doc;
|
|
|
|
JsonArray arr = doc["servers"].to<JsonArray>();
|
|
for (const auto& server : store.getServers()) {
|
|
JsonObject obj = arr.add<JsonObject>();
|
|
obj["name"] = server.name;
|
|
obj["url"] = server.url;
|
|
obj["username"] = server.username;
|
|
obj["password_obf"] = obfuscation::obfuscateToBase64(server.password);
|
|
}
|
|
|
|
String json;
|
|
serializeJson(doc, json);
|
|
return Storage.writeFile(path, json);
|
|
}
|
|
|
|
bool JsonSettingsIO::loadOpds(OpdsServerStore& store, const char* json, bool* needsResave) {
|
|
if (needsResave) *needsResave = false;
|
|
JsonDocument doc;
|
|
auto error = deserializeJson(doc, json);
|
|
if (error) {
|
|
LOG_ERR("OPS", "JSON parse error: %s", error.c_str());
|
|
return false;
|
|
}
|
|
|
|
store.servers.clear();
|
|
JsonArray arr = doc["servers"].as<JsonArray>();
|
|
for (JsonObject obj : arr) {
|
|
if (store.servers.size() >= OpdsServerStore::MAX_SERVERS) break;
|
|
OpdsServer server;
|
|
server.name = obj["name"] | std::string("");
|
|
server.url = obj["url"] | std::string("");
|
|
server.username = obj["username"] | std::string("");
|
|
// Try the obfuscated key first; fall back to plaintext "password" for
|
|
// files written before obfuscation was added (or hand-edited JSON).
|
|
bool ok = false;
|
|
server.password = obfuscation::deobfuscateFromBase64(obj["password_obf"] | "", &ok);
|
|
if (!ok || server.password.empty()) {
|
|
server.password = obj["password"] | std::string("");
|
|
if (!server.password.empty() && needsResave) *needsResave = true;
|
|
}
|
|
store.servers.push_back(std::move(server));
|
|
}
|
|
|
|
LOG_DBG("OPS", "Loaded %zu OPDS servers from file", store.servers.size());
|
|
return true;
|
|
}
|