From 76bfe2d35df75d680760aad518eab615c5b85822 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sun, 1 Feb 2026 19:55:14 -0800 Subject: [PATCH] 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). --- src/SettingsList.h | 32 +++++++++++---- .../settings/CategorySettingsActivity.h | 34 ++++++++++++++++ src/network/CrossPointWebServer.cpp | 39 ++++++++++++++----- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/SettingsList.h b/src/SettingsList.h index ba52e571..05336566 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -2,11 +2,13 @@ #include #include "CrossPointSettings.h" +#include "KOReaderCredentialStore.h" #include "activities/settings/CategorySettingsActivity.h" -// Returns the flat list of all settings for the web API. -// This is used by CrossPointWebServer to expose settings over HTTP. -// Categories match the device UI grouping for consistency. +// Returns the list of all settings, used by both the device UI and the web API. +// Categories match the device UI grouping. Settings with categories that don't +// 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 getSettingsList() { return { // Display @@ -55,12 +57,28 @@ inline std::vector getSettingsList() { SettingInfo::Enum("sleepTimeout", "Time to Sleep", "System", &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}), - // Calibre / OPDS - SettingInfo::String("opdsServerUrl", "OPDS Server URL", "Calibre", SETTINGS.opdsServerUrl, + // KOReader Sync (device sub-screen accessed via System > KOReader Sync action) + 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(KOREADER_STORE.getMatchMethod()); }, + [](uint8_t v) { KOREADER_STORE.setMatchMethod(static_cast(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), - SettingInfo::String("opdsUsername", "OPDS Username", "Calibre", SETTINGS.opdsUsername, + SettingInfo::String("opdsUsername", "Username", "OPDS Browser", SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername) - 1), - SettingInfo::String("opdsPassword", "OPDS Password", "Calibre", SETTINGS.opdsPassword, + SettingInfo::String("opdsPassword", "Password", "OPDS Browser", SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword) - 1), }; } diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index c706c890..b0760865 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -30,6 +30,12 @@ struct SettingInfo { }; ValueRange valueRange; + // Dynamic accessors for settings not in CrossPointSettings + std::function valueGetter; + std::function valueSetter; + std::function stringGetter; + std::function stringSetter; + static SettingInfo Toggle(const char* key, const char* name, const char* category, uint8_t CrossPointSettings::* ptr) { 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) { return {key, name, category, SettingType::STRING, nullptr, ptr, maxLen, {}, {}}; } + + static SettingInfo DynamicEnum(const char* key, const char* name, const char* category, + std::function getter, std::function setter, + std::vector 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 getter, + std::function 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 { diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index bb0609d6..5ca469ff 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -972,12 +972,12 @@ void CrossPointWebServer::handleGetSettings() const { switch (setting.type) { case SettingType::TOGGLE: obj["type"] = "toggle"; - obj["value"] = SETTINGS.*(setting.valuePtr) ? 1 : 0; + obj["value"] = (setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr)) ? 1 : 0; break; case SettingType::ENUM: { obj["type"] = "enum"; - obj["value"] = SETTINGS.*(setting.valuePtr); + obj["value"] = setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr); JsonArray opts = obj["options"].to(); for (const auto& opt : setting.enumValues) { opts.add(opt); @@ -987,7 +987,7 @@ void CrossPointWebServer::handleGetSettings() const { case SettingType::VALUE: obj["type"] = "value"; - obj["value"] = SETTINGS.*(setting.valuePtr); + obj["value"] = setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr); obj["min"] = setting.valueRange.min; obj["max"] = setting.valueRange.max; obj["step"] = setting.valueRange.step; @@ -995,7 +995,11 @@ void CrossPointWebServer::handleGetSettings() const { case SettingType::STRING: obj["type"] = "string"; - obj["value"] = setting.stringPtr; + if (setting.stringGetter) { + obj["value"] = setting.stringGetter(); + } else { + obj["value"] = setting.stringPtr; + } obj["maxLength"] = setting.stringMaxLen; break; @@ -1043,7 +1047,11 @@ void CrossPointWebServer::handlePostSettings() { switch (setting.type) { case SettingType::TOGGLE: { const int value = doc[setting.key].as(); - SETTINGS.*(setting.valuePtr) = value ? 1 : 0; + if (setting.valueSetter) { + setting.valueSetter(value ? 1 : 0); + } else { + SETTINGS.*(setting.valuePtr) = value ? 1 : 0; + } updatedCount++; break; } @@ -1051,7 +1059,11 @@ void CrossPointWebServer::handlePostSettings() { case SettingType::ENUM: { const int value = doc[setting.key].as(); if (value >= 0 && value < static_cast(setting.enumValues.size())) { - SETTINGS.*(setting.valuePtr) = static_cast(value); + if (setting.valueSetter) { + setting.valueSetter(static_cast(value)); + } else { + SETTINGS.*(setting.valuePtr) = static_cast(value); + } updatedCount++; } break; @@ -1060,7 +1072,11 @@ void CrossPointWebServer::handlePostSettings() { case SettingType::VALUE: { const int value = doc[setting.key].as(); if (value >= setting.valueRange.min && value <= setting.valueRange.max) { - SETTINGS.*(setting.valuePtr) = static_cast(value); + if (setting.valueSetter) { + setting.valueSetter(static_cast(value)); + } else { + SETTINGS.*(setting.valuePtr) = static_cast(value); + } updatedCount++; } break; @@ -1069,8 +1085,12 @@ void CrossPointWebServer::handlePostSettings() { case SettingType::STRING: { const char* value = doc[setting.key].as(); if (value != nullptr) { - strncpy(setting.stringPtr, value, setting.stringMaxLen); - setting.stringPtr[setting.stringMaxLen] = '\0'; + if (setting.stringSetter) { + setting.stringSetter(value); + } else { + strncpy(setting.stringPtr, value, setting.stringMaxLen); + setting.stringPtr[setting.stringMaxLen] = '\0'; + } updatedCount++; } break; @@ -1083,6 +1103,7 @@ void CrossPointWebServer::handlePostSettings() { if (updatedCount > 0) { SETTINGS.saveToFile(); + KOREADER_STORE.saveToFile(); Serial.printf("[%lu] [WEB] Updated %d settings and saved to file\n", millis(), updatedCount); }