From 3e71e74c668d449fc0836b7f191dad5d7179a11c Mon Sep 17 00:00:00 2001 From: dpoulter Date: Thu, 8 Jan 2026 17:38:24 +0100 Subject: [PATCH 01/10] Add sleep BMP selection feature - Add selectedSleepBmp field to CrossPointSettings to store selected BMP filename - Create SleepBmpSelectionActivity for selecting BMP files from /sleep folder - Add conditional 'Select Sleep BMP' setting that only shows when /sleep folder has BMPs - Modify SleepActivity to use selected BMP instead of random selection when set - Cache sleep BMP check result to avoid mutex conflicts during rendering --- src/CrossPointSettings.cpp | 10 +- src/CrossPointSettings.h | 2 + src/activities/boot_sleep/SleepActivity.cpp | 21 +- src/activities/settings/SettingsActivity.cpp | 92 +++++++-- src/activities/settings/SettingsActivity.h | 1 + .../settings/SleepBmpSelectionActivity.cpp | 191 ++++++++++++++++++ .../settings/SleepBmpSelectionActivity.h | 34 ++++ 7 files changed, 334 insertions(+), 17 deletions(-) create mode 100644 src/activities/settings/SleepBmpSelectionActivity.cpp create mode 100644 src/activities/settings/SleepBmpSelectionActivity.h diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index cd8b56f7..80ced05e 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 17; +constexpr uint8_t SETTINGS_COUNT = 18; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -46,6 +46,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, sleepScreenCoverMode); serialization::writeString(outputFile, std::string(opdsServerUrl)); serialization::writePod(outputFile, textAntiAliasing); + serialization::writeString(outputFile, std::string(selectedSleepBmp)); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -110,6 +111,13 @@ bool CrossPointSettings::loadFromFile() { } serialization::readPod(inputFile, textAntiAliasing); if (++settingsRead >= fileSettingsCount) break; + { + std::string bmpStr; + serialization::readString(inputFile, bmpStr); + strncpy(selectedSleepBmp, bmpStr.c_str(), sizeof(selectedSleepBmp) - 1); + selectedSleepBmp[sizeof(selectedSleepBmp) - 1] = '\0'; + } + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 3a2a3503..b79d4e97 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -82,6 +82,8 @@ class CrossPointSettings { uint8_t screenMargin = 5; // OPDS browser settings char opdsServerUrl[128] = ""; + // Selected sleep BMP filename (empty means random selection) + char selectedSleepBmp[256] = ""; ~CrossPointSettings() = default; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 43e8e60b..f025fa2a 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -45,12 +45,28 @@ void SleepActivity::renderPopup(const char* message) const { } void SleepActivity::renderCustomSleepScreen() const { - // Check if we have a /sleep directory auto dir = SdMan.open("/sleep"); if (dir && dir.isDirectory()) { + if (SETTINGS.selectedSleepBmp[0] != '\0') { + const std::string selectedFile = std::string(SETTINGS.selectedSleepBmp); + const std::string filename = "/sleep/" + selectedFile; + FsFile file; + if (SdMan.openFileForRead("SLP", filename, file)) { + Bitmap bitmap(file); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + Serial.printf("[%lu] [SLP] Loading selected: /sleep/%s\n", millis(), selectedFile.c_str()); + delay(100); + renderBitmapSleepScreen(bitmap); + dir.close(); + return; + } + file.close(); + } + Serial.printf("[%lu] [SLP] Selected BMP not found or invalid, falling back to random\n", millis()); + } + std::vector files; char name[500]; - // collect all valid BMP files for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { if (file.isDirectory()) { file.close(); @@ -79,7 +95,6 @@ void SleepActivity::renderCustomSleepScreen() const { } const auto numFiles = files.size(); if (numFiles > 0) { - // Generate a random number between 1 and numFiles const auto randomFileIndex = random(numFiles); const auto filename = "/sleep/" + files[randomFileIndex]; FsFile file; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 702db172..7dd491d2 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -5,19 +5,23 @@ #include +#include + #include "CalibreSettingsActivity.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" #include "OtaUpdateActivity.h" +#include "SleepBmpSelectionActivity.h" #include "fontIds.h" // Define the static settings list namespace { -constexpr int settingsCount = 18; +constexpr int settingsCount = 19; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), + SettingInfo::Action("Select Sleep BMP"), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), @@ -43,6 +47,40 @@ const SettingInfo settingsList[settingsCount] = { SettingInfo::Action("Check for updates")}; } // namespace +namespace { +bool checkSleepBmps() { + auto dir = SdMan.open("/sleep"); + if (!dir || !dir.isDirectory()) { + if (dir) dir.close(); + return false; + } + + dir.rewindDirectory(); + char name[500]; + bool foundBmp = false; + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + if (file.isDirectory()) { + file.close(); + continue; + } + file.getName(name, sizeof(name)); + auto filename = std::string(name); + if (filename[0] == '.') { + file.close(); + continue; + } + + if (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".bmp") { + foundBmp = true; + } + file.close(); + if (foundBmp) break; + } + dir.close(); + return foundBmp; +} +} // namespace + void SettingsActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); @@ -52,10 +90,9 @@ void SettingsActivity::onEnter() { Activity::onEnter(); renderingMutex = xSemaphoreCreateMutex(); - // Reset selection to first item - selectedSettingIndex = 0; + hasSleepBmpsCached = checkSleepBmps(); - // Trigger first update + selectedSettingIndex = 0; updateRequired = true; xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask", @@ -99,26 +136,30 @@ void SettingsActivity::loop() { } // Handle navigation + const int visibleCount = hasSleepBmpsCached ? settingsCount : settingsCount - 1; if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { // Move selection up (with wrap-around) - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); + selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (visibleCount - 1); updateRequired = true; } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { // Move selection down (with wrap around) - selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; + selectedSettingIndex = (selectedSettingIndex < visibleCount - 1) ? (selectedSettingIndex + 1) : 0; updateRequired = true; } } void SettingsActivity::toggleCurrentSetting() { - // Validate index - if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { + int listIndex = selectedSettingIndex; + if (!hasSleepBmpsCached && selectedSettingIndex >= 2) { + listIndex = selectedSettingIndex + 1; + } + if (listIndex < 0 || listIndex >= settingsCount) { return; } - const auto& setting = settingsList[selectedSettingIndex]; + const auto& setting = settingsList[listIndex]; if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { // Toggle the boolean value using the member pointer @@ -153,6 +194,14 @@ void SettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Select Sleep BMP") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new SleepBmpSelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } } else { // Only toggle if it's a toggle type and has a value pointer @@ -184,15 +233,21 @@ void SettingsActivity::render() const { // Draw header renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD); + const int visibleCount = hasSleepBmpsCached ? settingsCount : settingsCount - 1; + // Draw selection renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); - // Draw all settings + int visibleIndex = 0; for (int i = 0; i < settingsCount; i++) { - const int settingY = 60 + i * 30; // 30 pixels between settings + if (i == 2 && !hasSleepBmpsCached) { + continue; + } + + const int settingY = 60 + visibleIndex * 30; // 30 pixels between settings // Draw setting name - renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, i != selectedSettingIndex); + renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, visibleIndex != selectedSettingIndex); // Draw value based on setting type std::string valueText = ""; @@ -204,9 +259,20 @@ void SettingsActivity::render() const { valueText = settingsList[i].enumValues[value]; } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); + } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Select Sleep BMP") == 0) { + if (SETTINGS.selectedSleepBmp[0] != '\0') { + valueText = SETTINGS.selectedSleepBmp; + if (valueText.length() > 20) { + valueText = valueText.substr(0, 17) + "..."; + } + } else { + valueText = "Random"; + } } const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex); + renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), visibleIndex != selectedSettingIndex); + + visibleIndex++; } // Draw version text above button hints diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 157689e3..8a469b6b 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -49,6 +49,7 @@ class SettingsActivity final : public ActivityWithSubactivity { SemaphoreHandle_t renderingMutex = nullptr; bool updateRequired = false; int selectedSettingIndex = 0; // Currently selected setting + bool hasSleepBmpsCached = false; // Cached result of sleep BMP check const std::function onGoHome; static void taskTrampoline(void* param); diff --git a/src/activities/settings/SleepBmpSelectionActivity.cpp b/src/activities/settings/SleepBmpSelectionActivity.cpp new file mode 100644 index 00000000..c6095f28 --- /dev/null +++ b/src/activities/settings/SleepBmpSelectionActivity.cpp @@ -0,0 +1,191 @@ +#include "SleepBmpSelectionActivity.h" + +#include +#include + +#include +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "fontIds.h" +#include "util/StringUtils.h" + +#include "../../../lib/GfxRenderer/Bitmap.h" + +namespace { +constexpr int PAGE_ITEMS = 23; +constexpr int SKIP_PAGE_MS = 700; +constexpr unsigned long IGNORE_INPUT_MS = 300; // Ignore input for 300ms after entering + +void sortFileList(std::vector& strs) { + std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { + return lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), + [](const char& char1, const char& char2) { + return tolower(char1) < tolower(char2); + }); + }); +} +} // namespace + +void SleepBmpSelectionActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void SleepBmpSelectionActivity::loadFiles() { + files.clear(); + + auto dir = SdMan.open("/sleep"); + if (!dir || !dir.isDirectory()) { + if (dir) dir.close(); + return; + } + + dir.rewindDirectory(); + + char name[500]; + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + if (file.isDirectory()) { + file.close(); + continue; + } + file.getName(name, sizeof(name)); + auto filename = std::string(name); + if (filename[0] == '.') { + file.close(); + continue; + } + + if (filename.substr(filename.length() - 4) != ".bmp") { + file.close(); + continue; + } + + Bitmap bitmap(file); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + file.close(); + continue; + } + file.close(); + + files.emplace_back(filename); + } + dir.close(); + sortFileList(files); +} + +void SleepBmpSelectionActivity::onEnter() { + Activity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + + loadFiles(); + selectorIndex = 0; + enterTime = millis(); + + updateRequired = true; + + xTaskCreate(&SleepBmpSelectionActivity::taskTrampoline, "SleepBmpSelectionActivityTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void SleepBmpSelectionActivity::onExit() { + Activity::onExit(); + + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; + files.clear(); +} + +void SleepBmpSelectionActivity::loop() { + const unsigned long timeSinceEnter = millis() - enterTime; + if (timeSinceEnter < IGNORE_INPUT_MS) { + return; + } + + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || + mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || + mappedInput.wasReleased(MappedInputManager::Button::Right); + + const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (files.empty()) { + return; + } + + const std::string selectedFile = files[selectorIndex]; + strncpy(SETTINGS.selectedSleepBmp, selectedFile.c_str(), sizeof(SETTINGS.selectedSleepBmp) - 1); + SETTINGS.selectedSleepBmp[sizeof(SETTINGS.selectedSleepBmp) - 1] = '\0'; + SETTINGS.saveToFile(); + + onBack(); + } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onBack(); + } else if (prevReleased) { + if (skipPage) { + selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + files.size()) % files.size(); + } else { + selectorIndex = (selectorIndex + files.size() - 1) % files.size(); + } + updateRequired = true; + } else if (nextReleased) { + if (skipPage) { + selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size(); + } else { + selectorIndex = (selectorIndex + 1) % files.size(); + } + updateRequired = true; + } +} + +void SleepBmpSelectionActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void SleepBmpSelectionActivity::render() const { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Sleep BMP", true, EpdFontFamily::BOLD); + + // Help text + const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + if (files.empty()) { + renderer.drawText(UI_10_FONT_ID, 20, 60, "No BMP files found in /sleep"); + renderer.displayBuffer(); + return; + } + + const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; + renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); + for (size_t i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40); + renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex); + } + + renderer.displayBuffer(); +} + diff --git a/src/activities/settings/SleepBmpSelectionActivity.h b/src/activities/settings/SleepBmpSelectionActivity.h new file mode 100644 index 00000000..f1737d05 --- /dev/null +++ b/src/activities/settings/SleepBmpSelectionActivity.h @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "../Activity.h" + +class SleepBmpSelectionActivity final : public Activity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + std::vector files; + size_t selectorIndex = 0; + bool updateRequired = false; + unsigned long enterTime = 0; // Time when activity was entered + const std::function onBack; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void loadFiles(); + + public: + explicit SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack) + : Activity("SleepBmpSelection", renderer, mappedInput), onBack(onBack) {} + void onEnter() override; + void onExit() override; + void loop() override; +}; + From a481ec1e1cdf8a9d03d81bb91beed975b8d70d00 Mon Sep 17 00:00:00 2001 From: dpoulter Date: Wed, 21 Jan 2026 16:03:22 +0200 Subject: [PATCH 02/10] feat: add sleep screen selection --- src/CrossPointSettings.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 2312ec06..90220033 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 20; +constexpr uint8_t SETTINGS_COUNT = 19; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -111,7 +111,6 @@ bool CrossPointSettings::loadFromFile() { 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; serialization::readPod(inputFile, hideBatteryPercentage); @@ -124,7 +123,6 @@ bool CrossPointSettings::loadFromFile() { strncpy(selectedSleepBmp, bmpStr.c_str(), sizeof(selectedSleepBmp) - 1); selectedSleepBmp[sizeof(selectedSleepBmp) - 1] = '\0'; } - ++settingsRead; // Final increment (should equal fileSettingsCount for current version) } while (false); inputFile.close(); From ab906c499bfa4407aa0e12a0ca5f010a5b39574f Mon Sep 17 00:00:00 2001 From: dpoulter Date: Wed, 21 Jan 2026 20:36:51 +0200 Subject: [PATCH 03/10] feat: add sleep screen selection --- src/CrossPointSettings.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 90220033..81160d03 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -75,54 +75,77 @@ bool CrossPointSettings::loadFromFile() { // load settings that exist (support older files with fewer fields) uint8_t settingsRead = 0; do { + Serial.printf("settingsRead: %d, fileSettingsCount: %d\n", settingsRead, fileSettingsCount); serialization::readPod(inputFile, sleepScreen); + Serial.printf("sleepScreen: %d\n", sleepScreen); if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, extraParagraphSpacing); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("extraParagraphSpacing: %d\n", extraParagraphSpacing); serialization::readPod(inputFile, shortPwrBtn); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("shortPwrBtn: %d\n", shortPwrBtn); serialization::readPod(inputFile, statusBar); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("statusBar: %d\n", statusBar); serialization::readPod(inputFile, orientation); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("orientation: %d\n", orientation); serialization::readPod(inputFile, frontButtonLayout); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("frontButtonLayout: %d\n", frontButtonLayout); serialization::readPod(inputFile, sideButtonLayout); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("sideButtonLayout: %d\n", sideButtonLayout); serialization::readPod(inputFile, fontFamily); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("fontFamily: %d\n", fontFamily); serialization::readPod(inputFile, fontSize); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("fontSize: %d\n", fontSize); serialization::readPod(inputFile, lineSpacing); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("lineSpacing: %d\n", lineSpacing); serialization::readPod(inputFile, paragraphAlignment); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, sleepTimeout); + Serial.printf("paragraphAlignment: %d\n", paragraphAlignment); + serialization::readPod(inputFile, sleepTimeout); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("sleepTimeout: %d\n", sleepTimeout); serialization::readPod(inputFile, refreshFrequency); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("refreshFrequency: %d\n", refreshFrequency); serialization::readPod(inputFile, screenMargin); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("screenMargin: %d\n", screenMargin); serialization::readPod(inputFile, sleepScreenCoverMode); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("sleepScreenCoverMode: %d\n", sleepScreenCoverMode); { std::string urlStr; serialization::readString(inputFile, urlStr); strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1); opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0'; } + if (++settingsRead >= fileSettingsCount) break; + Serial.printf("textAntiAliasing: %d\n", textAntiAliasing); serialization::readPod(inputFile, textAntiAliasing); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("hideBatteryPercentage: %d\n", hideBatteryPercentage); serialization::readPod(inputFile, hideBatteryPercentage); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("longPressChapterSkip: %d\n", longPressChapterSkip); serialization::readPod(inputFile, longPressChapterSkip); if (++settingsRead >= fileSettingsCount) break; + Serial.printf("selectedSleepBmp: %s\n", selectedSleepBmp); { std::string bmpStr; serialization::readString(inputFile, bmpStr); strncpy(selectedSleepBmp, bmpStr.c_str(), sizeof(selectedSleepBmp) - 1); selectedSleepBmp[sizeof(selectedSleepBmp) - 1] = '\0'; } + if (++settingsRead >= fileSettingsCount) break; + Serial.printf("settingsRead: %d, fileSettingsCount: %d\n", settingsRead, fileSettingsCount); } while (false); inputFile.close(); From 5c739fa530bdad6cef37f73c0096dd9e11117e32 Mon Sep 17 00:00:00 2001 From: dpoulter Date: Fri, 23 Jan 2026 08:26:16 +0200 Subject: [PATCH 04/10] feat: add sleep screen selection --- .../settings/CategorySettingsActivity.cpp | 135 +++++++++++++-- .../settings/CategorySettingsActivity.h | 4 + .../settings/SleepBmpSelectionActivity.cpp | 161 ++++++++++++------ .../settings/SleepBmpSelectionActivity.h | 4 +- 4 files changed, 241 insertions(+), 63 deletions(-) diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index a6182b5c..46e934eb 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -11,6 +11,7 @@ #include "KOReaderSettingsActivity.h" #include "MappedInputManager.h" #include "OtaUpdateActivity.h" +#include "SleepBmpSelectionActivity.h" #include "fontIds.h" void CategorySettingsActivity::taskTrampoline(void* param) { @@ -61,24 +62,40 @@ void CategorySettingsActivity::loop() { return; } - // Handle navigation + // Handle navigation (skip hidden settings) + const int visibleCount = getVisibleSettingsCount(); + if (visibleCount == 0) { + return; // No visible settings + } + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1); + // Move to previous visible setting + int currentActual = mapVisibleIndexToActualIndex(selectedSettingIndex); + do { + currentActual = (currentActual > 0) ? (currentActual - 1) : (settingsCount - 1); + } while (!shouldShowSetting(currentActual) && currentActual != mapVisibleIndexToActualIndex(selectedSettingIndex)); + selectedSettingIndex = mapActualIndexToVisibleIndex(currentActual); updateRequired = true; } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || mappedInput.wasPressed(MappedInputManager::Button::Right)) { - selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; + // Move to next visible setting + int currentActual = mapVisibleIndexToActualIndex(selectedSettingIndex); + do { + currentActual = (currentActual < settingsCount - 1) ? (currentActual + 1) : 0; + } while (!shouldShowSetting(currentActual) && currentActual != mapVisibleIndexToActualIndex(selectedSettingIndex)); + selectedSettingIndex = mapActualIndexToVisibleIndex(currentActual); updateRequired = true; } } void CategorySettingsActivity::toggleCurrentSetting() { - if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { + const int actualIndex = mapVisibleIndexToActualIndex(selectedSettingIndex); + if (actualIndex < 0 || actualIndex >= settingsCount) { return; } - const auto& setting = settingsList[selectedSettingIndex]; + const auto& setting = settingsList[actualIndex]; if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { // Toggle the boolean value using the member pointer @@ -87,6 +104,16 @@ void CategorySettingsActivity::toggleCurrentSetting() { } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { const uint8_t currentValue = SETTINGS.*(setting.valuePtr); SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); + + // If sleep screen changed away from CUSTOM, adjust selection if needed + if (setting.valuePtr == &CrossPointSettings::sleepScreen) { + const int visibleCount = getVisibleSettingsCount(); + // If current selection is now hidden or out of bounds, adjust it + const int currentActual = mapVisibleIndexToActualIndex(selectedSettingIndex); + if (!shouldShowSetting(currentActual) || selectedSettingIndex >= visibleCount) { + selectedSettingIndex = visibleCount > 0 ? visibleCount - 1 : 0; + } + } } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { const int8_t currentValue = SETTINGS.*(setting.valuePtr); if (currentValue + setting.valueRange.step > setting.valueRange.max) { @@ -127,6 +154,14 @@ void CategorySettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Select Sleep BMP") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new SleepBmpSelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } } else { return; @@ -147,6 +182,57 @@ void CategorySettingsActivity::displayTaskLoop() { } } +bool CategorySettingsActivity::shouldShowSetting(int index) const { + if (index < 0 || index >= settingsCount) { + return false; + } + // Hide "Select Sleep BMP" if sleep screen is not set to CUSTOM + if (settingsList[index].type == SettingType::ACTION && + strcmp(settingsList[index].name, "Select Sleep BMP") == 0) { + return SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; + } + return true; +} + +int CategorySettingsActivity::getVisibleSettingsCount() const { + int count = 0; + for (int i = 0; i < settingsCount; i++) { + if (shouldShowSetting(i)) { + count++; + } + } + return count; +} + +int CategorySettingsActivity::mapVisibleIndexToActualIndex(int visibleIndex) const { + int visibleCount = 0; + for (int i = 0; i < settingsCount; i++) { + if (shouldShowSetting(i)) { + if (visibleCount == visibleIndex) { + return i; + } + visibleCount++; + } + } + // If visibleIndex is out of bounds, return first visible setting + for (int i = 0; i < settingsCount; i++) { + if (shouldShowSetting(i)) { + return i; + } + } + return 0; // Fallback +} + +int CategorySettingsActivity::mapActualIndexToVisibleIndex(int actualIndex) const { + int visibleIndex = 0; + for (int i = 0; i < actualIndex; i++) { + if (shouldShowSetting(i)) { + visibleIndex++; + } + } + return visibleIndex; +} + void CategorySettingsActivity::render() const { renderer.clearScreen(); @@ -155,13 +241,31 @@ void CategorySettingsActivity::render() const { renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); + // Calculate visible settings count and map selection + const int visibleCount = getVisibleSettingsCount(); + const int actualSelectedIndex = mapVisibleIndexToActualIndex(selectedSettingIndex); + // Draw selection highlight - renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); - - // Draw all settings + int visibleIndex = 0; for (int i = 0; i < settingsCount; i++) { - const int settingY = 60 + i * 30; // 30 pixels between settings - const bool isSelected = (i == selectedSettingIndex); + if (shouldShowSetting(i)) { + if (i == actualSelectedIndex) { + renderer.fillRect(0, 60 + visibleIndex * 30 - 2, pageWidth - 1, 30); + break; + } + visibleIndex++; + } + } + + // Draw all visible settings + visibleIndex = 0; + for (int i = 0; i < settingsCount; i++) { + if (!shouldShowSetting(i)) { + continue; + } + + const int settingY = 60 + visibleIndex * 30; // 30 pixels between settings + const bool isSelected = (i == actualSelectedIndex); // Draw setting name renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); @@ -176,11 +280,22 @@ void CategorySettingsActivity::render() const { valueText = settingsList[i].enumValues[value]; } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); + } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Select Sleep BMP") == 0) { + if (SETTINGS.selectedSleepBmp[0] != '\0') { + valueText = SETTINGS.selectedSleepBmp; + if (valueText.length() > 20) { + valueText = valueText.substr(0, 17) + "..."; + } + } else { + valueText = "Random"; + } } if (!valueText.empty()) { const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected); } + + visibleIndex++; } renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), diff --git a/src/activities/settings/CategorySettingsActivity.h b/src/activities/settings/CategorySettingsActivity.h index a7d1f0ce..b31dc1e7 100644 --- a/src/activities/settings/CategorySettingsActivity.h +++ b/src/activities/settings/CategorySettingsActivity.h @@ -55,6 +55,10 @@ class CategorySettingsActivity final : public ActivityWithSubactivity { [[noreturn]] void displayTaskLoop(); void render() const; void toggleCurrentSetting(); + bool shouldShowSetting(int index) const; + int getVisibleSettingsCount() const; + int mapVisibleIndexToActualIndex(int visibleIndex) const; + int mapActualIndexToVisibleIndex(int actualIndex) const; public: CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName, diff --git a/src/activities/settings/SleepBmpSelectionActivity.cpp b/src/activities/settings/SleepBmpSelectionActivity.cpp index c6095f28..fd93976c 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.cpp +++ b/src/activities/settings/SleepBmpSelectionActivity.cpp @@ -14,16 +14,18 @@ #include "../../../lib/GfxRenderer/Bitmap.h" namespace { -constexpr int PAGE_ITEMS = 23; constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long IGNORE_INPUT_MS = 300; // Ignore input for 300ms after entering +constexpr int LINE_HEIGHT = 30; +constexpr int START_Y = 60; +constexpr int BOTTOM_BAR_HEIGHT = 60; // Space for button hints void sortFileList(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { - return lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), - [](const char& char1, const char& char2) { - return tolower(char1) < tolower(char2); - }); + return std::lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), + [](const char& char1, const char& char2) { + return std::tolower(char1) < std::tolower(char2); + }); }); } } // namespace @@ -35,44 +37,48 @@ void SleepBmpSelectionActivity::taskTrampoline(void* param) { void SleepBmpSelectionActivity::loadFiles() { files.clear(); - + + std::vector bmpFiles; + auto dir = SdMan.open("/sleep"); - if (!dir || !dir.isDirectory()) { - if (dir) dir.close(); - return; + if (dir && dir.isDirectory()) { + dir.rewindDirectory(); + char name[500]; + + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + if (file.isDirectory()) { + file.close(); + continue; + } + + file.getName(name, sizeof(name)); + auto filename = std::string(name); + + if (filename[0] == '.' || filename.length() < 4 || + filename.substr(filename.length() - 4) != ".bmp") { + file.close(); + continue; + } + + // Validate BMP + Bitmap bitmap(file); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + file.close(); + continue; + } + file.close(); + + bmpFiles.emplace_back(filename); + } + dir.close(); + + // Sort alphabetically (case-insensitive) + sortFileList(bmpFiles); } - - dir.rewindDirectory(); - - char name[500]; - for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { - if (file.isDirectory()) { - file.close(); - continue; - } - file.getName(name, sizeof(name)); - auto filename = std::string(name); - if (filename[0] == '.') { - file.close(); - continue; - } - - if (filename.substr(filename.length() - 4) != ".bmp") { - file.close(); - continue; - } - - Bitmap bitmap(file); - if (bitmap.parseHeaders() != BmpReaderError::Ok) { - file.close(); - continue; - } - file.close(); - - files.emplace_back(filename); - } - dir.close(); - sortFileList(files); + + // Add "Random" as first option, then sorted BMP files + files.emplace_back("Random"); + files.insert(files.end(), bmpFiles.begin(), bmpFiles.end()); } void SleepBmpSelectionActivity::onEnter() { @@ -81,7 +87,21 @@ void SleepBmpSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); loadFiles(); - selectorIndex = 0; + + // Set initial selection: "Random" if no file selected, otherwise find the selected file + if (SETTINGS.selectedSleepBmp[0] == '\0') { + selectorIndex = 0; // "Random" is at index 0 + } else { + // Find the selected file in the sorted list + selectorIndex = 0; // Default to "Random" if not found + for (size_t i = 1; i < files.size(); i++) { + if (files[i] == SETTINGS.selectedSleepBmp) { + selectorIndex = i; + break; + } + } + } + enterTime = millis(); updateRequired = true; @@ -122,28 +142,51 @@ void SleepBmpSelectionActivity::loop() { const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (files.empty()) { + if (files.empty() || selectorIndex >= files.size()) { return; } const std::string selectedFile = files[selectorIndex]; - strncpy(SETTINGS.selectedSleepBmp, selectedFile.c_str(), sizeof(SETTINGS.selectedSleepBmp) - 1); - SETTINGS.selectedSleepBmp[sizeof(SETTINGS.selectedSleepBmp) - 1] = '\0'; + if (selectedFile == "Random") { + // Clear the selection to use random + SETTINGS.selectedSleepBmp[0] = '\0'; + } else { + strncpy(SETTINGS.selectedSleepBmp, selectedFile.c_str(), sizeof(SETTINGS.selectedSleepBmp) - 1); + SETTINGS.selectedSleepBmp[sizeof(SETTINGS.selectedSleepBmp) - 1] = '\0'; + } SETTINGS.saveToFile(); onBack(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onBack(); } else if (prevReleased) { + if (files.empty()) { + return; + } + + // Calculate items per page dynamically + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + files.size()) % files.size(); + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + files.size()) % files.size(); } else { selectorIndex = (selectorIndex + files.size() - 1) % files.size(); } updateRequired = true; } else if (nextReleased) { + if (files.empty()) { + return; + } + + // Calculate items per page dynamically + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size(); + selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % files.size(); } else { selectorIndex = (selectorIndex + 1) % files.size(); } @@ -179,11 +222,27 @@ void SleepBmpSelectionActivity::render() const { return; } - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - for (size_t i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) { + // Calculate items per page based on screen height + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + + // Calculate page start index + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + + // Draw selection highlight + const int visibleSelectedIndex = static_cast(selectorIndex - pageStartIndex); + if (visibleSelectedIndex >= 0 && visibleSelectedIndex < pageItems && selectorIndex < files.size()) { + renderer.fillRect(0, START_Y + visibleSelectedIndex * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT); + } + + // Draw visible files + int visibleIndex = 0; + for (size_t i = pageStartIndex; i < files.size() && visibleIndex < pageItems; i++) { auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex); + const bool isSelected = (i == selectorIndex); + renderer.drawText(UI_10_FONT_ID, 20, START_Y + visibleIndex * LINE_HEIGHT, item.c_str(), !isSelected); + visibleIndex++; } renderer.displayBuffer(); diff --git a/src/activities/settings/SleepBmpSelectionActivity.h b/src/activities/settings/SleepBmpSelectionActivity.h index f1737d05..f9906a39 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.h +++ b/src/activities/settings/SleepBmpSelectionActivity.h @@ -12,7 +12,7 @@ class SleepBmpSelectionActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; - std::vector files; + std::vector files; // Sorted list of valid BMP filenames ("Random" at index 0) size_t selectorIndex = 0; bool updateRequired = false; unsigned long enterTime = 0; // Time when activity was entered @@ -21,7 +21,7 @@ class SleepBmpSelectionActivity final : public Activity { static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render() const; - void loadFiles(); + void loadFiles(); // Load and sort all valid BMP files public: explicit SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, From 77ae2a8dc0bf150392877688f5748ed1963332ab Mon Sep 17 00:00:00 2001 From: dpoulter Date: Tue, 27 Jan 2026 09:47:38 +0200 Subject: [PATCH 05/10] feat: add sleep screen selection --- src/CrossPointSettings.cpp | 42 ++++ src/CrossPointSettings.h | 8 + src/activities/ListSelectionActivity.cpp | 162 +++++++++++++++ src/activities/ListSelectionActivity.h | 88 ++++++++ .../settings/CategorySettingsActivity.cpp | 55 ++++- .../RefreshFrequencySelectionActivity.cpp | 37 ++++ .../RefreshFrequencySelectionActivity.h | 17 ++ .../ScreenMarginSelectionActivity.cpp | 47 +++++ .../settings/ScreenMarginSelectionActivity.h | 17 ++ src/activities/settings/SettingsActivity.cpp | 10 +- .../settings/SleepBmpSelectionActivity.cpp | 188 +++--------------- .../settings/SleepBmpSelectionActivity.h | 26 +-- .../settings/SleepScreenSelectionActivity.cpp | 37 ++++ .../settings/SleepScreenSelectionActivity.h | 17 ++ .../SleepTimeoutSelectionActivity.cpp | 37 ++++ .../settings/SleepTimeoutSelectionActivity.h | 17 ++ 16 files changed, 607 insertions(+), 198 deletions(-) create mode 100644 src/activities/ListSelectionActivity.cpp create mode 100644 src/activities/ListSelectionActivity.h create mode 100644 src/activities/settings/RefreshFrequencySelectionActivity.cpp create mode 100644 src/activities/settings/RefreshFrequencySelectionActivity.h create mode 100644 src/activities/settings/ScreenMarginSelectionActivity.cpp create mode 100644 src/activities/settings/ScreenMarginSelectionActivity.h create mode 100644 src/activities/settings/SleepScreenSelectionActivity.cpp create mode 100644 src/activities/settings/SleepScreenSelectionActivity.h create mode 100644 src/activities/settings/SleepTimeoutSelectionActivity.cpp create mode 100644 src/activities/settings/SleepTimeoutSelectionActivity.h diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index cebe1e23..87ede84b 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -244,3 +244,45 @@ int CrossPointSettings::getReaderFontId() const { } } } + +const char* CrossPointSettings::getRefreshFrequencyString(uint8_t value) { + static const char* options[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}; + static constexpr size_t count = sizeof(options) / sizeof(options[0]); + if (value < count) { + return options[value]; + } + return options[REFRESH_15]; // Default +} + +size_t CrossPointSettings::getRefreshFrequencyCount() { + static const char* options[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}; + return sizeof(options) / sizeof(options[0]); +} + +const char* CrossPointSettings::getSleepScreenString(uint8_t value) { + static const char* options[] = {"Dark", "Light", "Custom", "Cover", "None"}; + static constexpr size_t count = sizeof(options) / sizeof(options[0]); + if (value < count) { + return options[value]; + } + return options[DARK]; // Default +} + +size_t CrossPointSettings::getSleepScreenCount() { + static const char* options[] = {"Dark", "Light", "Custom", "Cover", "None"}; + return sizeof(options) / sizeof(options[0]); +} + +const char* CrossPointSettings::getSleepTimeoutString(uint8_t value) { + static const char* options[] = {"1 min", "5 min", "10 min", "15 min", "30 min"}; + static constexpr size_t count = sizeof(options) / sizeof(options[0]); + if (value < count) { + return options[value]; + } + return options[SLEEP_10_MIN]; // Default +} + +size_t CrossPointSettings::getSleepTimeoutCount() { + static const char* options[] = {"1 min", "5 min", "10 min", "15 min", "30 min"}; + return sizeof(options) / sizeof(options[0]); +} diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 0ea890ab..21509197 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -110,6 +110,14 @@ class CrossPointSettings { bool saveToFile() const; bool loadFromFile(); + // Helper functions to get option strings from enum values + static const char* getRefreshFrequencyString(uint8_t value); + static size_t getRefreshFrequencyCount(); + static const char* getSleepScreenString(uint8_t value); + static size_t getSleepScreenCount(); + static const char* getSleepTimeoutString(uint8_t value); + static size_t getSleepTimeoutCount(); + float getReaderLineCompression() const; unsigned long getSleepTimeoutMs() const; int getRefreshFrequency() const; diff --git a/src/activities/ListSelectionActivity.cpp b/src/activities/ListSelectionActivity.cpp new file mode 100644 index 00000000..e562a441 --- /dev/null +++ b/src/activities/ListSelectionActivity.cpp @@ -0,0 +1,162 @@ +#include "ListSelectionActivity.h" + +#include + +#include "MappedInputManager.h" +#include "fontIds.h" + +void ListSelectionActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +int ListSelectionActivity::getPageItems() const { + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + return pageItems > 0 ? pageItems : 1; +} + +void ListSelectionActivity::onEnter() { + Activity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + enterTime = millis(); + + // Load items (allows subclasses to populate data) + loadItems(); + + // Ensure selector index is valid + const size_t itemCount = getItemCount(); + if (selectorIndex >= itemCount && itemCount > 0) { + selectorIndex = 0; + } + + updateRequired = true; + + xTaskCreate(&ListSelectionActivity::taskTrampoline, "ListSelectionTask", 2048, this, 1, + &displayTaskHandle); +} + +void ListSelectionActivity::onExit() { + Activity::onExit(); + + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void ListSelectionActivity::loop() { + const unsigned long timeSinceEnter = millis() - enterTime; + if (timeSinceEnter < IGNORE_INPUT_MS) { + return; + } + + const size_t itemCount = getItemCount(); + if (itemCount == 0) { + // Handle back button even when empty + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onBack(); + } + return; + } + + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || + mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || + mappedInput.wasReleased(MappedInputManager::Button::Right); + + const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + const int pageItems = getPageItems(); + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (selectorIndex < itemCount) { + onItemSelected(selectorIndex); + } + } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onBack(); + } else if (prevReleased) { + if (skipPage) { + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; + } else { + selectorIndex = (selectorIndex + itemCount - 1) % itemCount; + } + updateRequired = true; + } else if (nextReleased) { + if (skipPage) { + selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % itemCount; + } else { + selectorIndex = (selectorIndex + 1) % itemCount; + } + updateRequired = true; + } +} + +void ListSelectionActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void ListSelectionActivity::render() const { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD); + + // Help text + const auto labels = mappedInput.mapLabels(backLabel.c_str(), confirmLabel.c_str(), "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + const size_t itemCount = getItemCount(); + if (itemCount == 0) { + renderer.drawText(UI_10_FONT_ID, 20, START_Y, emptyMessage.c_str()); + renderer.displayBuffer(); + return; + } + + // Calculate items per page based on screen height + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; + const int pageItems = (availableHeight / LINE_HEIGHT); + + // Calculate page start index + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + + // Draw selection highlight + const int visibleSelectedIndex = static_cast(selectorIndex - pageStartIndex); + if (visibleSelectedIndex >= 0 && visibleSelectedIndex < pageItems && selectorIndex < itemCount) { + renderer.fillRect(0, START_Y + visibleSelectedIndex * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT); + } + + // Draw visible items + int visibleIndex = 0; + for (size_t i = pageStartIndex; i < itemCount && visibleIndex < pageItems; i++) { + const bool isSelected = (i == selectorIndex); + const int itemY = START_Y + visibleIndex * LINE_HEIGHT; + + if (customRenderItem) { + // Use custom renderer if provided + customRenderItem(i, 20, itemY, isSelected); + } else { + // Default rendering: truncate text and draw + const std::string itemText = getItemText(i); + auto truncated = renderer.truncatedText(UI_10_FONT_ID, itemText.c_str(), pageWidth - 40); + renderer.drawText(UI_10_FONT_ID, 20, itemY, truncated.c_str(), !isSelected); + } + visibleIndex++; + } + + renderer.displayBuffer(); +} diff --git a/src/activities/ListSelectionActivity.h b/src/activities/ListSelectionActivity.h new file mode 100644 index 00000000..e2595b07 --- /dev/null +++ b/src/activities/ListSelectionActivity.h @@ -0,0 +1,88 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "Activity.h" + +/** + * ListSelectionActivity is a reusable base class for activities that display + * a scrollable list of items with selection capabilities. + * + * Features: + * - Automatic pagination based on screen size + * - Page skipping when holding navigation buttons + * - Configurable title, empty message, and button labels + * - Customizable item rendering + */ +class ListSelectionActivity : public Activity { + protected: + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + size_t selectorIndex = 0; + bool updateRequired = false; + unsigned long enterTime = 0; + + // Configuration + std::string title; + std::string emptyMessage; + std::string backLabel; + std::string confirmLabel; + std::function getItemCount; + std::function getItemText; + std::function onItemSelected; + std::function onBack; + std::function customRenderItem; // index, x, y, isSelected + + // Constants + static constexpr int SKIP_PAGE_MS = 700; + static constexpr unsigned long IGNORE_INPUT_MS = 300; + static constexpr int LINE_HEIGHT = 30; + static constexpr int START_Y = 60; + static constexpr int BOTTOM_BAR_HEIGHT = 60; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + int getPageItems() const; + virtual void loadItems() {} // Override to load items on enter + + public: + explicit ListSelectionActivity(const std::string& activityName, GfxRenderer& renderer, + MappedInputManager& mappedInput, const std::string& title, + std::function getItemCount, + std::function getItemText, + std::function onItemSelected, + std::function onBack, + const std::string& emptyMessage = "No items available", + const std::string& backLabel = "« Back", + const std::string& confirmLabel = "Select") + : Activity(activityName, renderer, mappedInput), + title(title), + emptyMessage(emptyMessage), + backLabel(backLabel), + confirmLabel(confirmLabel), + getItemCount(getItemCount), + getItemText(getItemText), + onItemSelected(onItemSelected), + onBack(onBack) {} + + virtual ~ListSelectionActivity() = default; + + void onEnter() override; + void onExit() override; + void loop() override; + + // Allow subclasses to set initial selection + void setInitialSelection(size_t index) { selectorIndex = index; } + size_t getCurrentSelection() const { return selectorIndex; } + + // Allow custom item rendering + void setCustomItemRenderer(std::function renderer) { + customRenderItem = renderer; + } +}; diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 46e934eb..a69349bf 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -11,7 +11,11 @@ #include "KOReaderSettingsActivity.h" #include "MappedInputManager.h" #include "OtaUpdateActivity.h" +#include "RefreshFrequencySelectionActivity.h" +#include "ScreenMarginSelectionActivity.h" #include "SleepBmpSelectionActivity.h" +#include "SleepScreenSelectionActivity.h" +#include "SleepTimeoutSelectionActivity.h" #include "fontIds.h" void CategorySettingsActivity::taskTrampoline(void* param) { @@ -104,16 +108,6 @@ void CategorySettingsActivity::toggleCurrentSetting() { } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { const uint8_t currentValue = SETTINGS.*(setting.valuePtr); SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); - - // If sleep screen changed away from CUSTOM, adjust selection if needed - if (setting.valuePtr == &CrossPointSettings::sleepScreen) { - const int visibleCount = getVisibleSettingsCount(); - // If current selection is now hidden or out of bounds, adjust it - const int currentActual = mapVisibleIndexToActualIndex(selectedSettingIndex); - if (!shouldShowSetting(currentActual) || selectedSettingIndex >= visibleCount) { - selectedSettingIndex = visibleCount > 0 ? visibleCount - 1 : 0; - } - } } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { const int8_t currentValue = SETTINGS.*(setting.valuePtr); if (currentValue + setting.valueRange.step > setting.valueRange.max) { @@ -154,6 +148,38 @@ void CategorySettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Sleep Screen") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new SleepScreenSelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Refresh Frequency") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new RefreshFrequencySelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Screen Margin") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new ScreenMarginSelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Time to Sleep") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new SleepTimeoutSelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } else if (strcmp(setting.name, "Select Sleep BMP") == 0) { xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); @@ -280,6 +306,15 @@ void CategorySettingsActivity::render() const { valueText = settingsList[i].enumValues[value]; } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); + } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Sleep Screen") == 0) { + valueText = CrossPointSettings::getSleepScreenString(SETTINGS.sleepScreen); + } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Refresh Frequency") == 0) { + valueText = CrossPointSettings::getRefreshFrequencyString(SETTINGS.refreshFrequency); + } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Screen Margin") == 0) { + // Format margin value as "X px" + valueText = std::to_string(SETTINGS.screenMargin) + " px"; + } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Time to Sleep") == 0) { + valueText = CrossPointSettings::getSleepTimeoutString(SETTINGS.sleepTimeout); } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Select Sleep BMP") == 0) { if (SETTINGS.selectedSleepBmp[0] != '\0') { valueText = SETTINGS.selectedSleepBmp; diff --git a/src/activities/settings/RefreshFrequencySelectionActivity.cpp b/src/activities/settings/RefreshFrequencySelectionActivity.cpp new file mode 100644 index 00000000..c8205363 --- /dev/null +++ b/src/activities/settings/RefreshFrequencySelectionActivity.cpp @@ -0,0 +1,37 @@ +#include "RefreshFrequencySelectionActivity.h" + +#include + +#include "CrossPointSettings.h" + +RefreshFrequencySelectionActivity::RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack) + : ListSelectionActivity( + "RefreshFrequencySelection", renderer, mappedInput, "Select Refresh Frequency", + [this]() { return options.size(); }, + [this](size_t index) { return options[index]; }, + [this, onBack](size_t index) { + if (index >= options.size()) { + return; + } + // Map option index to enum value (index matches enum value) + SETTINGS.refreshFrequency = static_cast(index); + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No options available") { + // Initialize options from enum + for (uint8_t i = 0; i < CrossPointSettings::getRefreshFrequencyCount(); i++) { + options.push_back(CrossPointSettings::getRefreshFrequencyString(i)); + } +} + +void RefreshFrequencySelectionActivity::loadItems() { + // Options are already set in constructor, just set initial selection + // Map current enum value to option index + if (SETTINGS.refreshFrequency < options.size()) { + selectorIndex = SETTINGS.refreshFrequency; + } else { + selectorIndex = 3; // Default to "15 pages" (REFRESH_15) + } +} diff --git a/src/activities/settings/RefreshFrequencySelectionActivity.h b/src/activities/settings/RefreshFrequencySelectionActivity.h new file mode 100644 index 00000000..b4c31cb9 --- /dev/null +++ b/src/activities/settings/RefreshFrequencySelectionActivity.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include + +#include "../ListSelectionActivity.h" + +class RefreshFrequencySelectionActivity final : public ListSelectionActivity { + std::vector options; // Refresh frequency options + + protected: + void loadItems() override; // Called by base class onEnter + + public: + explicit RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack); +}; diff --git a/src/activities/settings/ScreenMarginSelectionActivity.cpp b/src/activities/settings/ScreenMarginSelectionActivity.cpp new file mode 100644 index 00000000..1e891d5c --- /dev/null +++ b/src/activities/settings/ScreenMarginSelectionActivity.cpp @@ -0,0 +1,47 @@ +#include "ScreenMarginSelectionActivity.h" + +#include +#include + +#include "CrossPointSettings.h" + +ScreenMarginSelectionActivity::ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack) + : ListSelectionActivity( + "ScreenMarginSelection", renderer, mappedInput, "Select Screen Margin", + [this]() { return options.size(); }, + [this](size_t index) { return options[index]; }, + [this, onBack](size_t index) { + if (index >= options.size()) { + return; + } + // Map option index to margin value + // Options: "5 px", "10 px", "15 px", "20 px", "25 px", "30 px", "35 px", "40 px" + // Values: 5, 10, 15, 20, 25, 30, 35, 40 + SETTINGS.screenMargin = static_cast((index + 1) * 5); + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No options available") { + // Initialize options: 5 to 40 in steps of 5 + for (int i = 5; i <= 40; i += 5) { + std::ostringstream oss; + oss << i << " px"; + options.push_back(oss.str()); + } +} + +void ScreenMarginSelectionActivity::loadItems() { + // Options are already set in constructor, just set initial selection + // Map current margin value to option index + // margin value / 5 - 1 = index (e.g., 5 -> 0, 10 -> 1, etc.) + if (SETTINGS.screenMargin >= 5 && SETTINGS.screenMargin <= 40) { + selectorIndex = (SETTINGS.screenMargin / 5) - 1; + // Ensure index is within bounds + if (selectorIndex >= options.size()) { + selectorIndex = 0; // Default to "5 px" + } + } else { + selectorIndex = 0; // Default to "5 px" + } +} diff --git a/src/activities/settings/ScreenMarginSelectionActivity.h b/src/activities/settings/ScreenMarginSelectionActivity.h new file mode 100644 index 00000000..8178ce92 --- /dev/null +++ b/src/activities/settings/ScreenMarginSelectionActivity.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include + +#include "../ListSelectionActivity.h" + +class ScreenMarginSelectionActivity final : public ListSelectionActivity { + std::vector options; // Screen margin options + + protected: + void loadItems() override; // Called by base class onEnter + + public: + explicit ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack); +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 33e15a5a..89e681cd 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -21,20 +21,19 @@ namespace { constexpr int displaySettingsCount = 6; const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE - SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), + SettingInfo::Action("Sleep Screen"), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Action("Select Sleep BMP"), 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"})}; + SettingInfo::Action("Refresh Frequency")}; 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::Action("Screen Margin"), SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right"}), SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), @@ -54,8 +53,7 @@ const SettingInfo controlsSettings[controlsSettingsCount] = { 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("Time to Sleep"), SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), SettingInfo::Action("Check for updates")}; } // namespace diff --git a/src/activities/settings/SleepBmpSelectionActivity.cpp b/src/activities/settings/SleepBmpSelectionActivity.cpp index fd93976c..15d9c4ef 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.cpp +++ b/src/activities/settings/SleepBmpSelectionActivity.cpp @@ -1,25 +1,15 @@ #include "SleepBmpSelectionActivity.h" -#include #include #include #include +#include #include "CrossPointSettings.h" -#include "MappedInputManager.h" -#include "fontIds.h" -#include "util/StringUtils.h" - #include "../../../lib/GfxRenderer/Bitmap.h" namespace { -constexpr int SKIP_PAGE_MS = 700; -constexpr unsigned long IGNORE_INPUT_MS = 300; // Ignore input for 300ms after entering -constexpr int LINE_HEIGHT = 30; -constexpr int START_Y = 60; -constexpr int BOTTOM_BAR_HEIGHT = 60; // Space for button hints - void sortFileList(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { return std::lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), @@ -30,10 +20,28 @@ void sortFileList(std::vector& strs) { } } // namespace -void SleepBmpSelectionActivity::taskTrampoline(void* param) { - auto* self = static_cast(param); - self->displayTaskLoop(); -} +SleepBmpSelectionActivity::SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack) + : ListSelectionActivity( + "SleepBmpSelection", renderer, mappedInput, "Select Sleep BMP", + [this]() { return files.size(); }, + [this](size_t index) { return files[index]; }, + [this, onBack](size_t index) { + if (index >= files.size()) { + return; + } + const std::string selectedFile = files[index]; + if (selectedFile == "Random") { + // Clear the selection to use random + SETTINGS.selectedSleepBmp[0] = '\0'; + } else { + strncpy(SETTINGS.selectedSleepBmp, selectedFile.c_str(), sizeof(SETTINGS.selectedSleepBmp) - 1); + SETTINGS.selectedSleepBmp[sizeof(SETTINGS.selectedSleepBmp) - 1] = '\0'; + } + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No BMP files found in /sleep") {} void SleepBmpSelectionActivity::loadFiles() { files.clear(); @@ -81,14 +89,10 @@ void SleepBmpSelectionActivity::loadFiles() { files.insert(files.end(), bmpFiles.begin(), bmpFiles.end()); } -void SleepBmpSelectionActivity::onEnter() { - Activity::onEnter(); - - renderingMutex = xSemaphoreCreateMutex(); - +void SleepBmpSelectionActivity::loadItems() { loadFiles(); - // Set initial selection: "Random" if no file selected, otherwise find the selected file + // Set initial selection based on saved setting if (SETTINGS.selectedSleepBmp[0] == '\0') { selectorIndex = 0; // "Random" is at index 0 } else { @@ -101,150 +105,10 @@ void SleepBmpSelectionActivity::onEnter() { } } } - - enterTime = millis(); - - updateRequired = true; - - xTaskCreate(&SleepBmpSelectionActivity::taskTrampoline, "SleepBmpSelectionActivityTask", - 2048, // Stack size - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle - ); } void SleepBmpSelectionActivity::onExit() { - Activity::onExit(); - - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD - xSemaphoreTake(renderingMutex, portMAX_DELAY); - if (displayTaskHandle) { - vTaskDelete(displayTaskHandle); - displayTaskHandle = nullptr; - } - vSemaphoreDelete(renderingMutex); - renderingMutex = nullptr; + ListSelectionActivity::onExit(); files.clear(); } -void SleepBmpSelectionActivity::loop() { - const unsigned long timeSinceEnter = millis() - enterTime; - if (timeSinceEnter < IGNORE_INPUT_MS) { - return; - } - - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (files.empty() || selectorIndex >= files.size()) { - return; - } - - const std::string selectedFile = files[selectorIndex]; - if (selectedFile == "Random") { - // Clear the selection to use random - SETTINGS.selectedSleepBmp[0] = '\0'; - } else { - strncpy(SETTINGS.selectedSleepBmp, selectedFile.c_str(), sizeof(SETTINGS.selectedSleepBmp) - 1); - SETTINGS.selectedSleepBmp[sizeof(SETTINGS.selectedSleepBmp) - 1] = '\0'; - } - SETTINGS.saveToFile(); - - onBack(); - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onBack(); - } else if (prevReleased) { - if (files.empty()) { - return; - } - - // Calculate items per page dynamically - const int screenHeight = renderer.getScreenHeight(); - const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; - const int pageItems = (availableHeight / LINE_HEIGHT); - - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + files.size()) % files.size(); - } else { - selectorIndex = (selectorIndex + files.size() - 1) % files.size(); - } - updateRequired = true; - } else if (nextReleased) { - if (files.empty()) { - return; - } - - // Calculate items per page dynamically - const int screenHeight = renderer.getScreenHeight(); - const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; - const int pageItems = (availableHeight / LINE_HEIGHT); - - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % files.size(); - } else { - selectorIndex = (selectorIndex + 1) % files.size(); - } - updateRequired = true; - } -} - -void SleepBmpSelectionActivity::displayTaskLoop() { - while (true) { - if (updateRequired) { - updateRequired = false; - xSemaphoreTake(renderingMutex, portMAX_DELAY); - render(); - xSemaphoreGive(renderingMutex); - } - vTaskDelay(10 / portTICK_PERIOD_MS); - } -} - -void SleepBmpSelectionActivity::render() const { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Sleep BMP", true, EpdFontFamily::BOLD); - - // Help text - const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - - if (files.empty()) { - renderer.drawText(UI_10_FONT_ID, 20, 60, "No BMP files found in /sleep"); - renderer.displayBuffer(); - return; - } - - // Calculate items per page based on screen height - const int screenHeight = renderer.getScreenHeight(); - const int availableHeight = screenHeight - START_Y - BOTTOM_BAR_HEIGHT; - const int pageItems = (availableHeight / LINE_HEIGHT); - - // Calculate page start index - const auto pageStartIndex = selectorIndex / pageItems * pageItems; - - // Draw selection highlight - const int visibleSelectedIndex = static_cast(selectorIndex - pageStartIndex); - if (visibleSelectedIndex >= 0 && visibleSelectedIndex < pageItems && selectorIndex < files.size()) { - renderer.fillRect(0, START_Y + visibleSelectedIndex * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT); - } - - // Draw visible files - int visibleIndex = 0; - for (size_t i = pageStartIndex; i < files.size() && visibleIndex < pageItems; i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40); - const bool isSelected = (i == selectorIndex); - renderer.drawText(UI_10_FONT_ID, 20, START_Y + visibleIndex * LINE_HEIGHT, item.c_str(), !isSelected); - visibleIndex++; - } - - renderer.displayBuffer(); -} - diff --git a/src/activities/settings/SleepBmpSelectionActivity.h b/src/activities/settings/SleepBmpSelectionActivity.h index f9906a39..f3ecddda 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.h +++ b/src/activities/settings/SleepBmpSelectionActivity.h @@ -1,34 +1,20 @@ #pragma once -#include -#include -#include - #include #include #include -#include "../Activity.h" +#include "../ListSelectionActivity.h" -class SleepBmpSelectionActivity final : public Activity { - TaskHandle_t displayTaskHandle = nullptr; - SemaphoreHandle_t renderingMutex = nullptr; +class SleepBmpSelectionActivity final : public ListSelectionActivity { std::vector files; // Sorted list of valid BMP filenames ("Random" at index 0) - size_t selectorIndex = 0; - bool updateRequired = false; - unsigned long enterTime = 0; // Time when activity was entered - const std::function onBack; - - static void taskTrampoline(void* param); - [[noreturn]] void displayTaskLoop(); - void render() const; void loadFiles(); // Load and sort all valid BMP files + protected: + void loadItems() override; // Called by base class onEnter + public: explicit SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack) - : Activity("SleepBmpSelection", renderer, mappedInput), onBack(onBack) {} - void onEnter() override; + const std::function& onBack); void onExit() override; - void loop() override; }; diff --git a/src/activities/settings/SleepScreenSelectionActivity.cpp b/src/activities/settings/SleepScreenSelectionActivity.cpp new file mode 100644 index 00000000..b1417a9f --- /dev/null +++ b/src/activities/settings/SleepScreenSelectionActivity.cpp @@ -0,0 +1,37 @@ +#include "SleepScreenSelectionActivity.h" + +#include + +#include "CrossPointSettings.h" + +SleepScreenSelectionActivity::SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack) + : ListSelectionActivity( + "SleepScreenSelection", renderer, mappedInput, "Select Sleep Screen", + [this]() { return options.size(); }, + [this](size_t index) { return options[index]; }, + [this, onBack](size_t index) { + if (index >= options.size()) { + return; + } + // Map option index to enum value (index matches enum value) + SETTINGS.sleepScreen = static_cast(index); + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No options available") { + // Initialize options from enum + for (uint8_t i = 0; i < CrossPointSettings::getSleepScreenCount(); i++) { + options.push_back(CrossPointSettings::getSleepScreenString(i)); + } +} + +void SleepScreenSelectionActivity::loadItems() { + // Options are already set in constructor, just set initial selection + // Map current enum value to option index + if (SETTINGS.sleepScreen < options.size()) { + selectorIndex = SETTINGS.sleepScreen; + } else { + selectorIndex = 0; // Default to "Dark" + } +} diff --git a/src/activities/settings/SleepScreenSelectionActivity.h b/src/activities/settings/SleepScreenSelectionActivity.h new file mode 100644 index 00000000..1580d51a --- /dev/null +++ b/src/activities/settings/SleepScreenSelectionActivity.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include + +#include "../ListSelectionActivity.h" + +class SleepScreenSelectionActivity final : public ListSelectionActivity { + std::vector options; // Sleep screen mode options + + protected: + void loadItems() override; // Called by base class onEnter + + public: + explicit SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack); +}; diff --git a/src/activities/settings/SleepTimeoutSelectionActivity.cpp b/src/activities/settings/SleepTimeoutSelectionActivity.cpp new file mode 100644 index 00000000..1a564b7c --- /dev/null +++ b/src/activities/settings/SleepTimeoutSelectionActivity.cpp @@ -0,0 +1,37 @@ +#include "SleepTimeoutSelectionActivity.h" + +#include + +#include "CrossPointSettings.h" + +SleepTimeoutSelectionActivity::SleepTimeoutSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack) + : ListSelectionActivity( + "SleepTimeoutSelection", renderer, mappedInput, "Select Time to Sleep", + [this]() { return options.size(); }, + [this](size_t index) { return options[index]; }, + [this, onBack](size_t index) { + if (index >= options.size()) { + return; + } + // Map option index to enum value (index matches enum value) + SETTINGS.sleepTimeout = static_cast(index); + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No options available") { + // Initialize options from enum + for (uint8_t i = 0; i < CrossPointSettings::getSleepTimeoutCount(); i++) { + options.push_back(CrossPointSettings::getSleepTimeoutString(i)); + } +} + +void SleepTimeoutSelectionActivity::loadItems() { + // Options are already set in constructor, just set initial selection + // Map current enum value to option index + if (SETTINGS.sleepTimeout < options.size()) { + selectorIndex = SETTINGS.sleepTimeout; + } else { + selectorIndex = 2; // Default to "10 min" (SLEEP_10_MIN) + } +} diff --git a/src/activities/settings/SleepTimeoutSelectionActivity.h b/src/activities/settings/SleepTimeoutSelectionActivity.h new file mode 100644 index 00000000..031ca731 --- /dev/null +++ b/src/activities/settings/SleepTimeoutSelectionActivity.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include + +#include "../ListSelectionActivity.h" + +class SleepTimeoutSelectionActivity final : public ListSelectionActivity { + std::vector options; + + protected: + void loadItems() override; + + public: + explicit SleepTimeoutSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack); +}; From da142c362e72f5473cc5b339e9165378488992f5 Mon Sep 17 00:00:00 2001 From: dpoulter Date: Sun, 1 Feb 2026 09:55:00 +0200 Subject: [PATCH 06/10] feat: add sleep screen selection --- src/CrossPointSettings.cpp | 53 ++++++++++++++++++++++-------------- src/CrossPointSettings.h | 56 ++++++++++++++++++++++++++------------ 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index ba558057..8914384b 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -272,44 +272,55 @@ int CrossPointSettings::getReaderFontId() const { } } +namespace { +const char* const REFRESH_FREQUENCY_OPTIONS[] = { + #define X(name, val, str) str, + REFRESH_DATA + #undef X +}; + +const char* const SLEEP_SCREEN_OPTIONS[] = { + #define X(name, val, str) str, + SLEEP_SCREEN_DATA + #undef X +}; + +const char* const SLEEP_TIMEOUT_OPTIONS[] = { + #define X(name, val, str) str, + TIMEOUT_DATA + #undef X +}; +} // namespace + const char* CrossPointSettings::getRefreshFrequencyString(uint8_t value) { - static const char* options[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}; - static constexpr size_t count = sizeof(options) / sizeof(options[0]); - if (value < count) { - return options[value]; + if (value < REFRESH_FREQUENCY_COUNT) { + return REFRESH_FREQUENCY_OPTIONS[value]; } - return options[REFRESH_15]; // Default + return REFRESH_FREQUENCY_OPTIONS[REFRESH_15]; } size_t CrossPointSettings::getRefreshFrequencyCount() { - static const char* options[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}; - return sizeof(options) / sizeof(options[0]); + return REFRESH_FREQUENCY_COUNT; } const char* CrossPointSettings::getSleepScreenString(uint8_t value) { - static const char* options[] = {"Dark", "Light", "Custom", "Cover", "None"}; - static constexpr size_t count = sizeof(options) / sizeof(options[0]); - if (value < count) { - return options[value]; + if (value < SLEEP_SCREEN_MODE_COUNT) { + return SLEEP_SCREEN_OPTIONS[value]; } - return options[DARK]; // Default + return SLEEP_SCREEN_OPTIONS[DARK]; } size_t CrossPointSettings::getSleepScreenCount() { - static const char* options[] = {"Dark", "Light", "Custom", "Cover", "None"}; - return sizeof(options) / sizeof(options[0]); + return SLEEP_SCREEN_MODE_COUNT; } const char* CrossPointSettings::getSleepTimeoutString(uint8_t value) { - static const char* options[] = {"1 min", "5 min", "10 min", "15 min", "30 min"}; - static constexpr size_t count = sizeof(options) / sizeof(options[0]); - if (value < count) { - return options[value]; + if (value < SLEEP_TIMEOUT_COUNT) { + return SLEEP_TIMEOUT_OPTIONS[value]; } - return options[SLEEP_10_MIN]; // Default + return SLEEP_TIMEOUT_OPTIONS[SLEEP_10_MIN]; } size_t CrossPointSettings::getSleepTimeoutCount() { - static const char* options[] = {"1 min", "5 min", "10 min", "15 min", "30 min"}; - return sizeof(options) / sizeof(options[0]); + return SLEEP_TIMEOUT_COUNT; } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 7b66bb54..48a08931 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -15,7 +15,19 @@ class CrossPointSettings { CrossPointSettings(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 }; + #define SLEEP_SCREEN_DATA \ + X(DARK, 0, "Dark") \ + X(LIGHT, 1, "Light") \ + X(CUSTOM, 2, "Custom") \ + X(COVER, 3, "Cover") \ + X(BLANK, 4, "None") + + enum SLEEP_SCREEN_MODE { + #define X(name, val, str) name = val, + SLEEP_SCREEN_DATA + #undef X + SLEEP_SCREEN_MODE_COUNT + }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; enum SLEEP_SCREEN_COVER_FILTER { NO_FILTER = 0, @@ -71,24 +83,34 @@ class CrossPointSettings { PARAGRAPH_ALIGNMENT_COUNT }; - // Auto-sleep timeout options (in minutes) - enum SLEEP_TIMEOUT { - SLEEP_1_MIN = 0, - SLEEP_5_MIN = 1, - SLEEP_10_MIN = 2, - SLEEP_15_MIN = 3, - SLEEP_30_MIN = 4, - SLEEP_TIMEOUT_COUNT + // E-ink refresh frequency (pages between full refreshes) + #define REFRESH_DATA \ + X(REFRESH_1, 0, "1 page") \ + X(REFRESH_5, 1, "5 pages") \ + X(REFRESH_10, 2, "10 pages") \ + X(REFRESH_15, 3, "15 pages") \ + X(REFRESH_30, 4, "30 pages") + + enum REFRESH_FREQUENCY { + #define X(name, val, str) name = val, + REFRESH_DATA + #undef X + REFRESH_FREQUENCY_COUNT }; - // E-ink refresh frequency (pages between full refreshes) - enum REFRESH_FREQUENCY { - REFRESH_1 = 0, - REFRESH_5 = 1, - REFRESH_10 = 2, - REFRESH_15 = 3, - REFRESH_30 = 4, - REFRESH_FREQUENCY_COUNT + // Auto-sleep timeout options (in minutes) + #define TIMEOUT_DATA \ + X(SLEEP_1_MIN, 0, "1 min") \ + X(SLEEP_5_MIN, 1, "5 min") \ + X(SLEEP_10_MIN, 2, "10 min") \ + X(SLEEP_15_MIN, 3, "15 min") \ + X(SLEEP_30_MIN, 4, "30 min") + + enum SLEEP_TIMEOUT { + #define X(name, val, str) name = val, + TIMEOUT_DATA + #undef X + SLEEP_TIMEOUT_COUNT }; // Short power button press actions From d4036bf1f76d34f2a75f2c5f4d2793ff97181275 Mon Sep 17 00:00:00 2001 From: dpoulter Date: Sun, 1 Feb 2026 11:08:35 +0200 Subject: [PATCH 07/10] feat: add sleep screen selection --- src/CrossPointSettings.cpp | 31 ++++++++++----- src/CrossPointSettings.h | 23 +++++++++-- src/activities/ListSelectionActivity.cpp | 12 ++---- src/activities/ListSelectionActivity.h | 9 +---- src/activities/boot_sleep/SleepActivity.cpp | 38 ++++++++++--------- src/activities/boot_sleep/SleepActivity.h | 2 + .../RefreshFrequencySelectionActivity.cpp | 4 +- .../RefreshFrequencySelectionActivity.h | 4 +- .../ScreenMarginSelectionActivity.cpp | 27 ++++--------- .../settings/ScreenMarginSelectionActivity.h | 4 +- src/activities/settings/SettingsActivity.cpp | 1 - .../settings/SleepBmpSelectionActivity.h | 6 +-- .../settings/SleepScreenSelectionActivity.cpp | 6 +-- .../settings/SleepScreenSelectionActivity.h | 4 +- .../SleepTimeoutSelectionActivity.cpp | 6 +-- 15 files changed, 87 insertions(+), 90 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 8914384b..4348b516 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -290,6 +290,12 @@ const char* const SLEEP_TIMEOUT_OPTIONS[] = { TIMEOUT_DATA #undef X }; + +const char* const SCREEN_MARGIN_OPTIONS[] = { + #define X(val, str) str, + SCREEN_MARGIN_DATA + #undef X +}; } // namespace const char* CrossPointSettings::getRefreshFrequencyString(uint8_t value) { @@ -299,10 +305,6 @@ const char* CrossPointSettings::getRefreshFrequencyString(uint8_t value) { return REFRESH_FREQUENCY_OPTIONS[REFRESH_15]; } -size_t CrossPointSettings::getRefreshFrequencyCount() { - return REFRESH_FREQUENCY_COUNT; -} - const char* CrossPointSettings::getSleepScreenString(uint8_t value) { if (value < SLEEP_SCREEN_MODE_COUNT) { return SLEEP_SCREEN_OPTIONS[value]; @@ -310,10 +312,6 @@ const char* CrossPointSettings::getSleepScreenString(uint8_t value) { return SLEEP_SCREEN_OPTIONS[DARK]; } -size_t CrossPointSettings::getSleepScreenCount() { - return SLEEP_SCREEN_MODE_COUNT; -} - const char* CrossPointSettings::getSleepTimeoutString(uint8_t value) { if (value < SLEEP_TIMEOUT_COUNT) { return SLEEP_TIMEOUT_OPTIONS[value]; @@ -321,6 +319,19 @@ const char* CrossPointSettings::getSleepTimeoutString(uint8_t value) { return SLEEP_TIMEOUT_OPTIONS[SLEEP_10_MIN]; } -size_t CrossPointSettings::getSleepTimeoutCount() { - return SLEEP_TIMEOUT_COUNT; +const char* CrossPointSettings::getScreenMarginString(uint8_t index) { + if (index < SCREEN_MARGIN_COUNT) { + return SCREEN_MARGIN_OPTIONS[index]; + } + return SCREEN_MARGIN_OPTIONS[MARGIN_5]; } + +int CrossPointSettings::getScreenMarginIndex(uint8_t pixelValue) { + for (size_t i = 0; i < SCREEN_MARGIN_COUNT; i++) { + if (SCREEN_MARGIN_VALUES[i] == pixelValue) { + return static_cast(i); + } + } + return -1; +} + diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 48a08931..bdb8221f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -113,6 +113,23 @@ class CrossPointSettings { SLEEP_TIMEOUT_COUNT }; + // Reader screen margin options (pixel values) + #define SCREEN_MARGIN_DATA \ + X(5, "5 px") X(10, "10 px") X(15, "15 px") X(20, "20 px") \ + X(25, "25 px") X(30, "30 px") X(35, "35 px") X(40, "40 px") + + enum SCREEN_MARGIN { + #define X(val, str) MARGIN_##val, + SCREEN_MARGIN_DATA + #undef X + SCREEN_MARGIN_COUNT + }; + static inline constexpr uint8_t SCREEN_MARGIN_VALUES[SCREEN_MARGIN_COUNT] = { + #define X(val, str) val, + SCREEN_MARGIN_DATA + #undef X + }; + // Short power button press actions enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT }; @@ -177,11 +194,11 @@ class CrossPointSettings { // Helper functions to get option strings from enum values static const char* getRefreshFrequencyString(uint8_t value); - static size_t getRefreshFrequencyCount(); static const char* getSleepScreenString(uint8_t value); - static size_t getSleepScreenCount(); static const char* getSleepTimeoutString(uint8_t value); - static size_t getSleepTimeoutCount(); + static const char* getScreenMarginString(uint8_t index); + /** Returns index for pixel value, or -1 if not in allowed list. */ + static int getScreenMarginIndex(uint8_t pixelValue); float getReaderLineCompression() const; unsigned long getSleepTimeoutMs() const; diff --git a/src/activities/ListSelectionActivity.cpp b/src/activities/ListSelectionActivity.cpp index e562a441..bb0051e3 100644 --- a/src/activities/ListSelectionActivity.cpp +++ b/src/activities/ListSelectionActivity.cpp @@ -146,15 +146,9 @@ void ListSelectionActivity::render() const { const bool isSelected = (i == selectorIndex); const int itemY = START_Y + visibleIndex * LINE_HEIGHT; - if (customRenderItem) { - // Use custom renderer if provided - customRenderItem(i, 20, itemY, isSelected); - } else { - // Default rendering: truncate text and draw - const std::string itemText = getItemText(i); - auto truncated = renderer.truncatedText(UI_10_FONT_ID, itemText.c_str(), pageWidth - 40); - renderer.drawText(UI_10_FONT_ID, 20, itemY, truncated.c_str(), !isSelected); - } + const std::string itemText = getItemText(i); + auto truncated = renderer.truncatedText(UI_10_FONT_ID, itemText.c_str(), pageWidth - 40); + renderer.drawText(UI_10_FONT_ID, 20, itemY, truncated.c_str(), !isSelected); visibleIndex++; } diff --git a/src/activities/ListSelectionActivity.h b/src/activities/ListSelectionActivity.h index e2595b07..a583bec9 100644 --- a/src/activities/ListSelectionActivity.h +++ b/src/activities/ListSelectionActivity.h @@ -17,7 +17,6 @@ * - Automatic pagination based on screen size * - Page skipping when holding navigation buttons * - Configurable title, empty message, and button labels - * - Customizable item rendering */ class ListSelectionActivity : public Activity { protected: @@ -36,8 +35,7 @@ class ListSelectionActivity : public Activity { std::function getItemText; std::function onItemSelected; std::function onBack; - std::function customRenderItem; // index, x, y, isSelected - + // Constants static constexpr int SKIP_PAGE_MS = 700; static constexpr unsigned long IGNORE_INPUT_MS = 300; @@ -80,9 +78,4 @@ class ListSelectionActivity : public Activity { // Allow subclasses to set initial selection void setInitialSelection(size_t index) { selectorIndex = index; } size_t getCurrentSelection() const { return selectorIndex; } - - // Allow custom item rendering - void setCustomItemRenderer(std::function renderer) { - customRenderItem = renderer; - } }; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index de6b939f..4be8925d 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -45,26 +45,30 @@ void SleepActivity::renderPopup(const char* message) const { renderer.displayBuffer(); } +bool SleepActivity::renderSelectedSleepBmp(FsFile& dir) const { + if (SETTINGS.selectedSleepBmp[0] == '\0') return false; + const std::string selectedFile = std::string(SETTINGS.selectedSleepBmp); + const std::string filename = "/sleep/" + selectedFile; + FsFile file; + if (!SdMan.openFileForRead("SLP", filename, file)) { + Serial.printf("[%lu] [SLP] Selected BMP not found or invalid, falling back to random\n", millis()); + return false; + } + Bitmap bitmap(file); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + file.close(); + Serial.printf("[%lu] [SLP] Selected BMP not found or invalid, falling back to random\n", millis()); + return false; + } + renderBitmapSleepScreen(bitmap); + dir.close(); + return true; +} + void SleepActivity::renderCustomSleepScreen() const { auto dir = SdMan.open("/sleep"); if (dir && dir.isDirectory()) { - if (SETTINGS.selectedSleepBmp[0] != '\0') { - const std::string selectedFile = std::string(SETTINGS.selectedSleepBmp); - const std::string filename = "/sleep/" + selectedFile; - FsFile file; - if (SdMan.openFileForRead("SLP", filename, file)) { - Bitmap bitmap(file); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - Serial.printf("[%lu] [SLP] Loading selected: /sleep/%s\n", millis(), selectedFile.c_str()); - delay(100); - renderBitmapSleepScreen(bitmap); - dir.close(); - return; - } - file.close(); - } - Serial.printf("[%lu] [SLP] Selected BMP not found or invalid, falling back to random\n", millis()); - } + if (renderSelectedSleepBmp(dir)) return; std::vector files; char name[500]; diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 283220ce..175ef794 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -1,5 +1,6 @@ #pragma once #include "../Activity.h" +#include class Bitmap; @@ -16,4 +17,5 @@ class SleepActivity final : public Activity { void renderCoverSleepScreen() const; void renderBitmapSleepScreen(const Bitmap& bitmap) const; void renderBlankSleepScreen() const; + bool renderSelectedSleepBmp(FsFile& dir) const; }; diff --git a/src/activities/settings/RefreshFrequencySelectionActivity.cpp b/src/activities/settings/RefreshFrequencySelectionActivity.cpp index c8205363..461feeb4 100644 --- a/src/activities/settings/RefreshFrequencySelectionActivity.cpp +++ b/src/activities/settings/RefreshFrequencySelectionActivity.cpp @@ -21,14 +21,12 @@ RefreshFrequencySelectionActivity::RefreshFrequencySelectionActivity(GfxRenderer }, onBack, "No options available") { // Initialize options from enum - for (uint8_t i = 0; i < CrossPointSettings::getRefreshFrequencyCount(); i++) { + for (uint8_t i = 0; i < CrossPointSettings::REFRESH_FREQUENCY_COUNT; i++) { options.push_back(CrossPointSettings::getRefreshFrequencyString(i)); } } void RefreshFrequencySelectionActivity::loadItems() { - // Options are already set in constructor, just set initial selection - // Map current enum value to option index if (SETTINGS.refreshFrequency < options.size()) { selectorIndex = SETTINGS.refreshFrequency; } else { diff --git a/src/activities/settings/RefreshFrequencySelectionActivity.h b/src/activities/settings/RefreshFrequencySelectionActivity.h index b4c31cb9..427a8c02 100644 --- a/src/activities/settings/RefreshFrequencySelectionActivity.h +++ b/src/activities/settings/RefreshFrequencySelectionActivity.h @@ -6,10 +6,10 @@ #include "../ListSelectionActivity.h" class RefreshFrequencySelectionActivity final : public ListSelectionActivity { - std::vector options; // Refresh frequency options + std::vector options; protected: - void loadItems() override; // Called by base class onEnter + void loadItems() override; public: explicit RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, diff --git a/src/activities/settings/ScreenMarginSelectionActivity.cpp b/src/activities/settings/ScreenMarginSelectionActivity.cpp index 1e891d5c..4ab70d4c 100644 --- a/src/activities/settings/ScreenMarginSelectionActivity.cpp +++ b/src/activities/settings/ScreenMarginSelectionActivity.cpp @@ -1,7 +1,6 @@ #include "ScreenMarginSelectionActivity.h" #include -#include #include "CrossPointSettings.h" @@ -15,33 +14,21 @@ ScreenMarginSelectionActivity::ScreenMarginSelectionActivity(GfxRenderer& render if (index >= options.size()) { return; } - // Map option index to margin value - // Options: "5 px", "10 px", "15 px", "20 px", "25 px", "30 px", "35 px", "40 px" - // Values: 5, 10, 15, 20, 25, 30, 35, 40 - SETTINGS.screenMargin = static_cast((index + 1) * 5); + SETTINGS.screenMargin = CrossPointSettings::SCREEN_MARGIN_VALUES[index]; SETTINGS.saveToFile(); onBack(); }, onBack, "No options available") { - // Initialize options: 5 to 40 in steps of 5 - for (int i = 5; i <= 40; i += 5) { - std::ostringstream oss; - oss << i << " px"; - options.push_back(oss.str()); + for (uint8_t i = 0; i < CrossPointSettings::SCREEN_MARGIN_COUNT; i++) { + options.push_back(CrossPointSettings::getScreenMarginString(i)); } } void ScreenMarginSelectionActivity::loadItems() { - // Options are already set in constructor, just set initial selection - // Map current margin value to option index - // margin value / 5 - 1 = index (e.g., 5 -> 0, 10 -> 1, etc.) - if (SETTINGS.screenMargin >= 5 && SETTINGS.screenMargin <= 40) { - selectorIndex = (SETTINGS.screenMargin / 5) - 1; - // Ensure index is within bounds - if (selectorIndex >= options.size()) { - selectorIndex = 0; // Default to "5 px" - } + const int idx = CrossPointSettings::getScreenMarginIndex(SETTINGS.screenMargin); + if (idx >= 0 && static_cast(idx) < options.size()) { + selectorIndex = static_cast(idx); } else { - selectorIndex = 0; // Default to "5 px" + selectorIndex = 0; } } diff --git a/src/activities/settings/ScreenMarginSelectionActivity.h b/src/activities/settings/ScreenMarginSelectionActivity.h index 8178ce92..f1169cd6 100644 --- a/src/activities/settings/ScreenMarginSelectionActivity.h +++ b/src/activities/settings/ScreenMarginSelectionActivity.h @@ -6,10 +6,10 @@ #include "../ListSelectionActivity.h" class ScreenMarginSelectionActivity final : public ListSelectionActivity { - std::vector options; // Screen margin options + std::vector options; protected: - void loadItems() override; // Called by base class onEnter + void loadItems() override; public: explicit ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index ff4c4f26..87be2b87 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -11,7 +11,6 @@ #include "CategorySettingsActivity.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" -#include "OtaUpdateActivity.h" #include "SleepBmpSelectionActivity.h" #include "fontIds.h" diff --git a/src/activities/settings/SleepBmpSelectionActivity.h b/src/activities/settings/SleepBmpSelectionActivity.h index f3ecddda..dbb86349 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.h +++ b/src/activities/settings/SleepBmpSelectionActivity.h @@ -6,11 +6,11 @@ #include "../ListSelectionActivity.h" class SleepBmpSelectionActivity final : public ListSelectionActivity { - std::vector files; // Sorted list of valid BMP filenames ("Random" at index 0) - void loadFiles(); // Load and sort all valid BMP files + std::vector files; + void loadFiles(); protected: - void loadItems() override; // Called by base class onEnter + void loadItems() override; public: explicit SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, diff --git a/src/activities/settings/SleepScreenSelectionActivity.cpp b/src/activities/settings/SleepScreenSelectionActivity.cpp index b1417a9f..c3203630 100644 --- a/src/activities/settings/SleepScreenSelectionActivity.cpp +++ b/src/activities/settings/SleepScreenSelectionActivity.cpp @@ -14,21 +14,17 @@ SleepScreenSelectionActivity::SleepScreenSelectionActivity(GfxRenderer& renderer if (index >= options.size()) { return; } - // Map option index to enum value (index matches enum value) SETTINGS.sleepScreen = static_cast(index); SETTINGS.saveToFile(); onBack(); }, onBack, "No options available") { - // Initialize options from enum - for (uint8_t i = 0; i < CrossPointSettings::getSleepScreenCount(); i++) { + for (uint8_t i = 0; i < CrossPointSettings::SLEEP_SCREEN_MODE_COUNT; i++) { options.push_back(CrossPointSettings::getSleepScreenString(i)); } } void SleepScreenSelectionActivity::loadItems() { - // Options are already set in constructor, just set initial selection - // Map current enum value to option index if (SETTINGS.sleepScreen < options.size()) { selectorIndex = SETTINGS.sleepScreen; } else { diff --git a/src/activities/settings/SleepScreenSelectionActivity.h b/src/activities/settings/SleepScreenSelectionActivity.h index 1580d51a..fa18c37d 100644 --- a/src/activities/settings/SleepScreenSelectionActivity.h +++ b/src/activities/settings/SleepScreenSelectionActivity.h @@ -6,10 +6,10 @@ #include "../ListSelectionActivity.h" class SleepScreenSelectionActivity final : public ListSelectionActivity { - std::vector options; // Sleep screen mode options + std::vector options; protected: - void loadItems() override; // Called by base class onEnter + void loadItems() override; public: explicit SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, diff --git a/src/activities/settings/SleepTimeoutSelectionActivity.cpp b/src/activities/settings/SleepTimeoutSelectionActivity.cpp index 1a564b7c..866e99ba 100644 --- a/src/activities/settings/SleepTimeoutSelectionActivity.cpp +++ b/src/activities/settings/SleepTimeoutSelectionActivity.cpp @@ -14,21 +14,17 @@ SleepTimeoutSelectionActivity::SleepTimeoutSelectionActivity(GfxRenderer& render if (index >= options.size()) { return; } - // Map option index to enum value (index matches enum value) SETTINGS.sleepTimeout = static_cast(index); SETTINGS.saveToFile(); onBack(); }, onBack, "No options available") { - // Initialize options from enum - for (uint8_t i = 0; i < CrossPointSettings::getSleepTimeoutCount(); i++) { + for (uint8_t i = 0; i < CrossPointSettings::SLEEP_TIMEOUT_COUNT; i++) { options.push_back(CrossPointSettings::getSleepTimeoutString(i)); } } void SleepTimeoutSelectionActivity::loadItems() { - // Options are already set in constructor, just set initial selection - // Map current enum value to option index if (SETTINGS.sleepTimeout < options.size()) { selectorIndex = SETTINGS.sleepTimeout; } else { From 78ebcf7856d6f3a6ad5ea19fb487238be3ab4b04 Mon Sep 17 00:00:00 2001 From: dpoulter Date: Sun, 1 Feb 2026 12:03:01 +0200 Subject: [PATCH 08/10] feat: add sleep screen selection --- src/CrossPointSettings.cpp | 30 +++++++++++++++---- src/CrossPointSettings.h | 13 +++----- src/activities/reader/EpubReaderActivity.cpp | 10 +++---- src/activities/reader/TxtReaderActivity.cpp | 2 +- .../settings/CategorySettingsActivity.cpp | 2 +- .../ScreenMarginSelectionActivity.cpp | 7 ++--- 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 4348b516..2b33bafe 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -24,6 +24,21 @@ constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields constexpr uint8_t SETTINGS_COUNT = 24; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; + +const uint8_t SCREEN_MARGIN_PIXEL_VALUES[CrossPointSettings::SCREEN_MARGIN_COUNT] = { + #define X(val, str) val, + SCREEN_MARGIN_DATA + #undef X +}; + +int screenMarginPixelToIndex(uint8_t pixelValue) { + for (size_t i = 0; i < CrossPointSettings::SCREEN_MARGIN_COUNT; i++) { + if (SCREEN_MARGIN_PIXEL_VALUES[i] == pixelValue) { + return static_cast(i); + } + } + return -1; +} } // namespace bool CrossPointSettings::saveToFile() const { @@ -114,6 +129,11 @@ bool CrossPointSettings::loadFromFile() { readAndValidate(inputFile, refreshFrequency, REFRESH_FREQUENCY_COUNT); if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, screenMargin); + // Migrate old format: stored pixel value (5,10,15...) → index (0,1,2...) + if (screenMargin >= SCREEN_MARGIN_COUNT) { + const int idx = screenMarginPixelToIndex(screenMargin); + screenMargin = (idx >= 0) ? static_cast(idx) : 0; + } if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sleepScreenCoverMode, SLEEP_SCREEN_COVER_MODE_COUNT); if (++settingsRead >= fileSettingsCount) break; @@ -326,12 +346,10 @@ const char* CrossPointSettings::getScreenMarginString(uint8_t index) { return SCREEN_MARGIN_OPTIONS[MARGIN_5]; } -int CrossPointSettings::getScreenMarginIndex(uint8_t pixelValue) { - for (size_t i = 0; i < SCREEN_MARGIN_COUNT; i++) { - if (SCREEN_MARGIN_VALUES[i] == pixelValue) { - return static_cast(i); - } +uint8_t CrossPointSettings::getScreenMarginPixels() const { + if (screenMargin < SCREEN_MARGIN_COUNT) { + return SCREEN_MARGIN_PIXEL_VALUES[screenMargin]; } - return -1; + return SCREEN_MARGIN_PIXEL_VALUES[MARGIN_5]; } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index bdb8221f..3d359a3a 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -124,11 +124,6 @@ class CrossPointSettings { #undef X SCREEN_MARGIN_COUNT }; - static inline constexpr uint8_t SCREEN_MARGIN_VALUES[SCREEN_MARGIN_COUNT] = { - #define X(val, str) val, - SCREEN_MARGIN_DATA - #undef X - }; // Short power button press actions enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT }; @@ -166,8 +161,8 @@ class CrossPointSettings { uint8_t refreshFrequency = REFRESH_15; uint8_t hyphenationEnabled = 0; - // Reader screen margin settings - uint8_t screenMargin = 5; + // Reader screen margin (enum index; use getScreenMarginPixels() for pixel value) + uint8_t screenMargin = MARGIN_5; // OPDS browser settings char opdsServerUrl[128] = ""; char opdsUsername[64] = ""; @@ -197,8 +192,8 @@ class CrossPointSettings { static const char* getSleepScreenString(uint8_t value); static const char* getSleepTimeoutString(uint8_t value); static const char* getScreenMarginString(uint8_t index); - /** Returns index for pixel value, or -1 if not in allowed list. */ - static int getScreenMarginIndex(uint8_t pixelValue); + /** Returns pixel margin for current screenMargin index (e.g. 5, 10, 15...). */ + uint8_t getScreenMarginPixels() const; float getReaderLineCompression() const; unsigned long getSleepTimeoutMs() const; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 58668c68..392c6cda 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -281,17 +281,17 @@ void EpubReaderActivity::renderScreen() { int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, &orientedMarginLeft); - orientedMarginTop += SETTINGS.screenMargin; - orientedMarginLeft += SETTINGS.screenMargin; - orientedMarginRight += SETTINGS.screenMargin; - orientedMarginBottom += SETTINGS.screenMargin; + orientedMarginTop += SETTINGS.getScreenMarginPixels(); + orientedMarginLeft += SETTINGS.getScreenMarginPixels(); + orientedMarginRight += SETTINGS.getScreenMarginPixels(); + orientedMarginBottom += SETTINGS.getScreenMarginPixels(); // Add status bar margin if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { // Add additional margin for status bar if progress bar is shown const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; - orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin + + orientedMarginBottom += statusBarMargin - SETTINGS.getScreenMarginPixels() + (showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index e9303de3..63555317 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -156,7 +156,7 @@ void TxtReaderActivity::initializeReader() { // Store current settings for cache validation cachedFontId = SETTINGS.getReaderFontId(); - cachedScreenMargin = SETTINGS.screenMargin; + cachedScreenMargin = SETTINGS.getScreenMarginPixels(); cachedParagraphAlignment = SETTINGS.paragraphAlignment; // Calculate viewport dimensions diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 8d6fee09..d1639028 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -312,7 +312,7 @@ void CategorySettingsActivity::render() const { valueText = CrossPointSettings::getRefreshFrequencyString(SETTINGS.refreshFrequency); } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Screen Margin") == 0) { // Format margin value as "X px" - valueText = std::to_string(SETTINGS.screenMargin) + " px"; + valueText = CrossPointSettings::getScreenMarginString(SETTINGS.screenMargin); } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Time to Sleep") == 0) { valueText = CrossPointSettings::getSleepTimeoutString(SETTINGS.sleepTimeout); } else if (settingsList[i].type == SettingType::ACTION && strcmp(settingsList[i].name, "Select Sleep BMP") == 0) { diff --git a/src/activities/settings/ScreenMarginSelectionActivity.cpp b/src/activities/settings/ScreenMarginSelectionActivity.cpp index 4ab70d4c..837c8f21 100644 --- a/src/activities/settings/ScreenMarginSelectionActivity.cpp +++ b/src/activities/settings/ScreenMarginSelectionActivity.cpp @@ -14,7 +14,7 @@ ScreenMarginSelectionActivity::ScreenMarginSelectionActivity(GfxRenderer& render if (index >= options.size()) { return; } - SETTINGS.screenMargin = CrossPointSettings::SCREEN_MARGIN_VALUES[index]; + SETTINGS.screenMargin = static_cast(index); SETTINGS.saveToFile(); onBack(); }, @@ -25,9 +25,8 @@ ScreenMarginSelectionActivity::ScreenMarginSelectionActivity(GfxRenderer& render } void ScreenMarginSelectionActivity::loadItems() { - const int idx = CrossPointSettings::getScreenMarginIndex(SETTINGS.screenMargin); - if (idx >= 0 && static_cast(idx) < options.size()) { - selectorIndex = static_cast(idx); + if (SETTINGS.screenMargin < options.size()) { + selectorIndex = SETTINGS.screenMargin; } else { selectorIndex = 0; } From 2d8e283b7bd3a942a3844869122cbc88a088d0a0 Mon Sep 17 00:00:00 2001 From: dpoulter Date: Sun, 1 Feb 2026 12:05:15 +0200 Subject: [PATCH 09/10] feat: add sleep screen selection --- src/CrossPointSettings.cpp | 21 +++-- src/CrossPointSettings.h | 80 +++++++++---------- src/activities/ListSelectionActivity.cpp | 7 +- src/activities/ListSelectionActivity.h | 21 +++-- src/activities/boot_sleep/SleepActivity.h | 3 +- .../settings/CategorySettingsActivity.cpp | 11 ++- .../RefreshFrequencySelectionActivity.cpp | 6 +- .../ScreenMarginSelectionActivity.cpp | 3 +- .../settings/ScreenMarginSelectionActivity.h | 2 +- src/activities/settings/SettingsActivity.cpp | 3 +- src/activities/settings/SettingsActivity.h | 2 +- .../settings/SleepBmpSelectionActivity.cpp | 36 ++++----- .../settings/SleepBmpSelectionActivity.h | 1 - .../settings/SleepScreenSelectionActivity.cpp | 3 +- .../SleepTimeoutSelectionActivity.cpp | 5 +- 15 files changed, 95 insertions(+), 109 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 2b33bafe..a46205d8 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -26,9 +26,9 @@ constexpr uint8_t SETTINGS_COUNT = 24; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; const uint8_t SCREEN_MARGIN_PIXEL_VALUES[CrossPointSettings::SCREEN_MARGIN_COUNT] = { - #define X(val, str) val, +#define X(val, str) val, SCREEN_MARGIN_DATA - #undef X +#undef X }; int screenMarginPixelToIndex(uint8_t pixelValue) { @@ -294,27 +294,27 @@ int CrossPointSettings::getReaderFontId() const { namespace { const char* const REFRESH_FREQUENCY_OPTIONS[] = { - #define X(name, val, str) str, +#define X(name, val, str) str, REFRESH_DATA - #undef X +#undef X }; const char* const SLEEP_SCREEN_OPTIONS[] = { - #define X(name, val, str) str, +#define X(name, val, str) str, SLEEP_SCREEN_DATA - #undef X +#undef X }; const char* const SLEEP_TIMEOUT_OPTIONS[] = { - #define X(name, val, str) str, +#define X(name, val, str) str, TIMEOUT_DATA - #undef X +#undef X }; const char* const SCREEN_MARGIN_OPTIONS[] = { - #define X(val, str) str, +#define X(val, str) str, SCREEN_MARGIN_DATA - #undef X +#undef X }; } // namespace @@ -352,4 +352,3 @@ uint8_t CrossPointSettings::getScreenMarginPixels() const { } return SCREEN_MARGIN_PIXEL_VALUES[MARGIN_5]; } - diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 3d359a3a..d3057ea9 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -15,18 +15,18 @@ class CrossPointSettings { CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete; - #define SLEEP_SCREEN_DATA \ - X(DARK, 0, "Dark") \ - X(LIGHT, 1, "Light") \ - X(CUSTOM, 2, "Custom") \ - X(COVER, 3, "Cover") \ - X(BLANK, 4, "None") +#define SLEEP_SCREEN_DATA \ + X(DARK, 0, "Dark") \ + X(LIGHT, 1, "Light") \ + X(CUSTOM, 2, "Custom") \ + X(COVER, 3, "Cover") \ + X(BLANK, 4, "None") enum SLEEP_SCREEN_MODE { - #define X(name, val, str) name = val, - SLEEP_SCREEN_DATA - #undef X - SLEEP_SCREEN_MODE_COUNT +#define X(name, val, str) name = val, + SLEEP_SCREEN_DATA +#undef X + SLEEP_SCREEN_MODE_COUNT }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; enum SLEEP_SCREEN_COVER_FILTER { @@ -83,46 +83,46 @@ class CrossPointSettings { PARAGRAPH_ALIGNMENT_COUNT }; - // E-ink refresh frequency (pages between full refreshes) - #define REFRESH_DATA \ - X(REFRESH_1, 0, "1 page") \ - X(REFRESH_5, 1, "5 pages") \ - X(REFRESH_10, 2, "10 pages") \ - X(REFRESH_15, 3, "15 pages") \ - X(REFRESH_30, 4, "30 pages") +// E-ink refresh frequency (pages between full refreshes) +#define REFRESH_DATA \ + X(REFRESH_1, 0, "1 page") \ + X(REFRESH_5, 1, "5 pages") \ + X(REFRESH_10, 2, "10 pages") \ + X(REFRESH_15, 3, "15 pages") \ + X(REFRESH_30, 4, "30 pages") enum REFRESH_FREQUENCY { - #define X(name, val, str) name = val, - REFRESH_DATA - #undef X - REFRESH_FREQUENCY_COUNT +#define X(name, val, str) name = val, + REFRESH_DATA +#undef X + REFRESH_FREQUENCY_COUNT }; - // Auto-sleep timeout options (in minutes) - #define TIMEOUT_DATA \ - X(SLEEP_1_MIN, 0, "1 min") \ - X(SLEEP_5_MIN, 1, "5 min") \ - X(SLEEP_10_MIN, 2, "10 min") \ - X(SLEEP_15_MIN, 3, "15 min") \ - X(SLEEP_30_MIN, 4, "30 min") +// Auto-sleep timeout options (in minutes) +#define TIMEOUT_DATA \ + X(SLEEP_1_MIN, 0, "1 min") \ + X(SLEEP_5_MIN, 1, "5 min") \ + X(SLEEP_10_MIN, 2, "10 min") \ + X(SLEEP_15_MIN, 3, "15 min") \ + X(SLEEP_30_MIN, 4, "30 min") enum SLEEP_TIMEOUT { - #define X(name, val, str) name = val, - TIMEOUT_DATA - #undef X - SLEEP_TIMEOUT_COUNT +#define X(name, val, str) name = val, + TIMEOUT_DATA +#undef X + SLEEP_TIMEOUT_COUNT }; - // Reader screen margin options (pixel values) - #define SCREEN_MARGIN_DATA \ - X(5, "5 px") X(10, "10 px") X(15, "15 px") X(20, "20 px") \ - X(25, "25 px") X(30, "30 px") X(35, "35 px") X(40, "40 px") +// Reader screen margin options (pixel values) +#define SCREEN_MARGIN_DATA \ + X(5, "5 px") X(10, "10 px") X(15, "15 px") X(20, "20 px") X(25, "25 px") X(30, "30 px") X(35, "35 px") X(40, "40 " \ + "px") enum SCREEN_MARGIN { - #define X(val, str) MARGIN_##val, - SCREEN_MARGIN_DATA - #undef X - SCREEN_MARGIN_COUNT +#define X(val, str) MARGIN_##val, + SCREEN_MARGIN_DATA +#undef X + SCREEN_MARGIN_COUNT }; // Short power button press actions diff --git a/src/activities/ListSelectionActivity.cpp b/src/activities/ListSelectionActivity.cpp index bb0051e3..7e66b7b4 100644 --- a/src/activities/ListSelectionActivity.cpp +++ b/src/activities/ListSelectionActivity.cpp @@ -22,10 +22,10 @@ void ListSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); enterTime = millis(); - + // Load items (allows subclasses to populate data) loadItems(); - + // Ensure selector index is valid const size_t itemCount = getItemCount(); if (selectorIndex >= itemCount && itemCount > 0) { @@ -34,8 +34,7 @@ void ListSelectionActivity::onEnter() { updateRequired = true; - xTaskCreate(&ListSelectionActivity::taskTrampoline, "ListSelectionTask", 2048, this, 1, - &displayTaskHandle); + xTaskCreate(&ListSelectionActivity::taskTrampoline, "ListSelectionTask", 2048, this, 1, &displayTaskHandle); } void ListSelectionActivity::onExit() { diff --git a/src/activities/ListSelectionActivity.h b/src/activities/ListSelectionActivity.h index a583bec9..7b7d1371 100644 --- a/src/activities/ListSelectionActivity.h +++ b/src/activities/ListSelectionActivity.h @@ -25,7 +25,7 @@ class ListSelectionActivity : public Activity { size_t selectorIndex = 0; bool updateRequired = false; unsigned long enterTime = 0; - + // Configuration std::string title; std::string emptyMessage; @@ -51,14 +51,11 @@ class ListSelectionActivity : public Activity { public: explicit ListSelectionActivity(const std::string& activityName, GfxRenderer& renderer, - MappedInputManager& mappedInput, const std::string& title, - std::function getItemCount, - std::function getItemText, - std::function onItemSelected, - std::function onBack, - const std::string& emptyMessage = "No items available", - const std::string& backLabel = "« Back", - const std::string& confirmLabel = "Select") + MappedInputManager& mappedInput, const std::string& title, + std::function getItemCount, std::function getItemText, + std::function onItemSelected, std::function onBack, + const std::string& emptyMessage = "No items available", + const std::string& backLabel = "« Back", const std::string& confirmLabel = "Select") : Activity(activityName, renderer, mappedInput), title(title), emptyMessage(emptyMessage), @@ -68,13 +65,13 @@ class ListSelectionActivity : public Activity { getItemText(getItemText), onItemSelected(onItemSelected), onBack(onBack) {} - + virtual ~ListSelectionActivity() = default; - + void onEnter() override; void onExit() override; void loop() override; - + // Allow subclasses to set initial selection void setInitialSelection(size_t index) { selectorIndex = index; } size_t getCurrentSelection() const { return selectorIndex; } diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 175ef794..5e4597f3 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -1,7 +1,8 @@ #pragma once -#include "../Activity.h" #include +#include "../Activity.h" + class Bitmap; class SleepActivity final : public Activity { diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index d1639028..5f1ba924 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -71,7 +71,7 @@ void CategorySettingsActivity::loop() { if (visibleCount == 0) { return; // No visible settings } - + if (mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left)) { // Move to previous visible setting @@ -213,8 +213,7 @@ bool CategorySettingsActivity::shouldShowSetting(int index) const { return false; } // Hide "Select Sleep BMP" if sleep screen is not set to CUSTOM - if (settingsList[index].type == SettingType::ACTION && - strcmp(settingsList[index].name, "Select Sleep BMP") == 0) { + if (settingsList[index].type == SettingType::ACTION && strcmp(settingsList[index].name, "Select Sleep BMP") == 0) { return SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; } return true; @@ -270,7 +269,7 @@ void CategorySettingsActivity::render() const { // Calculate visible settings count and map selection const int visibleCount = getVisibleSettingsCount(); const int actualSelectedIndex = mapVisibleIndexToActualIndex(selectedSettingIndex); - + // Draw selection highlight int visibleIndex = 0; for (int i = 0; i < settingsCount; i++) { @@ -289,7 +288,7 @@ void CategorySettingsActivity::render() const { if (!shouldShowSetting(i)) { continue; } - + const int settingY = 60 + visibleIndex * 30; // 30 pixels between settings const bool isSelected = (i == actualSelectedIndex); @@ -329,7 +328,7 @@ void CategorySettingsActivity::render() const { const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected); } - + visibleIndex++; } diff --git a/src/activities/settings/RefreshFrequencySelectionActivity.cpp b/src/activities/settings/RefreshFrequencySelectionActivity.cpp index 461feeb4..aa76c13b 100644 --- a/src/activities/settings/RefreshFrequencySelectionActivity.cpp +++ b/src/activities/settings/RefreshFrequencySelectionActivity.cpp @@ -4,12 +4,12 @@ #include "CrossPointSettings.h" -RefreshFrequencySelectionActivity::RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, +RefreshFrequencySelectionActivity::RefreshFrequencySelectionActivity(GfxRenderer& renderer, + MappedInputManager& mappedInput, const std::function& onBack) : ListSelectionActivity( "RefreshFrequencySelection", renderer, mappedInput, "Select Refresh Frequency", - [this]() { return options.size(); }, - [this](size_t index) { return options[index]; }, + [this]() { return options.size(); }, [this](size_t index) { return options[index]; }, [this, onBack](size_t index) { if (index >= options.size()) { return; diff --git a/src/activities/settings/ScreenMarginSelectionActivity.cpp b/src/activities/settings/ScreenMarginSelectionActivity.cpp index 837c8f21..4de91470 100644 --- a/src/activities/settings/ScreenMarginSelectionActivity.cpp +++ b/src/activities/settings/ScreenMarginSelectionActivity.cpp @@ -7,8 +7,7 @@ ScreenMarginSelectionActivity::ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onBack) : ListSelectionActivity( - "ScreenMarginSelection", renderer, mappedInput, "Select Screen Margin", - [this]() { return options.size(); }, + "ScreenMarginSelection", renderer, mappedInput, "Select Screen Margin", [this]() { return options.size(); }, [this](size_t index) { return options[index]; }, [this, onBack](size_t index) { if (index >= options.size()) { diff --git a/src/activities/settings/ScreenMarginSelectionActivity.h b/src/activities/settings/ScreenMarginSelectionActivity.h index f1169cd6..3b381da7 100644 --- a/src/activities/settings/ScreenMarginSelectionActivity.h +++ b/src/activities/settings/ScreenMarginSelectionActivity.h @@ -13,5 +13,5 @@ class ScreenMarginSelectionActivity final : public ListSelectionActivity { public: explicit ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack); + const std::function& onBack); }; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 87be2b87..37217b1e 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -2,11 +2,10 @@ #include #include +#include #include -#include - #include "CalibreSettingsActivity.h" #include "CategorySettingsActivity.h" #include "CrossPointSettings.h" diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 508e18cd..eeddc8c6 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -16,7 +16,7 @@ class SettingsActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; bool updateRequired = false; - int selectedCategoryIndex = 0; // Currently selected category + int selectedCategoryIndex = 0; // Currently selected category bool hasSleepBmpsCached = false; // Cached result of sleep BMP check const std::function onGoHome; diff --git a/src/activities/settings/SleepBmpSelectionActivity.cpp b/src/activities/settings/SleepBmpSelectionActivity.cpp index 15d9c4ef..29f8fad5 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.cpp +++ b/src/activities/settings/SleepBmpSelectionActivity.cpp @@ -6,16 +6,15 @@ #include #include -#include "CrossPointSettings.h" #include "../../../lib/GfxRenderer/Bitmap.h" +#include "CrossPointSettings.h" namespace { void sortFileList(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { - return std::lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), - [](const char& char1, const char& char2) { - return std::tolower(char1) < std::tolower(char2); - }); + return std::lexicographical_compare( + begin(str1), end(str1), begin(str2), end(str2), + [](const char& char1, const char& char2) { return std::tolower(char1) < std::tolower(char2); }); }); } } // namespace @@ -23,8 +22,7 @@ void sortFileList(std::vector& strs) { SleepBmpSelectionActivity::SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onBack) : ListSelectionActivity( - "SleepBmpSelection", renderer, mappedInput, "Select Sleep BMP", - [this]() { return files.size(); }, + "SleepBmpSelection", renderer, mappedInput, "Select Sleep BMP", [this]() { return files.size(); }, [this](size_t index) { return files[index]; }, [this, onBack](size_t index) { if (index >= files.size()) { @@ -45,29 +43,28 @@ SleepBmpSelectionActivity::SleepBmpSelectionActivity(GfxRenderer& renderer, Mapp void SleepBmpSelectionActivity::loadFiles() { files.clear(); - + std::vector bmpFiles; - + auto dir = SdMan.open("/sleep"); if (dir && dir.isDirectory()) { dir.rewindDirectory(); char name[500]; - + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { if (file.isDirectory()) { file.close(); continue; } - + file.getName(name, sizeof(name)); auto filename = std::string(name); - - if (filename[0] == '.' || filename.length() < 4 || - filename.substr(filename.length() - 4) != ".bmp") { + + if (filename[0] == '.' || filename.length() < 4 || filename.substr(filename.length() - 4) != ".bmp") { file.close(); continue; } - + // Validate BMP Bitmap bitmap(file); if (bitmap.parseHeaders() != BmpReaderError::Ok) { @@ -75,15 +72,15 @@ void SleepBmpSelectionActivity::loadFiles() { continue; } file.close(); - + bmpFiles.emplace_back(filename); } dir.close(); - + // Sort alphabetically (case-insensitive) sortFileList(bmpFiles); } - + // Add "Random" as first option, then sorted BMP files files.emplace_back("Random"); files.insert(files.end(), bmpFiles.begin(), bmpFiles.end()); @@ -91,7 +88,7 @@ void SleepBmpSelectionActivity::loadFiles() { void SleepBmpSelectionActivity::loadItems() { loadFiles(); - + // Set initial selection based on saved setting if (SETTINGS.selectedSleepBmp[0] == '\0') { selectorIndex = 0; // "Random" is at index 0 @@ -111,4 +108,3 @@ void SleepBmpSelectionActivity::onExit() { ListSelectionActivity::onExit(); files.clear(); } - diff --git a/src/activities/settings/SleepBmpSelectionActivity.h b/src/activities/settings/SleepBmpSelectionActivity.h index dbb86349..d0c07fb7 100644 --- a/src/activities/settings/SleepBmpSelectionActivity.h +++ b/src/activities/settings/SleepBmpSelectionActivity.h @@ -17,4 +17,3 @@ class SleepBmpSelectionActivity final : public ListSelectionActivity { const std::function& onBack); void onExit() override; }; - diff --git a/src/activities/settings/SleepScreenSelectionActivity.cpp b/src/activities/settings/SleepScreenSelectionActivity.cpp index c3203630..ffc1fcd8 100644 --- a/src/activities/settings/SleepScreenSelectionActivity.cpp +++ b/src/activities/settings/SleepScreenSelectionActivity.cpp @@ -7,8 +7,7 @@ SleepScreenSelectionActivity::SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onBack) : ListSelectionActivity( - "SleepScreenSelection", renderer, mappedInput, "Select Sleep Screen", - [this]() { return options.size(); }, + "SleepScreenSelection", renderer, mappedInput, "Select Sleep Screen", [this]() { return options.size(); }, [this](size_t index) { return options[index]; }, [this, onBack](size_t index) { if (index >= options.size()) { diff --git a/src/activities/settings/SleepTimeoutSelectionActivity.cpp b/src/activities/settings/SleepTimeoutSelectionActivity.cpp index 866e99ba..a1bad04f 100644 --- a/src/activities/settings/SleepTimeoutSelectionActivity.cpp +++ b/src/activities/settings/SleepTimeoutSelectionActivity.cpp @@ -5,10 +5,9 @@ #include "CrossPointSettings.h" SleepTimeoutSelectionActivity::SleepTimeoutSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack) + const std::function& onBack) : ListSelectionActivity( - "SleepTimeoutSelection", renderer, mappedInput, "Select Time to Sleep", - [this]() { return options.size(); }, + "SleepTimeoutSelection", renderer, mappedInput, "Select Time to Sleep", [this]() { return options.size(); }, [this](size_t index) { return options[index]; }, [this, onBack](size_t index) { if (index >= options.size()) { From 81cca95dfe7bdfc700af508bd2d17a707a05b0f2 Mon Sep 17 00:00:00 2001 From: dpoulter Date: Sun, 1 Feb 2026 12:15:08 +0200 Subject: [PATCH 10/10] feat: add sleep screen selection --- src/CrossPointSettings.h | 8 +++++--- src/activities/settings/CategorySettingsActivity.cpp | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index d3057ea9..284710f2 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -114,9 +114,11 @@ class CrossPointSettings { }; // Reader screen margin options (pixel values) -#define SCREEN_MARGIN_DATA \ - X(5, "5 px") X(10, "10 px") X(15, "15 px") X(20, "20 px") X(25, "25 px") X(30, "30 px") X(35, "35 px") X(40, "40 " \ - "px") +#define SCREEN_MARGIN_DATA \ + X(5, "5 px") \ + X(10, "10 px") X(15, "15 px") X(20, "20 px") X(25, "25 px") X(30, "30 px") X(35, "35 px") X(40, \ + "40 " \ + "px") enum SCREEN_MARGIN { #define X(val, str) MARGIN_##val, diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 5f1ba924..cb9080d0 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -318,7 +318,7 @@ void CategorySettingsActivity::render() const { if (SETTINGS.selectedSleepBmp[0] != '\0') { valueText = SETTINGS.selectedSleepBmp; if (valueText.length() > 20) { - valueText = valueText.substr(0, 17) + "..."; + valueText.replace(17, std::string::npos, "..."); } } else { valueText = "Random";