This commit is contained in:
Boris Faure 2026-02-01 19:23:26 +11:00 committed by GitHub
commit a3cc1b9ca3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 421 additions and 287 deletions

View File

@ -49,4 +49,12 @@ static void readString(FsFile& file, std::string& s) {
s.resize(len); s.resize(len);
file.read(&s[0], 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<uint8_t*>(buffer), bytesToRead);
buffer[bytesToRead] = '\0';
}
} // namespace serialization } // namespace serialization

View File

@ -11,21 +11,132 @@
// Initialize the static instance // Initialize the static instance
CrossPointSettings CrossPointSettings::instance; CrossPointSettings CrossPointSettings::instance;
void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { // SettingDescriptor implementations
uint8_t tempValue; bool SettingDescriptor::validate(const CrossPointSettings& settings) const {
serialization::readPod(file, tempValue); if (type == SettingType::STRING) {
if (tempValue < maxValue) { return true; // Strings are always valid
member = tempValue;
} }
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 { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; 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"; 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[] = {"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 <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);
}
// 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<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),
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 { bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists
SdMan.mkdir("/.crosspoint"); SdMan.mkdir("/.crosspoint");
@ -36,31 +147,15 @@ bool CrossPointSettings::saveToFile() const {
} }
serialization::writePod(outputFile, SETTINGS_FILE_VERSION); serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
serialization::writePod(outputFile, SETTINGS_COUNT); serialization::writePod(outputFile, static_cast<uint8_t>(CrossPointSettings::DESCRIPTOR_COUNT));
serialization::writePod(outputFile, sleepScreen);
serialization::writePod(outputFile, extraParagraphSpacing); // Use descriptors to automatically serialize all uint8_t settings
serialization::writePod(outputFile, shortPwrBtn); uint8_t descriptorIndex = 0;
serialization::writePod(outputFile, statusBar); for (const auto& desc : descriptors) {
serialization::writePod(outputFile, orientation); desc.save(outputFile, *this);
serialization::writePod(outputFile, frontButtonLayout); descriptorIndex++;
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
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -68,8 +163,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;
} }
@ -84,74 +181,26 @@ 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; uint8_t descriptorIndex = 0;
do { uint8_t filePosition = 0;
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);
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(); inputFile.close();
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,112 @@
#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, 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 <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,13 +121,17 @@ class CrossPointSettings {
CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings& operator=(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 }; // Static constexpr array of all setting descriptors
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; static constexpr size_t DESCRIPTOR_COUNT = 23;
static const std::array<SettingDescriptor, DESCRIPTOR_COUNT> 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 { enum SLEEP_SCREEN_COVER_FILTER {
NO_FILTER = 0, NO_FILTER = 0,
BLACK_AND_WHITE = 1, BLACK_AND_WHITE = 1,
INVERTED_BLACK_AND_WHITE = 2, INVERTED_BLACK_AND_WHITE = 2,
SLEEP_SCREEN_COVER_FILTER_COUNT
}; };
// Status bar display type enum // Status bar display type enum
@ -31,7 +141,6 @@ class CrossPointSettings {
FULL = 2, FULL = 2,
FULL_WITH_PROGRESS_BAR = 3, FULL_WITH_PROGRESS_BAR = 3,
ONLY_PROGRESS_BAR = 4, ONLY_PROGRESS_BAR = 4,
STATUS_BAR_MODE_COUNT
}; };
enum ORIENTATION { enum ORIENTATION {
@ -39,7 +148,6 @@ class CrossPointSettings {
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
INVERTED = 2, // 480x800 logical coordinates, inverted INVERTED = 2, // 480x800 logical coordinates, inverted
LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation
ORIENTATION_COUNT
}; };
// Front button layout options // Front button layout options
@ -50,25 +158,23 @@ class CrossPointSettings {
LEFT_RIGHT_BACK_CONFIRM = 1, LEFT_RIGHT_BACK_CONFIRM = 1,
LEFT_BACK_CONFIRM_RIGHT = 2, LEFT_BACK_CONFIRM_RIGHT = 2,
BACK_CONFIRM_RIGHT_LEFT = 3, BACK_CONFIRM_RIGHT_LEFT = 3,
FRONT_BUTTON_LAYOUT_COUNT
}; };
// Side button layout options // Side button layout options
// Default: Previous, Next // Default: Previous, Next
// Swapped: Next, Previous // 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 // 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 // Font size options
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT }; enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT }; enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
enum PARAGRAPH_ALIGNMENT { enum PARAGRAPH_ALIGNMENT {
JUSTIFIED = 0, JUSTIFIED = 0,
LEFT_ALIGN = 1, LEFT_ALIGN = 1,
CENTER_ALIGN = 2, CENTER_ALIGN = 2,
RIGHT_ALIGN = 3, RIGHT_ALIGN = 3,
PARAGRAPH_ALIGNMENT_COUNT
}; };
// Auto-sleep timeout options (in minutes) // Auto-sleep timeout options (in minutes)
@ -78,7 +184,6 @@ class CrossPointSettings {
SLEEP_10_MIN = 2, SLEEP_10_MIN = 2,
SLEEP_15_MIN = 3, SLEEP_15_MIN = 3,
SLEEP_30_MIN = 4, SLEEP_30_MIN = 4,
SLEEP_TIMEOUT_COUNT
}; };
// E-ink refresh frequency (pages between full refreshes) // E-ink refresh frequency (pages between full refreshes)
@ -88,14 +193,13 @@ class CrossPointSettings {
REFRESH_10 = 2, REFRESH_10 = 2,
REFRESH_15 = 3, REFRESH_15 = 3,
REFRESH_30 = 4, REFRESH_30 = 4,
REFRESH_FREQUENCY_COUNT
}; };
// Short power button press actions // 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 // 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 // Sleep screen settings
uint8_t sleepScreen = DARK; uint8_t sleepScreen = DARK;

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, "OPDS Browser") == 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,51 +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 = 6; 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("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);
@ -125,37 +88,51 @@ 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::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); 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;