Files
crosspoint-reader-mod/src/JsonSettingsIO.cpp
cottongin 60a3e21c0e mod: Phase 3 — Re-port unmerged upstream PRs
Re-applied upstream PRs not yet merged to upstream/master:

- #1055: Byte-level framebuffer writes (fillPhysicalHSpan*,
  optimized fillRect/drawLine/fillRectDither/fillPolygon)
- #1027: Word-width cache (FNV-1a, 128-entry) and hyphenation
  early exit in ParsedText for 7-9% layout speedup
- #1068: Already present in upstream — URL hyphenation fix
- #1019: Already present in upstream — file extensions in browser
- #1090/#1185/#1217: KOReader sync improvements — binary credential
  store, document hash caching, ChapterXPathIndexer integration
- #1209: OPDS multi-server — OpdsBookBrowserActivity accepts
  OpdsServer, directory picker for downloads, download-complete
  prompt with open/back options
- #857: Dictionary activities already ported in Phase 1/2
- #1003: Placeholder cover already integrated in Phase 2

Also fixed: STR_OFF i18n string, include paths, replaced
Epub::isValidThumbnailBmp with Storage.exists, replaced
StringUtils::checkFileExtension with FsHelpers equivalents.

Made-with: Cursor
2026-03-07 16:15:42 -05:00

305 lines
11 KiB
C++

#include "JsonSettingsIO.h"
#include <ArduinoJson.h>
#include <HalStorage.h>
#include <Logging.h>
#include <ObfuscationUtils.h>
#include <cstring>
#include <string>
#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<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;
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;
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;
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<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("");
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["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<JsonArray>();
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;
}