This commit is contained in:
Boris Faure 2026-01-27 20:22:07 +11:00 committed by GitHub
commit d26f6671e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 380 additions and 235 deletions

View File

@ -11,13 +11,105 @@
// Initialize the static instance // Initialize the static instance
CrossPointSettings CrossPointSettings::instance; CrossPointSettings CrossPointSettings::instance;
// SettingDescriptor implementations
bool SettingDescriptor::validate(const CrossPointSettings& settings) const {
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 { settings.*(memberPtr) = defaultValue; }
void SettingDescriptor::save(FsFile& file, const CrossPointSettings& settings) const {
serialization::writePod(file, settings.*(memberPtr));
}
void SettingDescriptor::load(FsFile& file, CrossPointSettings& settings) const {
uint8_t value;
serialization::readPod(file, value);
settings.*(memberPtr) = value;
}
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 20; constexpr uint8_t SETTINGS_COUNT = CrossPointSettings::DESCRIPTOR_COUNT + 1; // descriptors + opdsServerUrl string
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // 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[] = {"Back/Confirm/Left/Right", "Left/Right/Back/Confirm",
"Left/Back/Confirm/Right"};
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"};
// Helper function template to deduce array size automatically
template <size_t N>
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<N>, enumValues, N);
}
} // 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<SettingDescriptor, CrossPointSettings::DESCRIPTOR_COUNT> 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),
// Note: opdsServerUrl (string) at position 15 is handled separately in serialization
{"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}}};
bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists
SdMan.mkdir("/.crosspoint"); SdMan.mkdir("/.crosspoint");
@ -29,26 +121,20 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, SETTINGS_FILE_VERSION); serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
serialization::writePod(outputFile, SETTINGS_COUNT); serialization::writePod(outputFile, SETTINGS_COUNT);
serialization::writePod(outputFile, sleepScreen);
serialization::writePod(outputFile, extraParagraphSpacing); // Use descriptors to automatically serialize all uint8_t settings
serialization::writePod(outputFile, shortPwrBtn); // opdsServerUrl string is written at position 15 (between descriptors 14 and 15)
serialization::writePod(outputFile, statusBar); uint8_t descriptorIndex = 0;
serialization::writePod(outputFile, orientation); for (const auto& desc : descriptors) {
serialization::writePod(outputFile, frontButtonLayout); // Write opdsServerUrl string before descriptor 15 (at position 15)
serialization::writePod(outputFile, sideButtonLayout); if (descriptorIndex == 15) {
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::writeString(outputFile, std::string(opdsServerUrl));
serialization::writePod(outputFile, textAntiAliasing); }
serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip); desc.save(outputFile, *this);
serialization::writePod(outputFile, hyphenationEnabled); descriptorIndex++;
}
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -56,8 +142,10 @@ bool CrossPointSettings::saveToFile() const {
} }
bool CrossPointSettings::loadFromFile() { bool CrossPointSettings::loadFromFile() {
Serial.printf("[%lu] [CPS] Loading settings from file\n", millis());
FsFile inputFile; FsFile inputFile;
if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
Serial.printf("[%lu] [CPS] Deserialization failed: Could not open settings file\n", millis());
return false; return false;
} }
@ -72,57 +160,45 @@ bool CrossPointSettings::loadFromFile() {
uint8_t fileSettingsCount = 0; uint8_t fileSettingsCount = 0;
serialization::readPod(inputFile, fileSettingsCount); serialization::readPod(inputFile, fileSettingsCount);
// load settings that exist (support older files with fewer fields) // Use descriptors to automatically deserialize all uint8_t settings
uint8_t settingsRead = 0; // opdsServerUrl string is at position 15 (between descriptors 14 and 15)
do { uint8_t descriptorIndex = 0;
serialization::readPod(inputFile, sleepScreen); uint8_t filePosition = 0;
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, extraParagraphSpacing); for (const auto& desc : descriptors) {
if (++settingsRead >= fileSettingsCount) break; if (filePosition >= fileSettingsCount) {
serialization::readPod(inputFile, shortPwrBtn); break; // File has fewer settings than current version
if (++settingsRead >= fileSettingsCount) break; }
serialization::readPod(inputFile, statusBar);
if (++settingsRead >= fileSettingsCount) break; // Read opdsServerUrl string at position 15 (before descriptor 15)
serialization::readPod(inputFile, orientation); if (descriptorIndex == 15) {
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, frontButtonLayout);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, sideButtonLayout);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, fontFamily);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, fontSize);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, lineSpacing);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, paragraphAlignment);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, sleepTimeout);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, refreshFrequency);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, screenMargin);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, sleepScreenCoverMode);
if (++settingsRead >= fileSettingsCount) break;
{
std::string urlStr; std::string urlStr;
serialization::readString(inputFile, urlStr); serialization::readString(inputFile, urlStr);
strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1); strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1);
opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0'; opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0';
filePosition++;
if (filePosition >= fileSettingsCount) {
break;
}
}
desc.load(inputFile, *this);
descriptorIndex++;
filePosition++;
} }
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, textAntiAliasing);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hideBatteryPercentage);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, longPressChapterSkip);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hyphenationEnabled);
if (++settingsRead >= fileSettingsCount) break;
} while (false);
inputFile.close(); inputFile.close();
// Validate each setting and reset invalid values to defaults
for (const auto& desc : descriptors) {
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);
}
}
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis()); Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
return true; return true;
} }

