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.
This commit is contained in:
Jesse Vincent 2026-02-01 17:37:19 -08:00
parent 84fa55789c
commit 434fc9fb7d
5 changed files with 151 additions and 117 deletions

View File

@ -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<SettingInfo> 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),
};
}

View File

@ -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<std::string> 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<std::string> 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, {}, {}};
}
};

View File

@ -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")};

View File

@ -965,6 +965,9 @@ void CrossPointWebServer::handleGetSettings() const {
JsonObject obj = settingsArray.add<JsonObject>();
obj["key"] = setting.key;
obj["name"] = setting.name;
if (setting.category) {
obj["category"] = setting.category;
}
switch (setting.type) {
case SettingType::TOGGLE:

View File

@ -168,13 +168,11 @@
<a href="/settings">Settings</a>
</div>
<div class="card">
<div id="settings-container">
<div class="loading">Loading settings...</div>
<div class="card"><div class="loading">Loading settings...</div></div>
</div>
<button id="save-btn" class="save-btn" style="display: none;">Save Settings</button>
<div id="status-message" class="status-message"></div>
</div>
<div class="card">
<p style="text-align: center; color: #95a5a6; margin: 0">
@ -205,7 +203,23 @@
const container = document.getElementById('settings-container');
container.innerHTML = '';
// Group settings by category
const categories = new Map();
settings.forEach(setting => {
const cat = setting.category || 'Other';
if (!categories.has(cat)) categories.set(cat, []);
categories.get(cat).push(setting);
});
categories.forEach((catSettings, categoryName) => {
const card = document.createElement('div');
card.className = 'card';
const heading = document.createElement('h2');
heading.textContent = categoryName;
card.appendChild(heading);
catSettings.forEach(setting => {
originalSettings[setting.key] = setting.value;
currentSettings[setting.key] = setting.value;
@ -251,14 +265,14 @@
<input type="text" id="${setting.key}"
value="${setting.value || ''}"
maxlength="${setting.maxLength}"
placeholder="Enter URL...">
placeholder="Enter value...">
`;
break;
}
row.appendChild(label);
row.appendChild(control);
container.appendChild(row);
card.appendChild(row);
// Add change listener
const input = control.querySelector('input, select');
@ -268,6 +282,9 @@
}
});
container.appendChild(card);
});
document.getElementById('save-btn').style.display = 'block';
}