mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Merge 94b869f6d5 into d403044f76
This commit is contained in:
commit
ccbf42fe32
84
src/SettingsList.h
Normal file
84
src/SettingsList.h
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
#include "KOReaderCredentialStore.h"
|
||||||
|
#include "activities/settings/CategorySettingsActivity.h"
|
||||||
|
|
||||||
|
// 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<SettingInfo> getSettingsList() {
|
||||||
|
return {
|
||||||
|
// Display
|
||||||
|
SettingInfo::Enum("sleepScreen", "Sleep Screen", "Display", &CrossPointSettings::sleepScreen,
|
||||||
|
{"Dark", "Light", "Custom", "Cover", "None"}),
|
||||||
|
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", "Display", &CrossPointSettings::statusBar,
|
||||||
|
{"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
|
||||||
|
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", "Reader", &CrossPointSettings::fontFamily,
|
||||||
|
{"Bookerly", "Noto Sans", "Open Dyslexic"}),
|
||||||
|
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", "Reader",
|
||||||
|
&CrossPointSettings::extraParagraphSpacing),
|
||||||
|
SettingInfo::Toggle("textAntiAliasing", "Text Anti-Aliasing", "Reader", &CrossPointSettings::textAntiAliasing),
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
SettingInfo::Enum(
|
||||||
|
"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)", "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", "System", &CrossPointSettings::sleepTimeout,
|
||||||
|
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
||||||
|
|
||||||
|
// 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<uint8_t>(KOREADER_STORE.getMatchMethod()); },
|
||||||
|
[](uint8_t v) { KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(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", "Username", "OPDS Browser", SETTINGS.opdsUsername,
|
||||||
|
sizeof(SETTINGS.opdsUsername) - 1),
|
||||||
|
SettingInfo::String("opdsPassword", "Password", "OPDS Browser", SETTINGS.opdsPassword,
|
||||||
|
sizeof(SETTINGS.opdsPassword) - 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -11,12 +11,16 @@
|
|||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
|
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
|
||||||
|
|
||||||
struct SettingInfo {
|
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
|
||||||
|
const char* category; // Category for grouping in web UI (nullptr = uncategorized)
|
||||||
SettingType type;
|
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<std::string> enumValues;
|
std::vector<std::string> enumValues;
|
||||||
|
|
||||||
struct ValueRange {
|
struct ValueRange {
|
||||||
@ -26,18 +30,61 @@ struct SettingInfo {
|
|||||||
};
|
};
|
||||||
ValueRange valueRange;
|
ValueRange valueRange;
|
||||||
|
|
||||||
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) {
|
// Dynamic accessors for settings not in CrossPointSettings
|
||||||
return {name, SettingType::TOGGLE, ptr};
|
std::function<uint8_t()> valueGetter;
|
||||||
|
std::function<void(uint8_t)> valueSetter;
|
||||||
|
std::function<std::string()> stringGetter;
|
||||||
|
std::function<void(const std::string&)> 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, {}, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) {
|
static SettingInfo Enum(const char* key, const char* name, const char* category, uint8_t CrossPointSettings::* ptr,
|
||||||
return {name, SettingType::ENUM, ptr, std::move(values)};
|
std::vector<std::string> values) {
|
||||||
|
return {key, name, category, 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, nullptr, SettingType::ACTION, nullptr, nullptr, 0, {}, {}};
|
||||||
|
}
|
||||||
|
|
||||||
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
|
static SettingInfo Value(const char* key, const char* name, const char* category, uint8_t CrossPointSettings::* ptr,
|
||||||
return {name, SettingType::VALUE, ptr, {}, valueRange};
|
const ValueRange valueRange) {
|
||||||
|
return {key, name, category, SettingType::VALUE, ptr, nullptr, 0, {}, valueRange};
|
||||||
|
}
|
||||||
|
|
||||||
|
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<uint8_t()> getter, std::function<void(uint8_t)> setter,
|
||||||
|
std::vector<std::string> 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<std::string()> getter, std::function<void(const std::string&)> 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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,59 +3,15 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
#include "CategorySettingsActivity.h"
|
#include <cstring>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "SettingsList.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
||||||
|
|
||||||
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,
|
|
||||||
{"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,
|
|
||||||
{"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,
|
|
||||||
{"Justify", "Left", "Center", "Right"}),
|
|
||||||
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled),
|
|
||||||
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
|
|
||||||
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
|
|
||||||
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
|
|
||||||
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
|
|
||||||
|
|
||||||
constexpr int controlsSettingsCount = 4;
|
|
||||||
const SettingInfo controlsSettings[controlsSettingsCount] = {
|
|
||||||
SettingInfo::Enum(
|
|
||||||
"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,
|
|
||||||
{"Prev, Next", "Next, Prev"}),
|
|
||||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
|
||||||
SettingInfo::Enum("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,
|
|
||||||
{"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")};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void SettingsActivity::taskTrampoline(void* param) {
|
void SettingsActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<SettingsActivity*>(param);
|
auto* self = static_cast<SettingsActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
@ -68,6 +24,23 @@ void SettingsActivity::onEnter() {
|
|||||||
// Reset selection to first category
|
// Reset selection to first category
|
||||||
selectedCategoryIndex = 0;
|
selectedCategoryIndex = 0;
|
||||||
|
|
||||||
|
// Build per-category settings from the single source of truth
|
||||||
|
for (auto& cat : categorySettings) cat.clear();
|
||||||
|
for (const auto& setting : getSettingsList()) {
|
||||||
|
if (!setting.category) continue;
|
||||||
|
for (int i = 0; i < categoryCount; i++) {
|
||||||
|
if (strcmp(setting.category, categoryNames[i]) == 0) {
|
||||||
|
categorySettings[i].push_back(setting);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Append device-only Action items to System (index 3)
|
||||||
|
categorySettings[3].push_back(SettingInfo::Action("KOReader Sync"));
|
||||||
|
categorySettings[3].push_back(SettingInfo::Action("OPDS Browser"));
|
||||||
|
categorySettings[3].push_back(SettingInfo::Action("Clear Cache"));
|
||||||
|
categorySettings[3].push_back(SettingInfo::Action("Check for updates"));
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
@ -132,30 +105,9 @@ void SettingsActivity::enterCategory(int categoryIndex) {
|
|||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
const SettingInfo* settingsList = nullptr;
|
const auto& settings = categorySettings[categoryIndex];
|
||||||
int settingsCount = 0;
|
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settings.data(),
|
||||||
|
static_cast<int>(settings.size()), [this] {
|
||||||
switch (categoryIndex) {
|
|
||||||
case 0: // Display
|
|
||||||
settingsList = displaySettings;
|
|
||||||
settingsCount = displaySettingsCount;
|
|
||||||
break;
|
|
||||||
case 1: // Reader
|
|
||||||
settingsList = readerSettings;
|
|
||||||
settingsCount = readerSettingsCount;
|
|
||||||
break;
|
|
||||||
case 2: // Controls
|
|
||||||
settingsList = controlsSettings;
|
|
||||||
settingsCount = controlsSettingsCount;
|
|
||||||
break;
|
|
||||||
case 3: // System
|
|
||||||
settingsList = systemSettings;
|
|
||||||
settingsCount = systemSettingsCount;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList,
|
|
||||||
settingsCount, [this] {
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -7,20 +7,19 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CategorySettingsActivity.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
|
||||||
struct SettingInfo;
|
|
||||||
|
|
||||||
class SettingsActivity final : public ActivityWithSubactivity {
|
class SettingsActivity final : public ActivityWithSubactivity {
|
||||||
|
static constexpr int categoryCount = 4;
|
||||||
|
static const char* categoryNames[categoryCount];
|
||||||
|
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedCategoryIndex = 0; // Currently selected category
|
int selectedCategoryIndex = 0; // Currently selected category
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
std::vector<SettingInfo> categorySettings[categoryCount];
|
||||||
static constexpr int categoryCount = 4;
|
|
||||||
static const char* categoryNames[categoryCount];
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
|||||||
@ -8,9 +8,12 @@
|
|||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "SettingsList.h"
|
||||||
#include "html/FilesPageHtml.generated.h"
|
#include "html/FilesPageHtml.generated.h"
|
||||||
#include "html/HomePageHtml.generated.h"
|
#include "html/HomePageHtml.generated.h"
|
||||||
|
#include "html/SettingsPageHtml.generated.h"
|
||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -112,6 +115,11 @@ void CrossPointWebServer::begin() {
|
|||||||
// Delete file/folder endpoint
|
// Delete file/folder endpoint
|
||||||
server->on("/delete", HTTP_POST, [this] { handleDelete(); });
|
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(); });
|
server->onNotFound([this] { handleNotFound(); });
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
@ -936,3 +944,168 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
|
|||||||
break;
|
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<JsonArray>();
|
||||||
|
|
||||||
|
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<JsonObject>();
|
||||||
|
obj["key"] = setting.key;
|
||||||
|
obj["name"] = setting.name;
|
||||||
|
if (setting.category) {
|
||||||
|
obj["category"] = setting.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (setting.type) {
|
||||||
|
case SettingType::TOGGLE:
|
||||||
|
obj["type"] = "toggle";
|
||||||
|
obj["value"] = (setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr)) ? 1 : 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SettingType::ENUM: {
|
||||||
|
obj["type"] = "enum";
|
||||||
|
obj["value"] = setting.valueGetter ? setting.valueGetter() : SETTINGS.*(setting.valuePtr);
|
||||||
|
JsonArray opts = obj["options"].to<JsonArray>();
|
||||||
|
for (const auto& opt : setting.enumValues) {
|
||||||
|
opts.add(opt);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SettingType::VALUE:
|
||||||
|
obj["type"] = "value";
|
||||||
|
obj["value"] = setting.valueGetter ? setting.valueGetter() : 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";
|
||||||
|
if (setting.stringGetter) {
|
||||||
|
obj["value"] = setting.stringGetter();
|
||||||
|
} else {
|
||||||
|
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<int>();
|
||||||
|
if (setting.valueSetter) {
|
||||||
|
setting.valueSetter(value ? 1 : 0);
|
||||||
|
} else {
|
||||||
|
SETTINGS.*(setting.valuePtr) = value ? 1 : 0;
|
||||||
|
}
|
||||||
|
updatedCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SettingType::ENUM: {
|
||||||
|
const int value = doc[setting.key].as<int>();
|
||||||
|
if (value >= 0 && value < static_cast<int>(setting.enumValues.size())) {
|
||||||
|
if (setting.valueSetter) {
|
||||||
|
setting.valueSetter(static_cast<uint8_t>(value));
|
||||||
|
} else {
|
||||||
|
SETTINGS.*(setting.valuePtr) = static_cast<uint8_t>(value);
|
||||||
|
}
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SettingType::VALUE: {
|
||||||
|
const int value = doc[setting.key].as<int>();
|
||||||
|
if (value >= setting.valueRange.min && value <= setting.valueRange.max) {
|
||||||
|
if (setting.valueSetter) {
|
||||||
|
setting.valueSetter(static_cast<uint8_t>(value));
|
||||||
|
} else {
|
||||||
|
SETTINGS.*(setting.valuePtr) = static_cast<uint8_t>(value);
|
||||||
|
}
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SettingType::STRING: {
|
||||||
|
const char* value = doc[setting.key].as<const char*>();
|
||||||
|
if (value != nullptr) {
|
||||||
|
if (setting.stringSetter) {
|
||||||
|
setting.stringSetter(value);
|
||||||
|
} else {
|
||||||
|
strncpy(setting.stringPtr, value, setting.stringMaxLen);
|
||||||
|
setting.stringPtr[setting.stringMaxLen] = '\0';
|
||||||
|
}
|
||||||
|
updatedCount++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SettingType::ACTION:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedCount > 0) {
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
KOREADER_STORE.saveToFile();
|
||||||
|
Serial.printf("[%lu] [WEB] Updated %d settings and saved to file\n", millis(), updatedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
server->send(200, "text/plain", "Settings updated: " + String(updatedCount));
|
||||||
|
}
|
||||||
|
|||||||
@ -78,4 +78,9 @@ class CrossPointWebServer {
|
|||||||
void handleUploadPost() const;
|
void handleUploadPost() const;
|
||||||
void handleCreateFolder() const;
|
void handleCreateFolder() const;
|
||||||
void handleDelete() const;
|
void handleDelete() const;
|
||||||
|
|
||||||
|
// Settings handlers
|
||||||
|
void handleSettingsPage() const;
|
||||||
|
void handleGetSettings() const;
|
||||||
|
void handlePostSettings();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -575,6 +575,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/files">File Manager</a>
|
<a href="/files">File Manager</a>
|
||||||
|
<a href="/settings">Settings</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|||||||
@ -77,6 +77,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/files">File Manager</a>
|
<a href="/files">File Manager</a>
|
||||||
|
<a href="/settings">Settings</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
364
src/network/html/SettingsPage.html
Normal file
364
src/network/html/SettingsPage.html
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Settings - CrossPoint Reader</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
Oxygen, Ubuntu, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #34495e;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 15px 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.nav-links {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.nav-links a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.nav-links a:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
.setting-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.setting-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.setting-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
.setting-control {
|
||||||
|
min-width: 150px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
select, input[type="number"], input[type="text"] {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
select:focus, input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 50px;
|
||||||
|
height: 26px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 26px;
|
||||||
|
}
|
||||||
|
.toggle-slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
input:checked + .toggle-slider {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
input:checked + .toggle-slider:before {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.save-btn:hover {
|
||||||
|
background-color: #219a52;
|
||||||
|
}
|
||||||
|
.save-btn:disabled {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.status-message {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.status-message.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.status-message.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
<a href="/files">File Manager</a>
|
||||||
|
<a href="/settings">Settings</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settings-container">
|
||||||
|
<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 class="card">
|
||||||
|
<p style="text-align: center; color: #95a5a6; margin: 0">
|
||||||
|
CrossPoint E-Reader
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let originalSettings = {};
|
||||||
|
let currentSettings = {};
|
||||||
|
|
||||||
|
async function fetchSettings() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch settings');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
renderSettings(data.settings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching settings:', error);
|
||||||
|
document.getElementById('settings-container').innerHTML =
|
||||||
|
'<div class="loading" style="color: #e74c3c;">Failed to load settings. Please refresh the page.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSettings(settings) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'setting-row';
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'setting-label';
|
||||||
|
label.textContent = setting.name;
|
||||||
|
|
||||||
|
const control = document.createElement('div');
|
||||||
|
control.className = 'setting-control';
|
||||||
|
|
||||||
|
switch (setting.type) {
|
||||||
|
case 'toggle':
|
||||||
|
control.innerHTML = `
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="${setting.key}" ${setting.value ? 'checked' : ''}>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'enum':
|
||||||
|
let options = setting.options.map((opt, idx) =>
|
||||||
|
`<option value="${idx}" ${setting.value === idx ? 'selected' : ''}>${opt}</option>`
|
||||||
|
).join('');
|
||||||
|
control.innerHTML = `<select id="${setting.key}">${options}</select>`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'value':
|
||||||
|
control.innerHTML = `
|
||||||
|
<input type="number" id="${setting.key}"
|
||||||
|
value="${setting.value}"
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentValue(setting) {
|
||||||
|
const element = document.getElementById(setting.key);
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
switch (setting.type) {
|
||||||
|
case 'toggle':
|
||||||
|
currentSettings[setting.key] = element.checked ? 1 : 0;
|
||||||
|
break;
|
||||||
|
case 'enum':
|
||||||
|
case 'value':
|
||||||
|
currentSettings[setting.key] = parseInt(element.value, 10);
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
currentSettings[setting.key] = element.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings() {
|
||||||
|
const saveBtn = document.getElementById('save-btn');
|
||||||
|
const statusMsg = document.getElementById('status-message');
|
||||||
|
|
||||||
|
// Find changed settings
|
||||||
|
const changes = {};
|
||||||
|
for (const key in currentSettings) {
|
||||||
|
if (currentSettings[key] !== originalSettings[key]) {
|
||||||
|
changes[key] = currentSettings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(changes).length === 0) {
|
||||||
|
statusMsg.className = 'status-message success';
|
||||||
|
statusMsg.textContent = 'No changes to save.';
|
||||||
|
setTimeout(() => { statusMsg.className = 'status-message'; }, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.textContent = 'Saving...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(changes)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update original settings to match current
|
||||||
|
for (const key in changes) {
|
||||||
|
originalSettings[key] = changes[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
statusMsg.className = 'status-message success';
|
||||||
|
statusMsg.textContent = 'Settings saved successfully!';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving settings:', error);
|
||||||
|
statusMsg.className = 'status-message error';
|
||||||
|
statusMsg.textContent = 'Failed to save settings. Please try again.';
|
||||||
|
} finally {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.textContent = 'Save Settings';
|
||||||
|
setTimeout(() => { statusMsg.className = 'status-message'; }, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('save-btn').addEventListener('click', saveSettings);
|
||||||
|
window.onload = fetchSettings;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user