feat: Add KOReader Sync and OPDS Browser settings to web UI

Web settings now mirror the device UI structure:
- Display, Reader, Controls, System (same as device categories)
- KOReader Sync: username, password, server URL, document matching
- OPDS Browser: server URL, username, password

Adds dynamic getter/setter support to SettingInfo for settings
backed by stores other than CrossPointSettings (KOReaderCredentialStore).
This commit is contained in:
Jesse Vincent 2026-02-01 19:55:14 -08:00
parent 98bd0572e0
commit 76bfe2d35d
3 changed files with 89 additions and 16 deletions

View File

@ -2,11 +2,13 @@
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "KOReaderCredentialStore.h"
#include "activities/settings/CategorySettingsActivity.h" #include "activities/settings/CategorySettingsActivity.h"
// Returns the flat list of all settings for the web API. // Returns the list of all settings, used by both the device UI and the web API.
// This is used by CrossPointWebServer to expose settings over HTTP. // Categories match the device UI grouping. Settings with categories that don't
// Categories match the device UI grouping for consistency. // match a device UI category (e.g. "KOReader Sync", "OPDS Browser") are
// web-only — they correspond to device sub-screens accessed via Action items.
inline std::vector<SettingInfo> getSettingsList() { inline std::vector<SettingInfo> getSettingsList() {
return { return {
// Display // Display
@ -55,12 +57,28 @@ inline std::vector<SettingInfo> getSettingsList() {
SettingInfo::Enum("sleepTimeout", "Time to Sleep", "System", &CrossPointSettings::sleepTimeout, SettingInfo::Enum("sleepTimeout", "Time to Sleep", "System", &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}), {"1 min", "5 min", "10 min", "15 min", "30 min"}),
// Calibre / OPDS // KOReader Sync (device sub-screen accessed via System > KOReader Sync action)
SettingInfo::String("opdsServerUrl", "OPDS Server URL", "Calibre", SETTINGS.opdsServerUrl, SettingInfo::DynamicString(
"koUsername", "Username", "KOReader Sync", [] { return KOREADER_STORE.getUsername(); },
[](const std::string& v) { KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); }, 64),
SettingInfo::DynamicString(
"koPassword", "Password", "KOReader Sync", [] { return KOREADER_STORE.getPassword(); },
[](const std::string& v) { KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); }, 64),
SettingInfo::DynamicString(
"koServerUrl", "Sync Server URL", "KOReader Sync", [] { return KOREADER_STORE.getServerUrl(); },
[](const std::string& v) { KOREADER_STORE.setServerUrl(v); }, 128),
SettingInfo::DynamicEnum(
"koMatchMethod", "Document Matching", "KOReader Sync",
[] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); },
[](uint8_t v) { KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v)); },
{"Filename", "Binary"}),
// OPDS Browser (device sub-screen accessed via System > OPDS Browser action)
SettingInfo::String("opdsServerUrl", "Server URL", "OPDS Browser", SETTINGS.opdsServerUrl,
sizeof(SETTINGS.opdsServerUrl) - 1), sizeof(SETTINGS.opdsServerUrl) - 1),
SettingInfo::String("opdsUsername", "OPDS Username", "Calibre", SETTINGS.opdsUsername, SettingInfo::String("opdsUsername", "Username", "OPDS Browser", SETTINGS.opdsUsername,
sizeof(SETTINGS.opdsUsername) - 1), sizeof(SETTINGS.opdsUsername) - 1),
SettingInfo::String("opdsPassword", "OPDS Password", "Calibre", SETTINGS.opdsPassword, SettingInfo::String("opdsPassword", "Password", "OPDS Browser", SETTINGS.opdsPassword,
sizeof(SETTINGS.opdsPassword) - 1), sizeof(SETTINGS.opdsPassword) - 1),
}; };
} }

View File

