Files
crosspoint-reader-mod/src/CrossPointSettings.cpp
cottongin 1383d75c84 feat: Add per-family font and per-language hyphenation build flags
Add OMIT_BOOKERLY, OMIT_NOTOSANS, OMIT_OPENDYSLEXIC flags to
selectively exclude font families, and OMIT_HYPH_DE/EN/ES/FR/IT/RU
flags to exclude individual hyphenation language tries.

The mod build environment excludes OpenDyslexic (~1.03 MB) and all
hyphenation tries (~282 KB), reducing flash usage by ~1.3 MB.

Font Family setting switched from Enum to DynamicEnum with
index-to-value mapping to handle arbitrary font exclusion without
breaking the settings UI or persisted values.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 00:48:23 -05:00

398 lines
14 KiB
C++

#include "CrossPointSettings.h"
#include <HalStorage.h>
#include <Logging.h>
#include <Serialization.h>
#include <cstring>
#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;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 31;
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<CrossPointSettings::FRONT_BUTTON_LAYOUT>(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
bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists
Storage.mkdir("/.crosspoint");
FsFile outputFile;
if (!Storage.openFileForWrite("CPS", SETTINGS_FILE, outputFile)) {
return false;
}
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); // legacy
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);
serialization::writePod(outputFile, uiTheme);
serialization::writePod(outputFile, frontButtonBack);
serialization::writePod(outputFile, frontButtonConfirm);
serialization::writePod(outputFile, frontButtonLeft);
serialization::writePod(outputFile, frontButtonRight);
serialization::writePod(outputFile, fadingFix);
serialization::writePod(outputFile, embeddedStyle);
serialization::writePod(outputFile, sleepScreenLetterboxFill);
// New fields added at end for backward compatibility
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;
readAndValidate(inputFile, sleepScreenLetterboxFill, SLEEP_SCREEN_LETTERBOX_FILL_COUNT);
if (++settingsRead >= fileSettingsCount) break;
{ uint8_t _ignore; serialization::readPod(inputFile, _ignore); } // legacy: sleepScreenGradientDir
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) {
#ifndef OMIT_BOOKERLY
case BOOKERLY:
switch (lineSpacing) {
case TIGHT:
return 0.95f;
case NORMAL:
default:
return 1.0f;
case WIDE:
return 1.1f;
}
#endif // OMIT_BOOKERLY
#ifndef OMIT_NOTOSANS
case NOTOSANS:
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
#endif // OMIT_NOTOSANS
#ifndef OMIT_OPENDYSLEXIC
case OPENDYSLEXIC:
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
#endif // OMIT_OPENDYSLEXIC
default:
// Fallback: use Bookerly-style compression, or Noto Sans if Bookerly is omitted
#if !defined(OMIT_BOOKERLY)
switch (lineSpacing) {
case TIGHT:
return 0.95f;
case NORMAL:
default:
return 1.0f;
case WIDE:
return 1.1f;
}
#else
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
#endif
}
}
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) {
#ifndef OMIT_BOOKERLY
case BOOKERLY:
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;
}
#endif // OMIT_BOOKERLY
#ifndef OMIT_NOTOSANS
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;
}
#endif // OMIT_NOTOSANS
#ifndef OMIT_OPENDYSLEXIC
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;
}
#endif // OMIT_OPENDYSLEXIC
default:
// Fallback to first available font family at medium size
#if !defined(OMIT_BOOKERLY)
return BOOKERLY_14_FONT_ID;
#elif !defined(OMIT_NOTOSANS)
return NOTOSANS_14_FONT_ID;
#elif !defined(OMIT_OPENDYSLEXIC)
return OPENDYSLEXIC_10_FONT_ID;
#else
#error "At least one font family must be available"
#endif
}
}