#include "CrossPointSettings.h" #include #include #include #include #include #include "fontIds.h" // 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; } } namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // SETTINGS_COUNT is now calculated automatically in saveToFile constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; // Validate front button mapping to ensure each hardware button is unique. // If duplicates are detected, reset to the default physical order to prevent invalid mappings. void validateFrontButtonMapping(CrossPointSettings& settings) { // Snapshot the logical->hardware mapping so we can compare for duplicates. const uint8_t mapping[] = {settings.frontButtonBack, settings.frontButtonConfirm, settings.frontButtonLeft, settings.frontButtonRight}; for (size_t i = 0; i < 4; i++) { for (size_t j = i + 1; j < 4; j++) { if (mapping[i] == mapping[j]) { // Duplicate detected: restore the default physical order (Back, Confirm, Left, Right). settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; return; } } } } // Convert legacy front button layout into explicit logical->hardware mapping. void applyLegacyFrontButtonLayout(CrossPointSettings& settings) { switch (static_cast(settings.frontButtonLayout)) { case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: settings.frontButtonBack = CrossPointSettings::FRONT_HW_LEFT; settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_RIGHT; settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK; settings.frontButtonRight = CrossPointSettings::FRONT_HW_CONFIRM; break; case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: settings.frontButtonBack = CrossPointSettings::FRONT_HW_CONFIRM; settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_LEFT; settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK; settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; break; case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; settings.frontButtonLeft = CrossPointSettings::FRONT_HW_RIGHT; settings.frontButtonRight = CrossPointSettings::FRONT_HW_LEFT; break; case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: default: settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; break; } } } // namespace class SettingsWriter { public: bool is_counting = false; uint8_t item_count = 0; template void writeItem(FsFile& file, const T& value) { if (is_counting) { item_count++; } else { serialization::writePod(file, value); } } void writeItemString(FsFile& file, const char* value) { if (is_counting) { item_count++; } else { serialization::writeString(file, std::string(value)); } } }; uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const { SettingsWriter writer; writer.is_counting = count_only; writer.writeItem(file, sleepScreen); writer.writeItem(file, extraParagraphSpacing); writer.writeItem(file, shortPwrBtn); writer.writeItem(file, statusBar); writer.writeItem(file, orientation); writer.writeItem(file, frontButtonLayout); // legacy writer.writeItem(file, sideButtonLayout); writer.writeItem(file, fontFamily); writer.writeItem(file, fontSize); writer.writeItem(file, lineSpacing); writer.writeItem(file, paragraphAlignment); writer.writeItem(file, sleepTimeout); writer.writeItem(file, refreshFrequency); writer.writeItem(file, screenMargin); writer.writeItem(file, sleepScreenCoverMode); writer.writeItemString(file, opdsServerUrl); writer.writeItem(file, textAntiAliasing); writer.writeItem(file, hideBatteryPercentage); writer.writeItem(file, longPressChapterSkip); writer.writeItem(file, hyphenationEnabled); writer.writeItemString(file, opdsUsername); writer.writeItemString(file, opdsPassword); writer.writeItem(file, sleepScreenCoverFilter); writer.writeItem(file, uiTheme); writer.writeItem(file, frontButtonBack); writer.writeItem(file, frontButtonConfirm); writer.writeItem(file, frontButtonLeft); writer.writeItem(file, frontButtonRight); writer.writeItem(file, fadingFix); writer.writeItem(file, embeddedStyle); // New fields need to be added at end for backward compatibility return writer.item_count; } bool CrossPointSettings::saveToFile() const { // Make sure the directory exists Storage.mkdir("/.crosspoint"); FsFile outputFile; if (!Storage.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) { return false; } // First pass: count the items uint8_t item_count = writeSettings(outputFile, true); // This will just count, not write // Write header serialization::writePod(outputFile, SETTINGS_FILE_VERSION); serialization::writePod(outputFile, static_cast(item_count)); // Second pass: actually write the settings writeSettings(outputFile); // This will write the actual data outputFile.close(); LOG_DBG("CPS", "Settings saved to file"); return true; } bool CrossPointSettings::loadFromFile() { FsFile inputFile; if (!Storage.openFileForRead("CPS", SETTINGS_FILE, inputFile)) { return false; } uint8_t version; serialization::readPod(inputFile, version); if (version != SETTINGS_FILE_VERSION) { LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version); inputFile.close(); return false; } uint8_t fileSettingsCount = 0; serialization::readPod(inputFile, fileSettingsCount); // load settings that exist (support older files with fewer fields) uint8_t settingsRead = 0; // Track whether remap fields were present in the settings file. bool frontButtonMappingRead = false; 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); // legacy 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; serialization::readPod(inputFile, uiTheme); if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, frontButtonBack, FRONT_BUTTON_HARDWARE_COUNT); if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, frontButtonConfirm, FRONT_BUTTON_HARDWARE_COUNT); if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, frontButtonLeft, FRONT_BUTTON_HARDWARE_COUNT); if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, frontButtonRight, FRONT_BUTTON_HARDWARE_COUNT); frontButtonMappingRead = true; if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, fadingFix); if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, embeddedStyle); if (++settingsRead >= fileSettingsCount) break; // New fields added at end for backward compatibility } while (false); if (frontButtonMappingRead) { validateFrontButtonMapping(*this); } else { applyLegacyFrontButtonLayout(*this); } inputFile.close(); LOG_DBG("CPS", "Settings loaded from file"); return true; } float CrossPointSettings::getReaderLineCompression() const { switch (fontFamily) { case BOOKERLY: default: switch (lineSpacing) { case TIGHT: return 0.95f; case NORMAL: default: return 1.0f; case WIDE: return 1.1f; } case NOTOSANS: switch (lineSpacing) { case TIGHT: return 0.90f; case NORMAL: default: return 0.95f; case WIDE: return 1.0f; } case OPENDYSLEXIC: switch (lineSpacing) { case TIGHT: return 0.90f; case NORMAL: default: return 0.95f; case WIDE: return 1.0f; } } } unsigned long CrossPointSettings::getSleepTimeoutMs() const { switch (sleepTimeout) { case SLEEP_1_MIN: return 1UL * 60 * 1000; case SLEEP_5_MIN: return 5UL * 60 * 1000; case SLEEP_10_MIN: default: return 10UL * 60 * 1000; case SLEEP_15_MIN: return 15UL * 60 * 1000; case SLEEP_30_MIN: return 30UL * 60 * 1000; } } int CrossPointSettings::getRefreshFrequency() const { switch (refreshFrequency) { case REFRESH_1: return 1; case REFRESH_5: return 5; case REFRESH_10: return 10; case REFRESH_15: default: return 15; case REFRESH_30: return 30; } } int CrossPointSettings::getReaderFontId() const { switch (fontFamily) { case BOOKERLY: default: switch (fontSize) { case SMALL: return BOOKERLY_12_FONT_ID; case MEDIUM: default: return BOOKERLY_14_FONT_ID; case LARGE: return BOOKERLY_16_FONT_ID; case EXTRA_LARGE: return BOOKERLY_18_FONT_ID; } case NOTOSANS: switch (fontSize) { case SMALL: return NOTOSANS_12_FONT_ID; case MEDIUM: default: return NOTOSANS_14_FONT_ID; case LARGE: return NOTOSANS_16_FONT_ID; case EXTRA_LARGE: return NOTOSANS_18_FONT_ID; } case OPENDYSLEXIC: switch (fontSize) { case SMALL: return OPENDYSLEXIC_8_FONT_ID; case MEDIUM: default: return OPENDYSLEXIC_10_FONT_ID; case LARGE: return OPENDYSLEXIC_12_FONT_ID; case EXTRA_LARGE: return OPENDYSLEXIC_14_FONT_ID; } } }