@ -30,6 +30,12 @@ struct SettingInfo {
}; };
ValueRange valueRange; ValueRange valueRange;
// Dynamic accessors for settings not in CrossPointSettings
std::function<uint8_t()> valueGetter;
std::function<void(uint8_t)> valueSetter;
std::function<std::string()> stringGetter;
std::function<void(const std::string&)> stringSetter;
static SettingInfo Toggle(const char* key, const char* name, const char* category, static SettingInfo Toggle(const char* key, const char* name, const char* category,
uint8_t CrossPointSettings::* ptr) { uint8_t CrossPointSettings::* ptr) {
return {key, name, category, SettingType::TOGGLE, ptr, nullptr, 0, {}, {}}; return {key, name, category, SettingType::TOGGLE, ptr, nullptr, 0, {}, {}};
@ -52,6 +58,34 @@ struct SettingInfo {
static SettingInfo String(const char* key, const char* name, const char* category, char* ptr, size_t maxLen) { static SettingInfo String(const char* key, const char* name, const char* category, char* ptr, size_t maxLen) {
return {key, name, category, SettingType::STRING, nullptr, ptr, maxLen, {}, {}}; return {key, name, category, SettingType::STRING, nullptr, ptr, maxLen, {}, {}};
} }
static SettingInfo DynamicEnum(const char* key, const char* name, const char* category,
std::function<uint8_t()> getter, std::function<void(uint8_t)> setter,
std::vector<std::string> values) {
SettingInfo info{};
info.key = key;
info.name = name;
info.category = category;
info.type = SettingType::ENUM;
info.enumValues = std::move(values);
info.valueGetter = std::move(getter);
info.valueSetter = std::move(setter);
return info;
}
static SettingInfo DynamicString(const char* key, const char* name, const char* category,
std::function<std::string()> getter,
std::function<void(const std::string&)> setter, size_t maxLen) {
SettingInfo info{};
info.key = key;
info.name = name;
info.category = category;
info.type = SettingType::STRING;
info.stringMaxLen = maxLen;
info.stringGetter = std::move(getter);
info.stringSetter = std::move(setter);
return info;
}
}; };
class CategorySettingsActivity final : public ActivityWithSubactivity { class CategorySettingsActivity final : public ActivityWithSubactivity {

View File

@ -972,12 +972,12 @@ void CrossPointWebServer::handleGetSettings() const {
switch (setting.type) { switch (setting.type) {
case SettingType::TOGGLE: case SettingType::TOGGLE:
obj["type"] = "toggle"; obj["type"] = "toggle";
obj["value"] = SETTINGS.*(setting.valuePtr) ? 1 : 0; obj["value"] = (setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr)) ? 1 : 0;
break; break;
case SettingType::ENUM: { case SettingType::ENUM: {
obj["type"] = "enum"; obj["type"] = "enum";
obj["value"] = SETTINGS.*(setting.valuePtr); obj["value"] = setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr);
JsonArray opts = obj["options"].to<JsonArray>(); JsonArray opts = obj["options"].to<JsonArray>();
for (const auto& opt : setting.enumValues) { for (const auto& opt : setting.enumValues) {
opts.add(opt); opts.add(opt);
@ -987,7 +987,7 @@ void CrossPointWebServer::handleGetSettings() const {
case SettingType::VALUE: case SettingType::VALUE:
obj["type"] = "value"; obj["type"] = "value";
obj["value"] = SETTINGS.*(setting.valuePtr); obj["value"] = setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr);
obj["min"] = setting.valueRange.min; obj["min"] = setting.valueRange.min;
obj["max"] = setting.valueRange.max; obj["max"] = setting.valueRange.max;
obj["step"] = setting.valueRange.step; obj["step"] = setting.valueRange.step;
@ -995,7 +995,11 @@ void CrossPointWebServer::handleGetSettings() const {
case SettingType::STRING: case SettingType::STRING:
obj["type"] = "string"; obj["type"] = "string";
obj["value"] = setting.stringPtr; if (setting.stringGetter) {
obj["value"] = setting.stringGetter();
} else {
obj["value"] = setting.stringPtr;
}
obj["maxLength"] = setting.stringMaxLen; obj["maxLength"] = setting.stringMaxLen;
break; break;
@ -1043,7 +1047,11 @@ void CrossPointWebServer::handlePostSettings() {
switch (setting.type) { switch (setting.type) {
case SettingType::TOGGLE: { case SettingType::TOGGLE: {
const int value = doc[setting.key].as<int>(); const int value = doc[setting.key].as<int>();
SETTINGS.*(setting.valuePtr) = value ? 1 : 0; if (setting.valueSetter) {
setting.valueSetter(value ? 1 : 0);
} else {
SETTINGS.*(setting.valuePtr) = value ? 1 : 0;
}
updatedCount++; updatedCount++;
break; break;
} }
@ -1051,7 +1059,11 @@ void CrossPointWebServer::handlePostSettings() {
case SettingType::ENUM: { case SettingType::ENUM: {
const int value = doc[setting.key].as<int>(); const int value = doc[setting.key].as<int>();
if (value >= 0 && value < static_cast<int>(setting.enumValues.size())) { if (value >= 0 && value < static_cast<int>(setting.enumValues.size())) {
SETTINGS.*(setting.valuePtr) = static_cast<uint8_t>(value); if (setting.valueSetter) {
setting.valueSetter(static_cast<uint8_t>(value));
} else {
SETTINGS.*(setting.valuePtr) = static_cast<uint8_t>(value);
}
updatedCount++; updatedCount++;
} }
break; break;
@ -1060,7 +1072,11 @@ void CrossPointWebServer::handlePostSettings() {
case SettingType::VALUE: { case SettingType::VALUE: {
const int value = doc[setting.key].as<int>(); const int value = doc[setting.key].as<int>();
if (value >= setting.valueRange.min && value <= setting.valueRange.max) { if (value >= setting.valueRange.min && value <= setting.valueRange.max) {
SETTINGS.*(setting.valuePtr) = static_cast<uint8_t>(value); if (setting.valueSetter) {
setting.valueSetter(static_cast<uint8_t>(value));
} else {
SETTINGS.*(setting.valuePtr) = static_cast<uint8_t>(value);
}
updatedCount++; updatedCount++;
} }
break; break;
@ -1069,8 +1085,12 @@ void CrossPointWebServer::handlePostSettings() {
case SettingType::STRING: { case SettingType::STRING: {
const char* value = doc[setting.key].as<const char*>(); const char* value = doc[setting.key].as<const char*>();
if (value != nullptr) { if (value != nullptr) {
strncpy(setting.stringPtr, value, setting.stringMaxLen); if (setting.stringSetter) {
setting.stringPtr[setting.stringMaxLen] = '\0'; setting.stringSetter(value);
} else {
strncpy(setting.stringPtr, value, setting.stringMaxLen);
setting.stringPtr[setting.stringMaxLen] = '\0';
}
updatedCount++; updatedCount++;
} }
break; break;
@ -1083,6 +1103,7 @@ void CrossPointWebServer::handlePostSettings() {
if (updatedCount > 0) { if (updatedCount > 0) {
SETTINGS.saveToFile(); SETTINGS.saveToFile();
KOREADER_STORE.saveToFile();
Serial.printf("[%lu] [WEB] Updated %d settings and saved to file\n", millis(), updatedCount); Serial.printf("[%lu] [WEB] Updated %d settings and saved to file\n", millis(), updatedCount);
} }