View File

@ -1,6 +1,95 @@
#pragma once #pragma once
#include <array>
#include <cstdint> #include <cstdint>
#include <iosfwd> #include <iosfwd>
#include <string>
#include <vector>
// Setting descriptor infrastructure
enum class SettingType { TOGGLE, ENUM, VALUE };
// 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 {
uint8_t CrossPointSettings::* memberPtr; // Member pointer
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;
};
// 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) {}
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 <uint8_t MAX>
constexpr bool validateEnum(uint8_t v) {
return v < MAX;
}
template <uint8_t MIN, uint8_t MAX>
constexpr bool validateRange(uint8_t v) {
return v >= MIN && v <= MAX;
}
class CrossPointSettings { class CrossPointSettings {
private: private:
@ -15,6 +104,10 @@ class CrossPointSettings {
CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings& operator=(const CrossPointSettings&) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete;
// Static constexpr array of all setting descriptors
static constexpr size_t DESCRIPTOR_COUNT = 19;
static const std::array<SettingDescriptor, DESCRIPTOR_COUNT> descriptors;
// Should match with SettingsActivity text // Should match with SettingsActivity text
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 }; 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_MODE { FIT = 0, CROP = 1 };

View File

@ -62,77 +62,84 @@ void CategorySettingsActivity::loop() {
} }
// Handle navigation // Handle navigation
const int totalItemsCount = descriptors.size() + actionItems.size();
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (totalItemsCount - 1);
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; selectedSettingIndex = (selectedSettingIndex < totalItemsCount - 1) ? (selectedSettingIndex + 1) : 0;
updateRequired = true; updateRequired = true;
} }
} }
void CategorySettingsActivity::toggleCurrentSetting() { void CategorySettingsActivity::toggleCurrentSetting() {
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { const int totalItemsCount = descriptors.size() + actionItems.size();
if (selectedSettingIndex < 0 || selectedSettingIndex >= totalItemsCount) {
return; return;
} }
const auto& setting = settingsList[selectedSettingIndex]; // Check if it's a descriptor or an action item
if (selectedSettingIndex < static_cast<int>(descriptors.size())) {
// Handle descriptor
const auto* desc = descriptors[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { if (desc->type == SettingType::TOGGLE) {
// Toggle the boolean value using the member pointer uint8_t currentValue = desc->getValue(SETTINGS);
const bool currentValue = SETTINGS.*(setting.valuePtr); desc->setValue(SETTINGS, !currentValue);
SETTINGS.*(setting.valuePtr) = !currentValue; } else if (desc->type == SettingType::ENUM) {
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { uint8_t currentValue = desc->getValue(SETTINGS);
const uint8_t currentValue = SETTINGS.*(setting.valuePtr); desc->setValue(SETTINGS, (currentValue + 1) % desc->enumData.count);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size()); } else if (desc->type == SettingType::VALUE) {
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { uint8_t currentValue = desc->getValue(SETTINGS);
const int8_t currentValue = SETTINGS.*(setting.valuePtr); if (currentValue + desc->valueRange.step > desc->valueRange.max) {
if (currentValue + setting.valueRange.step > setting.valueRange.max) { desc->setValue(SETTINGS, desc->valueRange.min);
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
} else { } else {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; desc->setValue(SETTINGS, currentValue + desc->valueRange.step);
} }
} else if (setting.type == SettingType::ACTION) { }
if (strcmp(setting.name, "KOReader Sync") == 0) {
SETTINGS.saveToFile();
} else {
// Handle action item
const int actionIndex = selectedSettingIndex - descriptors.size();
const auto& action = actionItems[actionIndex];
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
switch (action.type) {
case ActionItem::Type::KOREADER_SYNC:
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); break;
} else if (strcmp(setting.name, "Calibre Settings") == 0) { case ActionItem::Type::CALIBRE_SETTINGS:
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); break;
} else if (strcmp(setting.name, "Clear Cache") == 0) { case ActionItem::Type::CLEAR_CACHE:
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); break;
} else if (strcmp(setting.name, "Check for updates") == 0) { case ActionItem::Type::CHECK_UPDATES:
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); break;
}
} else {
return;
} }
SETTINGS.saveToFile(); xSemaphoreGive(renderingMutex);
}
} }
void CategorySettingsActivity::displayTaskLoop() { void CategorySettingsActivity::displayTaskLoop() {
@ -153,29 +160,31 @@ void CategorySettingsActivity::render() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header with category name
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD);
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
// Draw all settings // Draw all descriptors
for (int i = 0; i < settingsCount; i++) { for (size_t i = 0; i < descriptors.size(); i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings const auto* desc = descriptors[i];
const int settingY = 60 + i * 30;
const bool isSelected = (i == selectedSettingIndex); const bool isSelected = (i == selectedSettingIndex);
// Draw setting name // 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 // Draw value based on setting type
std::string valueText; std::string valueText;
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { if (desc->type == SettingType::TOGGLE) {
const bool value = SETTINGS.*(settingsList[i].valuePtr); const bool value = desc->getValue(SETTINGS);
valueText = value ? "ON" : "OFF"; valueText = value ? "ON" : "OFF";
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { } else if (desc->type == SettingType::ENUM) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); const uint8_t value = desc->getValue(SETTINGS);
valueText = settingsList[i].enumValues[value]; valueText = desc->getEnumValueString(value);
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { } else if (desc->type == SettingType::VALUE) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); valueText = std::to_string(desc->getValue(SETTINGS));
} }
if (!valueText.empty()) { if (!valueText.empty()) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); 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), renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION); pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@ -7,38 +7,14 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "CrossPointSettings.h"
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
class CrossPointSettings; // Action items for the System category
struct ActionItem {
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
struct SettingInfo {
const char* name; const char* name;
SettingType type; enum class Type { KOREADER_SYNC, CALIBRE_SETTINGS, CLEAR_CACHE, CHECK_UPDATES };
uint8_t CrossPointSettings::* valuePtr; Type type;
std::vector<std::string> 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<std::string> 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};
}
}; };
class CategorySettingsActivity final : public ActivityWithSubactivity { class CategorySettingsActivity final : public ActivityWithSubactivity {
@ -47,8 +23,8 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
bool updateRequired = false; bool updateRequired = false;
int selectedSettingIndex = 0; int selectedSettingIndex = 0;
const char* categoryName; const char* categoryName;
const SettingInfo* settingsList; const std::vector<const SettingDescriptor*> descriptors;
int settingsCount; const std::vector<ActionItem> actionItems;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
@ -58,11 +34,12 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
public: public:
CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName, CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName,
const SettingInfo* settingsList, int settingsCount, const std::function<void()>& onGoBack) const std::vector<const SettingDescriptor*>& descriptors,
const std::vector<ActionItem>& actionItems, const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CategorySettings", renderer, mappedInput), : ActivityWithSubactivity("CategorySettings", renderer, mappedInput),
categoryName(categoryName), categoryName(categoryName),
settingsList(settingsList), descriptors(descriptors),
settingsCount(settingsCount), actionItems(actionItems),
onGoBack(onGoBack) {} onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;

View File

@ -3,6 +3,8 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <algorithm>
#include "CategorySettingsActivity.h" #include "CategorySettingsActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
@ -10,47 +12,12 @@
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
namespace { // Helper function to find descriptor by member pointer
constexpr int displaySettingsCount = 5; static const SettingDescriptor* findDescriptor(uint8_t CrossPointSettings::* memberPtr) {
const SettingInfo displaySettings[displaySettingsCount] = { auto it = std::find_if(CrossPointSettings::descriptors.begin(), CrossPointSettings::descriptors.end(),
// Should match with SLEEP_SCREEN_MODE [memberPtr](const SettingDescriptor& desc) { return desc.memberPtr == memberPtr; });
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), return (it != CrossPointSettings::descriptors.end()) ? &(*it) : nullptr;
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), }
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}),
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"}),
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("Calibre Settings"), 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);
@ -121,37 +88,50 @@ void SettingsActivity::loop() {
} }
void SettingsActivity::enterCategory(int categoryIndex) { void SettingsActivity::enterCategory(int categoryIndex) {
if (categoryIndex < 0 || categoryIndex >= categoryCount) { std::vector<const SettingDescriptor*> descriptors;
return; std::vector<ActionItem> actionItems;
switch (categoryIndex) {
case 0: // Display
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreen));
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverMode));
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); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], descriptors,
const SettingInfo* settingsList = nullptr; actionItems, [this] {
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] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));

View File

@ -9,9 +9,6 @@
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
class CrossPointSettings;
struct SettingInfo;
class SettingsActivity final : public ActivityWithSubactivity { class SettingsActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;