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. // Returns the flat list of all settings for the web API.
// This is used by CrossPointWebServer to expose settings over HTTP. // This is used by CrossPointWebServer to expose settings over HTTP.
// Categories match the device UI grouping for consistency.
inline std::vector<SettingInfo> getSettingsList() { inline std::vector<SettingInfo> getSettingsList() {
return { return {
// Display // Display
SettingInfo::Enum("sleepScreen", "Sleep Screen", &CrossPointSettings::sleepScreen, SettingInfo::Enum("sleepScreen", "Sleep Screen", "Display", &CrossPointSettings::sleepScreen,
{"Dark", "Light", "Custom", "Cover", "None"}), {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", "Display",
{"Fit", "Crop"}), &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", "Display",
&CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), &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"}), {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", &CrossPointSettings::hideBatteryPercentage, SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", "Display",
{"Never", "In Reader", "Always"}), &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("refreshFrequency", "Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum("refreshFrequency", "Refresh Frequency", "Display", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
// Reader // Reader
SettingInfo::Enum("fontFamily", "Font Family", &CrossPointSettings::fontFamily, SettingInfo::Enum("fontFamily", "Font Family", "Reader", &CrossPointSettings::fontFamily,
{"Bookerly", "Noto Sans", "Open Dyslexic"}), {"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"}), {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("lineSpacing", "Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), SettingInfo::Enum("lineSpacing", "Line Spacing", "Reader", &CrossPointSettings::lineSpacing,
SettingInfo::Value("screenMargin", "Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), {"Tight", "Normal", "Wide"}),
SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", &CrossPointSettings::paragraphAlignment, SettingInfo::Value("screenMargin", "Screen Margin", "Reader", &CrossPointSettings::screenMargin, {5, 40, 5}),
{"Justify", "Left", "Center", "Right"}), SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", "Reader",
SettingInfo::Toggle("hyphenationEnabled", "Hyphenation", &CrossPointSettings::hyphenationEnabled), &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right"}),
SettingInfo::Enum("orientation", "Reading Orientation", &CrossPointSettings::orientation, SettingInfo::Toggle("hyphenationEnabled", "Hyphenation", "Reader", &CrossPointSettings::hyphenationEnabled),
SettingInfo::Enum("orientation", "Reading Orientation", "Reader", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", "Reader",
&CrossPointSettings::extraParagraphSpacing), &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", "Reader", &CrossPointSettings::textAntiAliasing),
// Controls // Controls
SettingInfo::Enum( 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"}), {"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, SettingInfo::Enum("sideButtonLayout", "Side Button Layout (reader)", "Controls",
{"Prev, Next", "Next, Prev"}), &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", "Controls",
SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", &CrossPointSettings::shortPwrBtn, &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", "Controls", &CrossPointSettings::shortPwrBtn,
{"Ignore", "Sleep", "Page Turn"}), {"Ignore", "Sleep", "Page Turn"}),
// System // 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"}), {"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), 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 { struct SettingInfo {
const char* key; // JSON key for web API (nullptr for ACTION types) const char* key; // JSON key for web API (nullptr for ACTION types)
const char* name; // Display name of the setting const char* name; // Display name of the setting
const char* category; // Category for grouping in web UI (nullptr = uncategorized)
SettingType type; SettingType type;
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM/VALUE) uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM/VALUE)
char* stringPtr; // Pointer to char array (for STRING type) char* stringPtr; // Pointer to char array (for STRING type)
@ -29,26 +30,27 @@ struct SettingInfo {
}; };
ValueRange valueRange; ValueRange valueRange;
static SettingInfo Toggle(const char* key, const char* name, uint8_t CrossPointSettings::* ptr) { static SettingInfo Toggle(const char* key, const char* name, const char* category,
return {key, name, SettingType::TOGGLE, ptr, nullptr, 0, {}, {}}; 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, static SettingInfo Enum(const char* key, const char* name, const char* category,
std::vector<std::string> values) { uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) {
return {key, name, SettingType::ENUM, ptr, nullptr, 0, std::move(values), {}}; return {key, name, category, SettingType::ENUM, ptr, nullptr, 0, std::move(values), {}};
} }
static SettingInfo Action(const char* name) { 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, static SettingInfo Value(const char* key, const char* name, const char* category,
const ValueRange valueRange) { uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
return {key, name, SettingType::VALUE, ptr, nullptr, 0, {}, 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) { static SettingInfo String(const char* key, const char* name, const char* category, char* ptr, size_t maxLen) {
return {key, name, SettingType::STRING, nullptr, ptr, maxLen, {}, {}}; return {key, name, category, SettingType::STRING, nullptr, ptr, maxLen, {}, {}};
} }
}; };

View File

@ -14,49 +14,52 @@ namespace {
constexpr int displaySettingsCount = 6; constexpr int displaySettingsCount = 6;
const SettingInfo displaySettings[displaySettingsCount] = { const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE // 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"}), {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, SettingInfo::Enum("sleepScreenCoverMode", "Sleep Screen Cover Mode", "Display",
{"Fit", "Crop"}), &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", SettingInfo::Enum("sleepScreenCoverFilter", "Sleep Screen Cover Filter", "Display",
&CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), &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"}), {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", &CrossPointSettings::hideBatteryPercentage, SettingInfo::Enum("hideBatteryPercentage", "Hide Battery %", "Display",
{"Never", "In Reader", "Always"}), &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("refreshFrequency", "Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum("refreshFrequency", "Refresh Frequency", "Display", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
constexpr int readerSettingsCount = 9; constexpr int readerSettingsCount = 9;
const SettingInfo readerSettings[readerSettingsCount] = { const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("fontFamily", "Font Family", &CrossPointSettings::fontFamily, SettingInfo::Enum("fontFamily", "Font Family", "Reader", &CrossPointSettings::fontFamily,
{"Bookerly", "Noto Sans", "Open Dyslexic"}), {"Bookerly", "Noto Sans", "Open Dyslexic"}),
SettingInfo::Enum("fontSize", "Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), SettingInfo::Enum("fontSize", "Font Size", "Reader", &CrossPointSettings::fontSize,
SettingInfo::Enum("lineSpacing", "Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Value("screenMargin", "Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Enum("lineSpacing", "Line Spacing", "Reader", &CrossPointSettings::lineSpacing,
SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", &CrossPointSettings::paragraphAlignment, {"Tight", "Normal", "Wide"}),
{"Justify", "Left", "Center", "Right"}), SettingInfo::Value("screenMargin", "Screen Margin", "Reader", &CrossPointSettings::screenMargin, {5, 40, 5}),
SettingInfo::Toggle("hyphenationEnabled", "Hyphenation", &CrossPointSettings::hyphenationEnabled), SettingInfo::Enum("paragraphAlignment", "Paragraph Alignment", "Reader",
SettingInfo::Enum("orientation", "Reading Orientation", &CrossPointSettings::orientation, &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"}), {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", SettingInfo::Toggle("extraParagraphSpacing", "Extra Paragraph Spacing", "Reader",
&CrossPointSettings::extraParagraphSpacing), &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", "Reader", &CrossPointSettings::textAntiAliasing)};
constexpr int controlsSettingsCount = 4; constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = { const SettingInfo controlsSettings[controlsSettingsCount] = {
SettingInfo::Enum( 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"}), {"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, SettingInfo::Enum("sideButtonLayout", "Side Button Layout (reader)", "Controls",
{"Prev, Next", "Next, Prev"}), &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), SettingInfo::Toggle("longPressChapterSkip", "Long-press Chapter Skip", "Controls",
SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", &CrossPointSettings::shortPwrBtn, &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("shortPwrBtn", "Short Power Button Click", "Controls", &CrossPointSettings::shortPwrBtn,
{"Ignore", "Sleep", "Page Turn"})}; {"Ignore", "Sleep", "Page Turn"})};
constexpr int systemSettingsCount = 5; constexpr int systemSettingsCount = 5;
const SettingInfo systemSettings[systemSettingsCount] = { 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"}), {"1 min", "5 min", "10 min", "15 min", "30 min"}),
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"), SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"),
SettingInfo::Action("Check for updates")}; SettingInfo::Action("Check for updates")};

View File

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

View File

@ -168,13 +168,11 @@
<a href="/settings">Settings</a> <a href="/settings">Settings</a>
</div> </div>
<div class="card"> <div id="settings-container">
<div id="settings-container"> <div class="card"><div class="loading">Loading settings...</div></div>
<div class="loading">Loading settings...</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>
<button id="save-btn" class="save-btn" style="display: none;">Save Settings</button>
<div id="status-message" class="status-message"></div>
<div class="card"> <div class="card">
<p style="text-align: center; color: #95a5a6; margin: 0"> <p style="text-align: center; color: #95a5a6; margin: 0">
@ -205,67 +203,86 @@
const container = document.getElementById('settings-container'); const container = document.getElementById('settings-container');
container.innerHTML = ''; container.innerHTML = '';
// Group settings by category
const categories = new Map();
settings.forEach(setting => { settings.forEach(setting => {
originalSettings[setting.key] = setting.value; const cat = setting.category || 'Other';
currentSettings[setting.key] = setting.value; if (!categories.has(cat)) categories.set(cat, []);
categories.get(cat).push(setting);
});
const row = document.createElement('div'); categories.forEach((catSettings, categoryName) => {
row.className = 'setting-row'; const card = document.createElement('div');
card.className = 'card';
const label = document.createElement('span'); const heading = document.createElement('h2');
label.className = 'setting-label'; heading.textContent = categoryName;
label.textContent = setting.name; card.appendChild(heading);
const control = document.createElement('div'); catSettings.forEach(setting => {
control.className = 'setting-control'; originalSettings[setting.key] = setting.value;
currentSettings[setting.key] = setting.value;
switch (setting.type) { const row = document.createElement('div');
case 'toggle': row.className = 'setting-row';
control.innerHTML = `
<label class="toggle-switch">
<input type="checkbox" id="${setting.key}" ${setting.value ? 'checked' : ''}>
<span class="toggle-slider"></span>
</label>
`;
break;
case 'enum': const label = document.createElement('span');
let options = setting.options.map((opt, idx) => label.className = 'setting-label';
`<option value="${idx}" ${setting.value === idx ? 'selected' : ''}>${opt}</option>` label.textContent = setting.name;
).join('');
control.innerHTML = `<select id="${setting.key}">${options}</select>`;
break;
case 'value': const control = document.createElement('div');
control.innerHTML = ` control.className = 'setting-control';
<input type="number" id="${setting.key}"
value="${setting.value}"
min="${setting.min}"
max="${setting.max}"
step="${setting.step}">
`;
break;
case 'string': switch (setting.type) {
control.innerHTML = ` case 'toggle':
<input type="text" id="${setting.key}" control.innerHTML = `
value="${setting.value || ''}" <label class="toggle-switch">
maxlength="${setting.maxLength}" <input type="checkbox" id="${setting.key}" ${setting.value ? 'checked' : ''}>
placeholder="Enter URL..."> <span class="toggle-slider"></span>
`; </label>
break; `;
} break;
row.appendChild(label); case 'enum':
row.appendChild(control); let options = setting.options.map((opt, idx) =>
container.appendChild(row); `<option value="${idx}" ${setting.value === idx ? 'selected' : ''}>${opt}</option>`
).join('');
control.innerHTML = `<select id="${setting.key}">${options}</select>`;
break;
// Add change listener case 'value':
const input = control.querySelector('input, select'); control.innerHTML = `
if (input) { <input type="number" id="${setting.key}"
input.addEventListener('change', () => updateCurrentValue(setting)); value="${setting.value}"
input.addEventListener('input', () => updateCurrentValue(setting)); min="${setting.min}"
} max="${setting.max}"
step="${setting.step}">
`;
break;
case 'string':
control.innerHTML = `
<input type="text" id="${setting.key}"
value="${setting.value || ''}"
maxlength="${setting.maxLength}"
placeholder="Enter value...">
`;
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'; document.getElementById('save-btn').style.display = 'block';