## Summary * **What is the goal of this PR?** Mainly fixes #2196, but also allows users to set a sleep time of 1~30 min or to never sleep. * **What changes are included?** 1. Revert changes from #1948 and #2137 (Sorry @Uri-Tauber) 2. Cherrypick changes from https://github.com/uxjulia/CrossInk/commit/0cb91c3dd1c63f0559d4392fadc4a8340af5bdde and https://github.com/uxjulia/CrossInk/commit/4e6bd634b6ede51ff1a7107ba3e8efcd152bc6ee for a timeout interval picker UI, the custom sleep time feature, and migrate the existing setting in v1.3.0, `sleepTimeout`, to `sleepTimeoutMinutes` (Thanks! @uxjulia) 3. Add a "Never" at the far right of the timeout interval picker --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_ --------- Co-authored-by: Julia <julia@uxj.io>
395 lines
13 KiB
C++
395 lines
13 KiB
C++
#include "CrossPointSettings.h"
|
|
|
|
#include <HalStorage.h>
|
|
#include <JsonSettingsIO.h>
|
|
#include <Logging.h>
|
|
#include <Serialization.h>
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "I18nKeys.h"
|
|
#include "fontIds.h"
|
|
|
|
// Initialize the static instance
|
|
CrossPointSettings CrossPointSettings::instance;
|
|
|
|
void readAndValidate(HalFile& 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;
|
|
constexpr char SETTINGS_FILE_BIN[] = "/.crosspoint/settings.bin";
|
|
constexpr char SETTINGS_FILE_JSON[] = "/.crosspoint/settings.json";
|
|
constexpr char SETTINGS_FILE_BAK[] = "/.crosspoint/settings.bin.bak";
|
|
constexpr char LANG_FILE_BIN[] = "/.crosspoint/language.bin";
|
|
constexpr char LANG_FILE_BAK[] = "/.crosspoint/language.bin.bak";
|
|
|
|
// 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
|
|
|
|
void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) {
|
|
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]) {
|
|
settings.frontButtonBack = FRONT_HW_BACK;
|
|
settings.frontButtonConfirm = FRONT_HW_CONFIRM;
|
|
settings.frontButtonLeft = FRONT_HW_LEFT;
|
|
settings.frontButtonRight = FRONT_HW_RIGHT;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
uint8_t CrossPointSettings::sleepTimeoutEnumToMinutes(const uint8_t legacyValue) {
|
|
switch (legacyValue) {
|
|
case SLEEP_1_MIN:
|
|
return 1;
|
|
case SLEEP_5_MIN:
|
|
return 5;
|
|
case SLEEP_15_MIN:
|
|
return 15;
|
|
case SLEEP_30_MIN:
|
|
return 30;
|
|
case SLEEP_10_MIN:
|
|
default:
|
|
return 10;
|
|
}
|
|
}
|
|
|
|
bool CrossPointSettings::saveToFile() const {
|
|
Storage.mkdir("/.crosspoint");
|
|
return JsonSettingsIO::saveSettings(*this, SETTINGS_FILE_JSON);
|
|
}
|
|
|
|
bool CrossPointSettings::loadFromFile() {
|
|
// Try JSON first
|
|
if (Storage.exists(SETTINGS_FILE_JSON)) {
|
|
String json = Storage.readFile(SETTINGS_FILE_JSON);
|
|
if (!json.isEmpty()) {
|
|
bool resave = false;
|
|
bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave);
|
|
if (result && resave) {
|
|
if (saveToFile()) {
|
|
LOG_DBG("CPS", "Resaved settings to update format");
|
|
} else {
|
|
LOG_ERR("CPS", "Failed to resave settings after format update");
|
|
}
|
|
}
|
|
migrateLanguageBinaryFile();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Fall back to binary migration
|
|
if (Storage.exists(SETTINGS_FILE_BIN)) {
|
|
if (loadFromBinaryFile()) {
|
|
migrateLanguageBinaryFile();
|
|
if (saveToFile()) {
|
|
Storage.rename(SETTINGS_FILE_BIN, SETTINGS_FILE_BAK);
|
|
LOG_DBG("CPS", "Migrated settings.bin to settings.json");
|
|
return true;
|
|
} else {
|
|
LOG_ERR("CPS", "Failed to save migrated settings to JSON");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// No settings files at all -- check for standalone language.bin
|
|
return migrateLanguageBinaryFile();
|
|
}
|
|
|
|
bool CrossPointSettings::migrateLanguageBinaryFile() {
|
|
// V1_LANGUAGES / V1_LANGUAGE_COUNT are emitted by gen_i18n.py with the
|
|
// frozen enum order from 2f969a9.
|
|
if (!Storage.exists(LANG_FILE_BIN)) return false;
|
|
|
|
HalFile f;
|
|
if (Storage.openFileForRead("CPS", LANG_FILE_BIN, f)) {
|
|
uint8_t version;
|
|
serialization::readPod(f, version);
|
|
if (version == 1) {
|
|
uint8_t oldIndex;
|
|
serialization::readPod(f, oldIndex);
|
|
if (oldIndex < V1_LANGUAGE_COUNT) {
|
|
language = static_cast<uint8_t>(V1_LANGUAGES[oldIndex]);
|
|
}
|
|
}
|
|
}
|
|
Storage.rename(LANG_FILE_BIN, LANG_FILE_BAK);
|
|
saveToFile();
|
|
LOG_DBG("CPS", "Migrated language.bin into settings.json");
|
|
return true;
|
|
}
|
|
|
|
bool CrossPointSettings::loadFromBinaryFile() {
|
|
HalFile inputFile;
|
|
if (!Storage.openFileForRead("CPS", SETTINGS_FILE_BIN, inputFile)) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t version;
|
|
serialization::readPod(inputFile, version);
|
|
if (version != SETTINGS_FILE_VERSION) {
|
|
LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version);
|
|
return false;
|
|
}
|
|
|
|
uint8_t fileSettingsCount = 0;
|
|
serialization::readPod(inputFile, fileSettingsCount);
|
|
|
|
uint8_t settingsRead = 0;
|
|
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); // legacy
|
|
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;
|
|
uint8_t legacySleepTimeout = SLEEP_10_MIN;
|
|
readAndValidate(inputFile, legacySleepTimeout, SLEEP_TIMEOUT_COUNT);
|
|
sleepTimeoutMinutes = sleepTimeoutEnumToMinutes(legacySleepTimeout);
|
|
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;
|
|
readAndValidate(inputFile, longPressButtonBehavior, LONG_PRESS_BUTTON_BEHAVIOR_COUNT);
|
|
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;
|
|
serialization::readPod(inputFile, frontButtonFollowOrientation);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
} while (false);
|
|
|
|
if (frontButtonMappingRead) {
|
|
CrossPointSettings::validateFrontButtonMapping(*this);
|
|
} else {
|
|
applyLegacyFrontButtonLayout(*this);
|
|
}
|
|
|
|
LOG_DBG("CPS", "Settings loaded from binary file");
|
|
return true;
|
|
}
|
|
|
|
float CrossPointSettings::getReaderLineCompression() const {
|
|
// SD card fonts use same compression as Bookerly (the most neutral values)
|
|
if (sdFontFamilyName[0] != '\0') {
|
|
switch (lineSpacing) {
|
|
case TIGHT:
|
|
return 0.95f;
|
|
case NORMAL:
|
|
default:
|
|
return 1.0f;
|
|
case WIDE:
|
|
return 1.1f;
|
|
}
|
|
}
|
|
|
|
switch (fontFamily) {
|
|
case NOTOSERIF:
|
|
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 {
|
|
if (sleepTimeoutMinutes >= SLEEP_TIMEOUT_NEVER_MINUTES) return 0UL;
|
|
const uint8_t minutes =
|
|
std::clamp(sleepTimeoutMinutes, MIN_SLEEP_TIMEOUT_MINUTES, static_cast<uint8_t>(SLEEP_TIMEOUT_NEVER_MINUTES - 1));
|
|
return static_cast<unsigned long>(minutes) * 60UL * 1000UL;
|
|
}
|
|
|
|
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 {
|
|
// Check SD card font first
|
|
if (sdFontFamilyName[0] != '\0' && sdFontIdResolver) {
|
|
int id = sdFontIdResolver(sdFontResolverCtx, sdFontFamilyName, fontSize);
|
|
if (id != 0) return id;
|
|
// Fall through to built-in if SD font not found
|
|
}
|
|
|
|
switch (fontFamily) {
|
|
case NOTOSERIF:
|
|
default:
|
|
switch (fontSize) {
|
|
case SMALL:
|
|
return NOTOSERIF_12_FONT_ID;
|
|
case MEDIUM:
|
|
default:
|
|
return NOTOSERIF_14_FONT_ID;
|
|
case LARGE:
|
|
return NOTOSERIF_16_FONT_ID;
|
|
case EXTRA_LARGE:
|
|
return NOTOSERIF_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;
|
|
}
|
|
}
|
|
}
|