diff --git a/lib/Serialization/Serialization.h b/lib/Serialization/Serialization.h index afea5646..f5e8f1bd 100644 --- a/lib/Serialization/Serialization.h +++ b/lib/Serialization/Serialization.h @@ -49,4 +49,12 @@ static void readString(FsFile& file, std::string& s) { s.resize(len); file.read(&s[0], len); } + +static void readString(FsFile& file, char* buffer, size_t maxLen) { + uint32_t len; + readPod(file, len); + const uint32_t bytesToRead = (len < maxLen - 1) ? len : (maxLen - 1); + file.read(reinterpret_cast(buffer), bytesToRead); + buffer[bytesToRead] = '\0'; +} } // namespace serialization diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 232c7c57..ce5c8b55 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -11,21 +11,132 @@ // Initialize the static instance CrossPointSettings CrossPointSettings::instance; -void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { - uint8_t tempValue; - serialization::readPod(file, tempValue); - if (tempValue < maxValue) { - member = tempValue; +// SettingDescriptor implementations +bool SettingDescriptor::validate(const CrossPointSettings& settings) const { + if (type == SettingType::STRING) { + return true; // Strings are always valid } + if (!validator) { + return true; // No validator means always valid + } + const uint8_t value = settings.*(memberPtr); + return validator(value); +} + +uint8_t SettingDescriptor::getValue(const CrossPointSettings& settings) const { return settings.*(memberPtr); } + +void SettingDescriptor::setValue(CrossPointSettings& settings, uint8_t value) const { settings.*(memberPtr) = value; } + +void SettingDescriptor::resetToDefault(CrossPointSettings& settings) const { + if (type == SettingType::STRING) { + strncpy(stringPtr, stringData.defaultString, stringData.maxSize - 1); + stringPtr[stringData.maxSize - 1] = '\0'; + return; + } + setValue(settings, defaultValue); +} + +void SettingDescriptor::save(FsFile& file, const CrossPointSettings& settings) const { + if (type == SettingType::STRING) { + serialization::writeString(file, std::string(stringPtr)); + return; + } + serialization::writePod(file, settings.*(memberPtr)); +} + +void SettingDescriptor::load(FsFile& file, CrossPointSettings& settings) const { + if (type == SettingType::STRING) { + serialization::readString(file, stringPtr, stringData.maxSize); + return; + } + uint8_t value; + serialization::readPod(file, value); + settings.*(memberPtr) = value; } namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; -// Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 23; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace +// Define enum value arrays +namespace { +constexpr const char* sleepScreenValues[] = {"Dark", "Light", "Custom", "Cover", "None"}; +constexpr const char* shortPwrBtnValues[] = {"Ignore", "Sleep", "Page Turn"}; +constexpr const char* statusBarValues[] = {"None", "No Progress", "Full"}; +constexpr const char* orientationValues[] = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}; +constexpr const char* frontButtonLayoutValues[] = {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", + "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}; +constexpr const char* sideButtonLayoutValues[] = {"Prev/Next", "Next/Prev"}; +constexpr const char* fontFamilyValues[] = {"Bookerly", "Noto Sans", "Open Dyslexic"}; +constexpr const char* fontSizeValues[] = {"Small", "Medium", "Large", "X Large"}; +constexpr const char* lineSpacingValues[] = {"Tight", "Normal", "Wide"}; +constexpr const char* paragraphAlignmentValues[] = {"Justify", "Left", "Center", "Right"}; +constexpr const char* sleepTimeoutValues[] = {"1 min", "5 min", "10 min", "15 min", "30 min"}; +constexpr const char* refreshFrequencyValues[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}; +constexpr const char* sleepScreenCoverModeValues[] = {"Fit", "Crop"}; +constexpr const char* hideBatteryPercentageValues[] = {"Never", "In Reader", "Always"}; +constexpr const char* sleepScreenCoverFilterValues[] = {"None", "Contrast", "Inverted"}; + +// Helper function template to deduce array size automatically +template +constexpr SettingDescriptor makeEnumDescriptor(const char* name, uint8_t CrossPointSettings::* ptr, + uint8_t defaultValue, const char* const (&enumValues)[N]) { + return SettingDescriptor(name, SettingType::ENUM, ptr, defaultValue, validateEnum, enumValues, N); +} + +// Helper macro to create STRING descriptors without repetition +#define makeStringDescriptor(name, member, defStr) \ + SettingDescriptor(name, SettingType::STRING, CrossPointSettings::instance.member, defStr, \ + sizeof(CrossPointSettings::member)) +} // namespace + +// Define static constexpr members (required in C++14 and earlier) +constexpr size_t CrossPointSettings::DESCRIPTOR_COUNT; + +// Define the static constexpr array of all setting descriptors +// Order must match current serialization order for file format compatibility! +const std::array CrossPointSettings::descriptors = {{ + makeEnumDescriptor("Sleep Screen", &CrossPointSettings::sleepScreen, CrossPointSettings::DARK, sleepScreenValues), + {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, 1, validateToggle, + nullptr, 0}, + makeEnumDescriptor("Short Power Button Click", &CrossPointSettings::shortPwrBtn, CrossPointSettings::IGNORE, + shortPwrBtnValues), + makeEnumDescriptor("Status Bar", &CrossPointSettings::statusBar, CrossPointSettings::FULL, statusBarValues), + makeEnumDescriptor("Reading Orientation", &CrossPointSettings::orientation, CrossPointSettings::PORTRAIT, + orientationValues), + makeEnumDescriptor("Front Button Layout", &CrossPointSettings::frontButtonLayout, + CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT, frontButtonLayoutValues), + makeEnumDescriptor("Side Button Layout", &CrossPointSettings::sideButtonLayout, CrossPointSettings::PREV_NEXT, + sideButtonLayoutValues), + makeEnumDescriptor("Reader Font Family", &CrossPointSettings::fontFamily, CrossPointSettings::BOOKERLY, + fontFamilyValues), + makeEnumDescriptor("Reader Font Size", &CrossPointSettings::fontSize, CrossPointSettings::MEDIUM, fontSizeValues), + makeEnumDescriptor("Reader Line Spacing", &CrossPointSettings::lineSpacing, CrossPointSettings::NORMAL, + lineSpacingValues), + makeEnumDescriptor("Reader Paragraph Alignment", &CrossPointSettings::paragraphAlignment, + CrossPointSettings::JUSTIFIED, paragraphAlignmentValues), + makeEnumDescriptor("Time to Sleep", &CrossPointSettings::sleepTimeout, CrossPointSettings::SLEEP_10_MIN, + sleepTimeoutValues), + makeEnumDescriptor("Refresh Frequency", &CrossPointSettings::refreshFrequency, CrossPointSettings::REFRESH_15, + refreshFrequencyValues), + {"Reader Screen Margin", SettingType::VALUE, &CrossPointSettings::screenMargin, 5, validateRange<5, 40>, + ValueRange{5, 40, 5}}, + makeEnumDescriptor("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, CrossPointSettings::FIT, + sleepScreenCoverModeValues), + makeStringDescriptor("OPDS Server URL", opdsServerUrl, ""), + {"Text Anti-Aliasing", SettingType::TOGGLE, &CrossPointSettings::textAntiAliasing, 1, validateToggle, nullptr, 0}, + makeEnumDescriptor("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, CrossPointSettings::HIDE_NEVER, + hideBatteryPercentageValues), + {"Long-press Chapter Skip", SettingType::TOGGLE, &CrossPointSettings::longPressChapterSkip, 1, validateToggle, + nullptr, 0}, + {"Hyphenation", SettingType::TOGGLE, &CrossPointSettings::hyphenationEnabled, 0, validateToggle, nullptr, 0}, + makeStringDescriptor("Username", opdsUsername, ""), + makeStringDescriptor("Password", opdsPassword, ""), + makeEnumDescriptor("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, + CrossPointSettings::NO_FILTER, sleepScreenCoverFilterValues), +}}; + bool CrossPointSettings::saveToFile() const { // Make sure the directory exists SdMan.mkdir("/.crosspoint"); @@ -36,31 +147,15 @@ bool CrossPointSettings::saveToFile() const { } serialization::writePod(outputFile, SETTINGS_FILE_VERSION); - serialization::writePod(outputFile, SETTINGS_COUNT); - serialization::writePod(outputFile, sleepScreen); - serialization::writePod(outputFile, extraParagraphSpacing); - serialization::writePod(outputFile, shortPwrBtn); - serialization::writePod(outputFile, statusBar); - serialization::writePod(outputFile, orientation); - serialization::writePod(outputFile, frontButtonLayout); - serialization::writePod(outputFile, sideButtonLayout); - serialization::writePod(outputFile, fontFamily); - serialization::writePod(outputFile, fontSize); - serialization::writePod(outputFile, lineSpacing); - serialization::writePod(outputFile, paragraphAlignment); - serialization::writePod(outputFile, sleepTimeout); - serialization::writePod(outputFile, refreshFrequency); - serialization::writePod(outputFile, screenMargin); - serialization::writePod(outputFile, sleepScreenCoverMode); - serialization::writeString(outputFile, std::string(opdsServerUrl)); - serialization::writePod(outputFile, textAntiAliasing); - serialization::writePod(outputFile, hideBatteryPercentage); - serialization::writePod(outputFile, longPressChapterSkip); - serialization::writePod(outputFile, hyphenationEnabled); - serialization::writeString(outputFile, std::string(opdsUsername)); - serialization::writeString(outputFile, std::string(opdsPassword)); - serialization::writePod(outputFile, sleepScreenCoverFilter); - // New fields added at end for backward compatibility + serialization::writePod(outputFile, static_cast(CrossPointSettings::DESCRIPTOR_COUNT)); + + // Use descriptors to automatically serialize all uint8_t settings + uint8_t descriptorIndex = 0; + for (const auto& desc : descriptors) { + desc.save(outputFile, *this); + descriptorIndex++; + } + outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -68,8 +163,10 @@ bool CrossPointSettings::saveToFile() const { } bool CrossPointSettings::loadFromFile() { + Serial.printf("[%lu] [CPS] Loading settings from file\n", millis()); FsFile inputFile; if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { + Serial.printf("[%lu] [CPS] Deserialization failed: Could not open settings file\n", millis()); return false; } @@ -84,74 +181,26 @@ bool CrossPointSettings::loadFromFile() { uint8_t fileSettingsCount = 0; serialization::readPod(inputFile, fileSettingsCount); - // load settings that exist (support older files with fewer fields) - uint8_t settingsRead = 0; - do { - readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, extraParagraphSpacing); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, shortPwrBtn, SHORT_PWRBTN_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, orientation, ORIENTATION_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, fontSize, FONT_SIZE_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, lineSpacing, LINE_COMPRESSION_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, paragraphAlignment, PARAGRAPH_ALIGNMENT_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, sleepTimeout, SLEEP_TIMEOUT_COUNT); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, refreshFrequency, REFRESH_FREQUENCY_COUNT); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, screenMargin); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, sleepScreenCoverMode, SLEEP_SCREEN_COVER_MODE_COUNT); - if (++settingsRead >= fileSettingsCount) break; - { - std::string urlStr; - serialization::readString(inputFile, urlStr); - strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1); - opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0'; - } - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, textAntiAliasing); - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, longPressChapterSkip); - if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, hyphenationEnabled); - if (++settingsRead >= fileSettingsCount) break; - { - std::string usernameStr; - serialization::readString(inputFile, usernameStr); - strncpy(opdsUsername, usernameStr.c_str(), sizeof(opdsUsername) - 1); - opdsUsername[sizeof(opdsUsername) - 1] = '\0'; - } - if (++settingsRead >= fileSettingsCount) break; - { - std::string passwordStr; - serialization::readString(inputFile, passwordStr); - strncpy(opdsPassword, passwordStr.c_str(), sizeof(opdsPassword) - 1); - opdsPassword[sizeof(opdsPassword) - 1] = '\0'; - } - if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); - if (++settingsRead >= fileSettingsCount) break; - // New fields added at end for backward compatibility - } while (false); + // Use descriptors to automatically deserialize all uint8_t settings + uint8_t descriptorIndex = 0; + uint8_t filePosition = 0; + for (const auto& desc : descriptors) { + if (filePosition >= fileSettingsCount) { + break; // File has fewer settings than current version + } + + desc.load(inputFile, *this); + if (!desc.validate(*this)) { + Serial.printf("[%lu] [CPS] Invalid value (0x%X) for %s, resetting to default\n", millis(), desc.getValue(*this), + desc.name); + desc.resetToDefault(*this); + } + descriptorIndex++; + filePosition++; + } inputFile.close(); + Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis()); return true; } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c450d348..44059bf1 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -1,6 +1,112 @@ #pragma once +#include #include #include +#include +#include + +// Setting descriptor infrastructure +enum class SettingType { TOGGLE, ENUM, VALUE, STRING }; + +// Validator function pointer (not std::function to save memory) +using SettingValidator = bool (*)(uint8_t); + +// Forward declare for descriptors +class CrossPointSettings; + +// Forward declare file type +class FsFile; + +// Base descriptor for all settings (non-virtual for constexpr) +struct SettingDescriptorBase { + const char* name; // Display name + SettingType type; +}; + +// Value range for VALUE type settings +struct ValueRange { + uint8_t min, max, step; +}; + +// Concrete descriptor for uint8_t settings (constexpr-compatible) +struct SettingDescriptor : public SettingDescriptorBase { + union { + uint8_t CrossPointSettings::* memberPtr; // For TOGGLE/ENUM/VALUE types + char* stringPtr; // For STRING type + }; + uint8_t defaultValue; + SettingValidator validator; // Optional validator function + + union { + // For ENUM types + struct { + const char* const* values; + uint8_t count; + } enumData; + + // For VALUE types + ValueRange valueRange; + + // For STRING types + struct { + const char* defaultString; // Default string value + size_t maxSize; // Max size of the string buffer + } stringData; + }; + + // Constexpr constructors for different setting types + // TOGGLE/ENUM constructor + constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal, + SettingValidator val, const char* const* enumVals, uint8_t enumCnt) + : SettingDescriptorBase{name_, type_}, + memberPtr(ptr), + defaultValue(defVal), + validator(val), + enumData{enumVals, enumCnt} {} + + // VALUE constructor + constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal, + SettingValidator val, ValueRange valRange) + : SettingDescriptorBase{name_, type_}, + memberPtr(ptr), + defaultValue(defVal), + validator(val), + valueRange(valRange) {} + + // STRING constructor + constexpr SettingDescriptor(const char* name_, SettingType type_, char* strPtr, const char* defStr, size_t maxSz) + : SettingDescriptorBase{name_, type_}, + stringPtr(strPtr), + defaultValue(0), + validator(nullptr), + stringData{defStr, maxSz} {} + + bool validate(const CrossPointSettings& settings) const; + uint8_t getValue(const CrossPointSettings& settings) const; + void setValue(CrossPointSettings& settings, uint8_t value) const; + void resetToDefault(CrossPointSettings& settings) const; + void save(FsFile& file, const CrossPointSettings& settings) const; + void load(FsFile& file, CrossPointSettings& settings) const; + + // Helper to get enum value as string + const char* getEnumValueString(uint8_t index) const { + if (index < enumData.count && enumData.values) { + return enumData.values[index]; + } + return ""; + } +}; + +// Validator functions (constexpr for compile-time optimization) +constexpr bool validateToggle(uint8_t v) { return v <= 1; } +template +constexpr bool validateEnum(uint8_t v) { + return v < MAX; +} +template +constexpr bool validateRange(uint8_t v) { + return v >= MIN && v <= MAX; +} class CrossPointSettings { private: @@ -15,13 +121,17 @@ class CrossPointSettings { CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete; - enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4, SLEEP_SCREEN_MODE_COUNT }; - enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; + // Static constexpr array of all setting descriptors + static constexpr size_t DESCRIPTOR_COUNT = 23; + static const std::array descriptors; + + // Should match with SettingsActivity text + enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 }; + enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 }; enum SLEEP_SCREEN_COVER_FILTER { NO_FILTER = 0, BLACK_AND_WHITE = 1, INVERTED_BLACK_AND_WHITE = 2, - SLEEP_SCREEN_COVER_FILTER_COUNT }; // Status bar display type enum @@ -31,7 +141,6 @@ class CrossPointSettings { FULL = 2, FULL_WITH_PROGRESS_BAR = 3, ONLY_PROGRESS_BAR = 4, - STATUS_BAR_MODE_COUNT }; enum ORIENTATION { @@ -39,7 +148,6 @@ class CrossPointSettings { LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) INVERTED = 2, // 480x800 logical coordinates, inverted LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation - ORIENTATION_COUNT }; // Front button layout options @@ -50,25 +158,23 @@ class CrossPointSettings { LEFT_RIGHT_BACK_CONFIRM = 1, LEFT_BACK_CONFIRM_RIGHT = 2, BACK_CONFIRM_RIGHT_LEFT = 3, - FRONT_BUTTON_LAYOUT_COUNT }; // Side button layout options // Default: Previous, Next // Swapped: Next, Previous - enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT }; + enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 }; // Font family options - enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT }; + enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 }; // Font size options - enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT }; - enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT }; + enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 }; + enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 }; enum PARAGRAPH_ALIGNMENT { JUSTIFIED = 0, LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3, - PARAGRAPH_ALIGNMENT_COUNT }; // Auto-sleep timeout options (in minutes) @@ -78,7 +184,6 @@ class CrossPointSettings { SLEEP_10_MIN = 2, SLEEP_15_MIN = 3, SLEEP_30_MIN = 4, - SLEEP_TIMEOUT_COUNT }; // E-ink refresh frequency (pages between full refreshes) @@ -88,14 +193,13 @@ class CrossPointSettings { REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4, - REFRESH_FREQUENCY_COUNT }; // Short power button press actions - enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT }; + enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 }; // Hide battery percentage - enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; + enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 }; // Sleep screen settings uint8_t sleepScreen = DARK; diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 7fd5ef5f..314511b6 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -62,77 +62,84 @@ void CategorySettingsActivity::loop() { } // Handle navigation + const int totalItemsCount = descriptors.size() + actionItems.size(); + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); + selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (totalItemsCount - 1); updateRequired = true; } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { - selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; + selectedSettingIndex = (selectedSettingIndex < totalItemsCount - 1) ? (selectedSettingIndex + 1) : 0; updateRequired = true; } } void CategorySettingsActivity::toggleCurrentSetting() { - if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { + const int totalItemsCount = descriptors.size() + actionItems.size(); + + if (selectedSettingIndex < 0 || selectedSettingIndex >= totalItemsCount) { return; } - const auto& setting = settingsList[selectedSettingIndex]; + // Check if it's a descriptor or an action item + if (selectedSettingIndex < static_cast(descriptors.size())) { + // Handle descriptor + const auto* desc = descriptors[selectedSettingIndex]; - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - // Toggle the boolean value using the member pointer - const bool currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = !currentValue; - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - const int8_t currentValue = SETTINGS.*(setting.valuePtr); - if (currentValue + setting.valueRange.step > setting.valueRange.max) { - SETTINGS.*(setting.valuePtr) = setting.valueRange.min; - } else { - SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; - } - } else if (setting.type == SettingType::ACTION) { - if (strcmp(setting.name, "KOReader Sync") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "OPDS Browser") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Clear Cache") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Check for updates") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); + if (desc->type == SettingType::TOGGLE) { + uint8_t currentValue = desc->getValue(SETTINGS); + desc->setValue(SETTINGS, !currentValue); + } else if (desc->type == SettingType::ENUM) { + uint8_t currentValue = desc->getValue(SETTINGS); + desc->setValue(SETTINGS, (currentValue + 1) % desc->enumData.count); + } else if (desc->type == SettingType::VALUE) { + uint8_t currentValue = desc->getValue(SETTINGS); + if (currentValue + desc->valueRange.step > desc->valueRange.max) { + desc->setValue(SETTINGS, desc->valueRange.min); + } else { + desc->setValue(SETTINGS, currentValue + desc->valueRange.step); + } } + + SETTINGS.saveToFile(); } else { - return; - } + // Handle action item + const int actionIndex = selectedSettingIndex - descriptors.size(); + const auto& action = actionItems[actionIndex]; - SETTINGS.saveToFile(); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + + switch (action.type) { + case ActionItem::Type::KOREADER_SYNC: + enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + case ActionItem::Type::CALIBRE_SETTINGS: + enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + case ActionItem::Type::CLEAR_CACHE: + enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + case ActionItem::Type::CHECK_UPDATES: + enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + break; + } + + xSemaphoreGive(renderingMutex); + } } void CategorySettingsActivity::displayTaskLoop() { @@ -153,29 +160,31 @@ void CategorySettingsActivity::render() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + // Draw header with category name renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); // Draw selection highlight renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); - // Draw all settings - for (int i = 0; i < settingsCount; i++) { - const int settingY = 60 + i * 30; // 30 pixels between settings + // Draw all descriptors + for (size_t i = 0; i < descriptors.size(); i++) { + const auto* desc = descriptors[i]; + const int settingY = 60 + i * 30; const bool isSelected = (i == selectedSettingIndex); // Draw setting name - renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); + renderer.drawText(UI_10_FONT_ID, 20, settingY, desc->name, !isSelected); // Draw value based on setting type std::string valueText; - if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { - const bool value = SETTINGS.*(settingsList[i].valuePtr); + if (desc->type == SettingType::TOGGLE) { + const bool value = desc->getValue(SETTINGS); valueText = value ? "ON" : "OFF"; - } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); - valueText = settingsList[i].enumValues[value]; - } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { - valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); + } else if (desc->type == SettingType::ENUM) { + const uint8_t value = desc->getValue(SETTINGS); + valueText = desc->getEnumValueString(value); + } else if (desc->type == SettingType::VALUE) { + valueText = std::to_string(desc->getValue(SETTINGS)); } if (!valueText.empty()) { const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); @@ -183,9 +192,22 @@ void CategorySettingsActivity::render() const { } } + // Draw all action items + for (size_t i = 0; i < actionItems.size(); i++) { + const auto& action = actionItems[i]; + const int itemIndex = descriptors.size() + i; + const int settingY = 60 + itemIndex * 30; + const bool isSelected = (itemIndex == selectedSettingIndex); + + // Draw action name + renderer.drawText(UI_10_FONT_ID, 20, settingY, action.name, !isSelected); + } + + // Draw version text above button hints renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), pageHeight - 60, CROSSPOINT_VERSION); + // Draw help text const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index a7d1f0ce..cf40655e 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -7,38 +7,14 @@ #include #include +#include "CrossPointSettings.h" #include "activities/ActivityWithSubactivity.h" -class CrossPointSettings; - -enum class SettingType { TOGGLE, ENUM, ACTION, VALUE }; - -struct SettingInfo { +// Action items for the System category +struct ActionItem { const char* name; - SettingType type; - uint8_t CrossPointSettings::* valuePtr; - std::vector enumValues; - - struct ValueRange { - uint8_t min; - uint8_t max; - uint8_t step; - }; - ValueRange valueRange; - - static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { - return {name, SettingType::TOGGLE, ptr}; - } - - static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector values) { - return {name, SettingType::ENUM, ptr, std::move(values)}; - } - - static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } - - static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { - return {name, SettingType::VALUE, ptr, {}, valueRange}; - } + enum class Type { KOREADER_SYNC, CALIBRE_SETTINGS, CLEAR_CACHE, CHECK_UPDATES }; + Type type; }; class CategorySettingsActivity final : public ActivityWithSubactivity { @@ -47,8 +23,8 @@ class CategorySettingsActivity final : public ActivityWithSubactivity { bool updateRequired = false; int selectedSettingIndex = 0; const char* categoryName; - const SettingInfo* settingsList; - int settingsCount; + const std::vector descriptors; + const std::vector actionItems; const std::function onGoBack; static void taskTrampoline(void* param); @@ -58,11 +34,12 @@ class CategorySettingsActivity final : public ActivityWithSubactivity { public: CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName, - const SettingInfo* settingsList, int settingsCount, const std::function& onGoBack) + const std::vector& descriptors, + const std::vector& actionItems, const std::function& onGoBack) : ActivityWithSubactivity("CategorySettings", renderer, mappedInput), categoryName(categoryName), - settingsList(settingsList), - settingsCount(settingsCount), + descriptors(descriptors), + actionItems(actionItems), onGoBack(onGoBack) {} void onEnter() override; void onExit() override; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..7c974e58 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "CategorySettingsActivity.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" @@ -10,51 +12,12 @@ 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 +// Helper function to find descriptor by member pointer +static const SettingDescriptor* findDescriptor(uint8_t CrossPointSettings::* memberPtr) { + auto it = std::find_if(CrossPointSettings::descriptors.begin(), CrossPointSettings::descriptors.end(), + [memberPtr](const SettingDescriptor& desc) { return desc.memberPtr == memberPtr; }); + return (it != CrossPointSettings::descriptors.end()) ? &(*it) : nullptr; +} void SettingsActivity::taskTrampoline(void* param) { auto* self = static_cast(param); @@ -125,37 +88,51 @@ void SettingsActivity::loop() { } void SettingsActivity::enterCategory(int categoryIndex) { - if (categoryIndex < 0 || categoryIndex >= categoryCount) { - return; + std::vector descriptors; + std::vector actionItems; + + switch (categoryIndex) { + case 0: // Display + descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreen)); + descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverMode)); + descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverFilter)); + descriptors.push_back(findDescriptor(&CrossPointSettings::statusBar)); + descriptors.push_back(findDescriptor(&CrossPointSettings::hideBatteryPercentage)); + descriptors.push_back(findDescriptor(&CrossPointSettings::refreshFrequency)); + break; + + case 1: // Reader + descriptors.push_back(findDescriptor(&CrossPointSettings::fontFamily)); + descriptors.push_back(findDescriptor(&CrossPointSettings::fontSize)); + descriptors.push_back(findDescriptor(&CrossPointSettings::lineSpacing)); + descriptors.push_back(findDescriptor(&CrossPointSettings::screenMargin)); + descriptors.push_back(findDescriptor(&CrossPointSettings::paragraphAlignment)); + descriptors.push_back(findDescriptor(&CrossPointSettings::hyphenationEnabled)); + descriptors.push_back(findDescriptor(&CrossPointSettings::orientation)); + descriptors.push_back(findDescriptor(&CrossPointSettings::extraParagraphSpacing)); + descriptors.push_back(findDescriptor(&CrossPointSettings::textAntiAliasing)); + break; + + case 2: // Controls + descriptors.push_back(findDescriptor(&CrossPointSettings::frontButtonLayout)); + descriptors.push_back(findDescriptor(&CrossPointSettings::sideButtonLayout)); + descriptors.push_back(findDescriptor(&CrossPointSettings::longPressChapterSkip)); + descriptors.push_back(findDescriptor(&CrossPointSettings::shortPwrBtn)); + break; + + case 3: // System + descriptors.push_back(findDescriptor(&CrossPointSettings::sleepTimeout)); + actionItems.push_back({"KOReader Sync", ActionItem::Type::KOREADER_SYNC}); + actionItems.push_back({"Calibre Settings", ActionItem::Type::CALIBRE_SETTINGS}); + actionItems.push_back({"Clear Cache", ActionItem::Type::CLEAR_CACHE}); + actionItems.push_back({"Check for updates", ActionItem::Type::CHECK_UPDATES}); + break; } xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); - - const SettingInfo* settingsList = nullptr; - int settingsCount = 0; - - 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] { + enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], descriptors, + actionItems, [this] { exitActivity(); updateRequired = true; })); diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 821dda42..4a93d7fd 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -9,9 +9,6 @@ #include "activities/ActivityWithSubactivity.h" -class CrossPointSettings; -struct SettingInfo; - class SettingsActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr;