From 434fc9fb7d1a700804f2699b94ffad40dcd030e0 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sun, 1 Feb 2026 17:37:19 -0800 Subject: [PATCH] feat: Group web settings by category and add missing OPDS credentials Address code review: settings are now grouped by category (Display, Reader, Controls, System, Calibre) in both the JSON API and the web UI. Adds category field to SettingInfo struct. Also adds opdsUsername and opdsPassword to the Calibre section, which were missing from the web settings. --- src/SettingsList.h | 59 ++++---- .../settings/CategorySettingsActivity.h | 24 ++-- src/activities/settings/SettingsActivity.cpp | 51 +++---- src/network/CrossPointWebServer.cpp | 3 + src/network/html/SettingsPage.html | 131 ++++++++++-------- 5 files changed, 151 insertions(+), 117 deletions(-) diff --git a/src/SettingsList.h b/src/SettingsList.h index 5b23d548..ba52e571 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -6,52 +6,61 @@ // 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. inline std::vector getSettingsList() { return { // Display - SettingInfo::Enum("sleepScreen", "Sleep Screen", &CrossPointSettings::sleepScreen, + SettingInfo::Enum("sleepScreen", "Sleep Screen", "Display", &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", + SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", "Display", + &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), + SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", "Display", &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), - SettingInfo::Enum("statusBar", "Status Bar", &CrossPointSettings::statusBar, + SettingInfo::Enum("statusBar", "Status Bar", "Display", &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, + SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", "Display", + &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), + SettingInfo::Enum("refreshFrequency", "Refresh Frequency", "Display", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), // Reader - SettingInfo::Enum("fontFamily", "Font Family", &CrossPointSettings::fontFamily, + SettingInfo::Enum("fontFamily", "Font Family", "Reader", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), - SettingInfo::Enum("fontSize", "Font Size", &CrossPointSettings::fontSize, + SettingInfo::Enum("fontSize", "Font Size", "Reader", &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, + SettingInfo::Enum("lineSpacing", "Line Spacing", "Reader", &CrossPointSettings::lineSpacing, + {"Tight", "Normal", "Wide"}), + SettingInfo::Value("screenMargin", "Screen Margin", "Reader", &CrossPointSettings::screenMargin, {5, 40, 5}), + SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", "Reader", + &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right"}), + SettingInfo::Toggle("hyphenationEnabled", "Hyphenation", "Reader", &CrossPointSettings::hyphenationEnabled), + SettingInfo::Enum("orientation", "Reading Orientation", "Reader", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), - SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", + SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", "Reader", &CrossPointSettings::extraParagraphSpacing), - SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), + SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", "Reader", &CrossPointSettings::textAntiAliasing), // Controls SettingInfo::Enum( - "frontButtonLayout", "Front Button Layout", &CrossPointSettings::frontButtonLayout, + "frontButtonLayout", "Front Button Layout", "Controls", &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, + SettingInfo::Enum("sideButtonLayout", "Side Button Layout (reader)", "Controls", + &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}), + SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", "Controls", + &CrossPointSettings::longPressChapterSkip), + SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", "Controls", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"}), // System - SettingInfo::Enum("sleepTimeout", "Time to Sleep", &CrossPointSettings::sleepTimeout, + SettingInfo::Enum("sleepTimeout", "Time to Sleep", "System", &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}), - SettingInfo::String("opdsServerUrl", "OPDS Server URL", SETTINGS.opdsServerUrl, + + // Calibre / OPDS + SettingInfo::String("opdsServerUrl", "OPDS Server URL", "Calibre", SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl) - 1), + SettingInfo::String("opdsUsername", "OPDS Username", "Calibre", SETTINGS.opdsUsername, + sizeof(SETTINGS.opdsUsername) - 1), + SettingInfo::String("opdsPassword", "OPDS Password", "Calibre", SETTINGS.opdsPassword, + sizeof(SETTINGS.opdsPassword) - 1), }; } diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index 664b164b..c706c890 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -16,6 +16,7 @@ enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; struct SettingInfo { const char* key; // JSON key for web API (nullptr for ACTION types) const char* name; // Display name of the setting + const char* category; // Category for grouping in web UI (nullptr = uncategorized) SettingType type; uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM/VALUE) char* stringPtr; // Pointer to char array (for STRING type) @@ -29,26 +30,27 @@ struct SettingInfo { }; ValueRange valueRange; - static SettingInfo Toggle(const char* key, const char* name, uint8_t CrossPointSettings::* ptr) { - return {key, name, SettingType::TOGGLE, ptr, nullptr, 0, {}, {}}; + 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, {}, {}}; } - 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 Enum(const char* key, const char* name, const char* category, + uint8_t CrossPointSettings::* ptr, std::vector values) { + return {key, name, category, SettingType::ENUM, ptr, nullptr, 0, std::move(values), {}}; } static SettingInfo Action(const char* name) { - return {nullptr, name, SettingType::ACTION, nullptr, nullptr, 0, {}, {}}; + return {nullptr, name, nullptr, SettingType::ACTION, nullptr, nullptr, 0, {}, {}}; } - 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 Value(const char* key, const char* name, const char* category, + uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { + return {key, name, category, 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, {}, {}}; + 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, {}, {}}; } }; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index da146c83..8ea68a8a 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -14,49 +14,52 @@ namespace { constexpr int displaySettingsCount = 6; const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE - SettingInfo::Enum("sleepScreen", "Sleep Screen", &CrossPointSettings::sleepScreen, + SettingInfo::Enum("sleepScreen", "Sleep Screen", "Display", &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", + SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", "Display", + &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), + SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", "Display", &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), - SettingInfo::Enum("statusBar", "Status Bar", &CrossPointSettings::statusBar, + SettingInfo::Enum("statusBar", "Status Bar", "Display", &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, + SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", "Display", + &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), + SettingInfo::Enum("refreshFrequency", "Refresh Frequency", "Display", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; constexpr int readerSettingsCount = 9; const SettingInfo readerSettings[readerSettingsCount] = { - SettingInfo::Enum("fontFamily", "Font Family", &CrossPointSettings::fontFamily, + SettingInfo::Enum("fontFamily", "Font Family", "Reader", &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, + SettingInfo::Enum("fontSize", "Font Size", "Reader", &CrossPointSettings::fontSize, + {"Small", "Medium", "Large", "X Large"}), + SettingInfo::Enum("lineSpacing", "Line Spacing", "Reader", &CrossPointSettings::lineSpacing, + {"Tight", "Normal", "Wide"}), + SettingInfo::Value("screenMargin", "Screen Margin", "Reader", &CrossPointSettings::screenMargin, {5, 40, 5}), + SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", "Reader", + &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right"}), + SettingInfo::Toggle("hyphenationEnabled", "Hyphenation", "Reader", &CrossPointSettings::hyphenationEnabled), + SettingInfo::Enum("orientation", "Reading Orientation", "Reader", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), - SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", + SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", "Reader", &CrossPointSettings::extraParagraphSpacing), - SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; + SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", "Reader", &CrossPointSettings::textAntiAliasing)}; constexpr int controlsSettingsCount = 4; const SettingInfo controlsSettings[controlsSettingsCount] = { SettingInfo::Enum( - "frontButtonLayout", "Front Button Layout", &CrossPointSettings::frontButtonLayout, + "frontButtonLayout", "Front Button Layout", "Controls", &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, + SettingInfo::Enum("sideButtonLayout", "Side Button Layout (reader)", "Controls", + &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}), + SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", "Controls", + &CrossPointSettings::longPressChapterSkip), + SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", "Controls", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { - SettingInfo::Enum("sleepTimeout", "Time to Sleep", &CrossPointSettings::sleepTimeout, + SettingInfo::Enum("sleepTimeout", "Time to Sleep", "System", &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 389e6843..bb0609d6 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -965,6 +965,9 @@ void CrossPointWebServer::handleGetSettings() const { JsonObject obj = settingsArray.add(); obj["key"] = setting.key; obj["name"] = setting.name; + if (setting.category) { + obj["category"] = setting.category; + } switch (setting.type) { case SettingType::TOGGLE: diff --git a/src/network/html/SettingsPage.html b/src/network/html/SettingsPage.html index 76934309..955d25ce 100644 --- a/src/network/html/SettingsPage.html +++ b/src/network/html/SettingsPage.html @@ -168,13 +168,11 @@ Settings -
-
-
Loading settings...
-
- -
+
+
Loading settings...
+ +

