From 77ae2a8dc0bf150392877688f5748ed1963332ab Mon Sep 17 00:00:00 2001 From: dpoulter Date: Tue, 27 Jan 2026 09:47:38 +0200 Subject: [PATCH] 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); +};