diff --git a/src/SettingsList.h b/src/SettingsList.h new file mode 100644 index 00000000..5b23d548 --- /dev/null +++ b/src/SettingsList.h @@ -0,0 +1,57 @@ +#pragma once +#include + +#include "CrossPointSettings.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. +inline std::vector getSettingsList() { + return { + // Display + SettingInfo::Enum("sleepScreen", "Sleep Screen", &CrossPointSettings::sleepScreen, + {"Dark", "Light", "Custom", "Cover", "None"}), + SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, + {"Fit", "Crop"}), + SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", + &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), + SettingInfo::Enum("statusBar", "Status Bar", &CrossPointSettings::statusBar, + {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}), + SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", &CrossPointSettings::hideBatteryPercentage, + {"Never", "In Reader", "Always"}), + SettingInfo::Enum("refreshFrequency", "Refresh Frequency", &CrossPointSettings::refreshFrequency, + {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), + + // Reader + SettingInfo::Enum("fontFamily", "Font Family", &CrossPointSettings::fontFamily, + {"Bookerly", "Noto Sans", "Open Dyslexic"}), + SettingInfo::Enum("fontSize", "Font Size", &CrossPointSettings::fontSize, + {"Small", "Medium", "Large", "X Large"}), + SettingInfo::Enum("lineSpacing", "Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), + SettingInfo::Value("screenMargin", "Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), + SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", &CrossPointSettings::paragraphAlignment, + {"Justify", "Left", "Center", "Right"}), + SettingInfo::Toggle("hyphenationEnabled", "Hyphenation", &CrossPointSettings::hyphenationEnabled), + SettingInfo::Enum("orientation", "Reading Orientation", &CrossPointSettings::orientation, + {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), + SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", + &CrossPointSettings::extraParagraphSpacing), + SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), + + // Controls + SettingInfo::Enum( + "frontButtonLayout", "Front Button Layout", &CrossPointSettings::frontButtonLayout, + {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}), + SettingInfo::Enum("sideButtonLayout", "Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, + {"Prev, Next", "Next, Prev"}), + SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), + SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", &CrossPointSettings::shortPwrBtn, + {"Ignore", "Sleep", "Page Turn"}), + + // System + SettingInfo::Enum("sleepTimeout", "Time to Sleep", &CrossPointSettings::sleepTimeout, + {"1 min", "5 min", "10 min", "15 min", "30 min"}), + SettingInfo::String("opdsServerUrl", "OPDS Server URL", SETTINGS.opdsServerUrl, + sizeof(SETTINGS.opdsServerUrl) - 1), + }; +} diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index a7d1f0ce..664b164b 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -11,12 +11,15 @@ class CrossPointSettings; -enum class SettingType { TOGGLE, ENUM, ACTION, VALUE }; +enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; struct SettingInfo { - const char* name; + const char* key; // JSON key for web API (nullptr for ACTION types) + const char* name; // Display name of the setting SettingType type; - uint8_t CrossPointSettings::* valuePtr; + uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM/VALUE) + char* stringPtr; // Pointer to char array (for STRING type) + size_t stringMaxLen; // Max length for STRING type std::vector enumValues; struct ValueRange { @@ -26,18 +29,26 @@ struct SettingInfo { }; ValueRange valueRange; - static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { - return {name, SettingType::TOGGLE, ptr}; + static SettingInfo Toggle(const char* key, const char* name, uint8_t CrossPointSettings::* ptr) { + return {key, name, SettingType::TOGGLE, ptr, nullptr, 0, {}, {}}; } - static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector values) { - return {name, SettingType::ENUM, ptr, std::move(values)}; + static SettingInfo Enum(const char* key, const char* name, uint8_t CrossPointSettings::* ptr, + std::vector values) { + return {key, name, SettingType::ENUM, ptr, nullptr, 0, std::move(values), {}}; } - static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } + static SettingInfo Action(const char* name) { + return {nullptr, name, SettingType::ACTION, nullptr, nullptr, 0, {}, {}}; + } - static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { - return {name, SettingType::VALUE, ptr, {}, valueRange}; + static SettingInfo Value(const char* key, const char* name, uint8_t CrossPointSettings::* ptr, + const ValueRange valueRange) { + return {key, name, SettingType::VALUE, ptr, nullptr, 0, {}, valueRange}; + } + + static SettingInfo String(const char* key, const char* name, char* ptr, size_t maxLen) { + return {key, name, SettingType::STRING, nullptr, ptr, maxLen, {}, {}}; } }; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..da146c83 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -14,43 +14,49 @@ namespace { constexpr int displaySettingsCount = 6; const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE - SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), - SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), - SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, - {"None", "Contrast", "Inverted"}), - SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, + SettingInfo::Enum("sleepScreen", "Sleep Screen", &CrossPointSettings::sleepScreen, + {"Dark", "Light", "Custom", "Cover", "None"}), + SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, + {"Fit", "Crop"}), + SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", + &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), + SettingInfo::Enum("statusBar", "Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}), - SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), - SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, + SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", &CrossPointSettings::hideBatteryPercentage, + {"Never", "In Reader", "Always"}), + SettingInfo::Enum("refreshFrequency", "Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; constexpr int readerSettingsCount = 9; const SettingInfo readerSettings[readerSettingsCount] = { - SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), - SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), - SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), - SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), - SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, + SettingInfo::Enum("fontFamily", "Font Family", &CrossPointSettings::fontFamily, + {"Bookerly", "Noto Sans", "Open Dyslexic"}), + SettingInfo::Enum("fontSize", "Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), + SettingInfo::Enum("lineSpacing", "Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), + SettingInfo::Value("screenMargin", "Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), + SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right"}), - SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), - SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, + SettingInfo::Toggle("hyphenationEnabled", "Hyphenation", &CrossPointSettings::hyphenationEnabled), + SettingInfo::Enum("orientation", "Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), - SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), - SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; + SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", + &CrossPointSettings::extraParagraphSpacing), + SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; constexpr int controlsSettingsCount = 4; const SettingInfo controlsSettings[controlsSettingsCount] = { SettingInfo::Enum( - "Front Button Layout", &CrossPointSettings::frontButtonLayout, + "frontButtonLayout", "Front Button Layout", &CrossPointSettings::frontButtonLayout, {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}), - SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, + SettingInfo::Enum("sideButtonLayout", "Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}), - SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), - SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; + SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), + SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", &CrossPointSettings::shortPwrBtn, + {"Ignore", "Sleep", "Page Turn"})}; constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { - SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, + SettingInfo::Enum("sleepTimeout", "Time to Sleep", &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}), SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"), SettingInfo::Action("Check for updates")}; diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index a135c9f0..389e6843 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -8,9 +8,12 @@ #include #include +#include +#include "SettingsList.h" #include "html/FilesPageHtml.generated.h" #include "html/HomePageHtml.generated.h" +#include "html/SettingsPageHtml.generated.h" #include "util/StringUtils.h" namespace { @@ -112,6 +115,11 @@ void CrossPointWebServer::begin() { // Delete file/folder endpoint server->on("/delete", HTTP_POST, [this] { handleDelete(); }); + // Settings endpoints + server->on("/settings", HTTP_GET, [this] { handleSettingsPage(); }); + server->on("/api/settings", HTTP_GET, [this] { handleGetSettings(); }); + server->on("/api/settings", HTTP_POST, [this] { handlePostSettings(); }); + server->onNotFound([this] { handleNotFound(); }); Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap()); @@ -936,3 +944,144 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* break; } } + +void CrossPointWebServer::handleSettingsPage() const { + server->send(200, "text/html", SettingsPageHtml); + Serial.printf("[%lu] [WEB] Served settings page\n", millis()); +} + +void CrossPointWebServer::handleGetSettings() const { + const auto settings = getSettingsList(); + + JsonDocument doc; + JsonArray settingsArray = doc["settings"].to(); + + for (const auto& setting : settings) { + // Skip ACTION types - they don't have web-editable values + if (setting.type == SettingType::ACTION) { + continue; + } + + JsonObject obj = settingsArray.add(); + obj["key"] = setting.key; + obj["name"] = setting.name; + + switch (setting.type) { + case SettingType::TOGGLE: + obj["type"] = "toggle"; + obj["value"] = SETTINGS.*(setting.valuePtr) ? 1 : 0; + break; + + case SettingType::ENUM: { + obj["type"] = "enum"; + obj["value"] = SETTINGS.*(setting.valuePtr); + JsonArray opts = obj["options"].to(); + for (const auto& opt : setting.enumValues) { + opts.add(opt); + } + break; + } + + case SettingType::VALUE: + obj["type"] = "value"; + obj["value"] = SETTINGS.*(setting.valuePtr); + obj["min"] = setting.valueRange.min; + obj["max"] = setting.valueRange.max; + obj["step"] = setting.valueRange.step; + break; + + case SettingType::STRING: + obj["type"] = "string"; + obj["value"] = setting.stringPtr; + obj["maxLength"] = setting.stringMaxLen; + break; + + case SettingType::ACTION: + break; + } + } + + String json; + serializeJson(doc, json); + server->send(200, "application/json", json); + Serial.printf("[%lu] [WEB] Served settings JSON\n", millis()); +} + +void CrossPointWebServer::handlePostSettings() { + if (!server->hasArg("plain")) { + server->send(400, "text/plain", "Missing request body"); + return; + } + + const String body = server->arg("plain"); + Serial.printf("[%lu] [WEB] Received settings update: %s\n", millis(), body.c_str()); + + JsonDocument doc; + const DeserializationError error = deserializeJson(doc, body); + + if (error) { + Serial.printf("[%lu] [WEB] JSON parse error: %s\n", millis(), error.c_str()); + server->send(400, "text/plain", "Invalid JSON"); + return; + } + + const auto settings = getSettingsList(); + int updatedCount = 0; + + for (const auto& setting : settings) { + if (setting.type == SettingType::ACTION || setting.key == nullptr) { + continue; + } + + if (doc[setting.key].isNull()) { + continue; + } + + switch (setting.type) { + case SettingType::TOGGLE: { + const int value = doc[setting.key].as(); + SETTINGS.*(setting.valuePtr) = value ? 1 : 0; + updatedCount++; + break; + } + + 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); + updatedCount++; + } + break; + } + + 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); + updatedCount++; + } + break; + } + + case SettingType::STRING: { + const char* value = doc[setting.key].as(); + if (value != nullptr) { + strncpy(setting.stringPtr, value, setting.stringMaxLen); + setting.stringPtr[setting.stringMaxLen] = '\0'; + updatedCount++; + } + break; + } + + case SettingType::ACTION: + break; + } + } + + if (updatedCount > 0) { + SETTINGS.saveToFile(); + Serial.printf("[%lu] [WEB] Updated %d settings and saved to file\n", millis(), updatedCount); + } + + server->send(200, "text/plain", "Settings updated: " + String(updatedCount)); +} diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index 36030292..d2306bad 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -78,4 +78,9 @@ class CrossPointWebServer { void handleUploadPost() const; void handleCreateFolder() const; void handleDelete() const; + + // Settings handlers + void handleSettingsPage() const; + void handleGetSettings() const; + void handlePostSettings(); }; diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index bfdbe3cc..9bcc030c 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -575,6 +575,7 @@