@@ -205,67 +203,86 @@ const container = document.getElementById('settings-container'); container.innerHTML = ''; + // Group settings by category + const categories = new Map(); settings.forEach(setting => { - originalSettings[setting.key] = setting.value; - currentSettings[setting.key] = setting.value; + const cat = setting.category || 'Other'; + if (!categories.has(cat)) categories.set(cat, []); + categories.get(cat).push(setting); + }); - const row = document.createElement('div'); - row.className = 'setting-row'; + categories.forEach((catSettings, categoryName) => { + const card = document.createElement('div'); + card.className = 'card'; - const label = document.createElement('span'); - label.className = 'setting-label'; - label.textContent = setting.name; + const heading = document.createElement('h2'); + heading.textContent = categoryName; + card.appendChild(heading); - const control = document.createElement('div'); - control.className = 'setting-control'; + catSettings.forEach(setting => { + originalSettings[setting.key] = setting.value; + currentSettings[setting.key] = setting.value; - switch (setting.type) { - case 'toggle': - control.innerHTML = ` - - `; - break; + const row = document.createElement('div'); + row.className = 'setting-row'; - case 'enum': - let options = setting.options.map((opt, idx) => - `` - ).join(''); - control.innerHTML = ``; - break; + const label = document.createElement('span'); + label.className = 'setting-label'; + label.textContent = setting.name; - case 'value': - control.innerHTML = ` - - `; - break; + const control = document.createElement('div'); + control.className = 'setting-control'; - case 'string': - control.innerHTML = ` - - `; - break; - } + switch (setting.type) { + case 'toggle': + control.innerHTML = ` + + `; + break; - row.appendChild(label); - row.appendChild(control); - container.appendChild(row); + case 'enum': + let options = setting.options.map((opt, idx) => + `` + ).join(''); + control.innerHTML = ``; + break; - // Add change listener - const input = control.querySelector('input, select'); - if (input) { - input.addEventListener('change', () => updateCurrentValue(setting)); - input.addEventListener('input', () => updateCurrentValue(setting)); - } + case 'value': + control.innerHTML = ` + + `; + break; + + case 'string': + control.innerHTML = ` + + `; + break; + } + + row.appendChild(label); + row.appendChild(control); + card.appendChild(row); + + // Add change listener + const input = control.querySelector('input, select'); + if (input) { + input.addEventListener('change', () => updateCurrentValue(setting)); + input.addEventListener('input', () => updateCurrentValue(setting)); + } + }); + + container.appendChild(card); }); document.getElementById('save-btn').style.display = 'block';