diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 232c7c57..a46205d8 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,8 +22,23 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 23; +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 { @@ -57,10 +72,10 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, hyphenationEnabled); + serialization::writeString(outputFile, std::string(selectedSleepBmp)); serialization::writeString(outputFile, std::string(opdsUsername)); serialization::writeString(outputFile, std::string(opdsPassword)); serialization::writePod(outputFile, sleepScreenCoverFilter); - // New fields added at end for backward compatibility outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -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; @@ -132,6 +152,13 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, hyphenationEnabled); 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; { std::string usernameStr; serialization::readString(inputFile, usernameStr); @@ -148,7 +175,6 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); if (++settingsRead >= fileSettingsCount) break; - // New fields added at end for backward compatibility } while (false); inputFile.close(); @@ -265,3 +291,64 @@ 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 +}; + +const char* const SCREEN_MARGIN_OPTIONS[] = { +#define X(val, str) str, + SCREEN_MARGIN_DATA +#undef X +}; +} // namespace + +const char* CrossPointSettings::getRefreshFrequencyString(uint8_t value) { + if (value < REFRESH_FREQUENCY_COUNT) { + return REFRESH_FREQUENCY_OPTIONS[value]; + } + return REFRESH_FREQUENCY_OPTIONS[REFRESH_15]; +} + +const char* CrossPointSettings::getSleepScreenString(uint8_t value) { + if (value < SLEEP_SCREEN_MODE_COUNT) { + return SLEEP_SCREEN_OPTIONS[value]; + } + return SLEEP_SCREEN_OPTIONS[DARK]; +} + +const char* CrossPointSettings::getSleepTimeoutString(uint8_t value) { + if (value < SLEEP_TIMEOUT_COUNT) { + return SLEEP_TIMEOUT_OPTIONS[value]; + } + return SLEEP_TIMEOUT_OPTIONS[SLEEP_10_MIN]; +} + +const char* CrossPointSettings::getScreenMarginString(uint8_t index) { + if (index < SCREEN_MARGIN_COUNT) { + return SCREEN_MARGIN_OPTIONS[index]; + } + return SCREEN_MARGIN_OPTIONS[MARGIN_5]; +} + +uint8_t CrossPointSettings::getScreenMarginPixels() const { + if (screenMargin < SCREEN_MARGIN_COUNT) { + return SCREEN_MARGIN_PIXEL_VALUES[screenMargin]; + } + return SCREEN_MARGIN_PIXEL_VALUES[MARGIN_5]; +} diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c450d348..e67f2093 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,52 @@ 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 + }; + +// 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 }; // Short power button press actions @@ -127,8 +167,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] = ""; @@ -137,6 +177,8 @@ class CrossPointSettings { uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press chapter skip on side buttons uint8_t longPressChapterSkip = 1; + // Selected sleep BMP filename (empty means random selection) + char selectedSleepBmp[256] = ""; ~CrossPointSettings() = default; @@ -151,6 +193,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 const char* getSleepScreenString(uint8_t value); + static const char* getSleepTimeoutString(uint8_t value); + static const char* getScreenMarginString(uint8_t index); + /** Returns pixel margin for current screenMargin index (e.g. 5, 10, 15...). */ + uint8_t getScreenMarginPixels() const; + 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..7e66b7b4 --- /dev/null +++ b/src/activities/ListSelectionActivity.cpp @@ -0,0 +1,155 @@ +#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; + + 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..7b7d1371 --- /dev/null +++ b/src/activities/ListSelectionActivity.h @@ -0,0 +1,78 @@ +#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 + */ +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; + + // 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; } +}; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 7ffc5851..270e8c2b 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -33,13 +33,33 @@ void SleepActivity::onEnter() { renderDefaultSleepScreen(); } +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 { - // Check if we have a /sleep directory auto dir = SdMan.open("/sleep"); if (dir && dir.isDirectory()) { + if (renderSelectedSleepBmp(dir)) return; + 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(); diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 87df8ba1..9978706e 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -1,4 +1,6 @@ #pragma once +#include + #include "../Activity.h" class Bitmap; @@ -15,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/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 5ccfb4fe..be9154a8 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -342,17 +342,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 eb1a9eef..85ccb422 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 7fd5ef5f..cb9080d0 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -11,6 +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) { @@ -61,24 +66,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 @@ -127,6 +148,46 @@ 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(); + enterNewActivity(new SleepBmpSelectionActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } } else { return; @@ -147,6 +208,56 @@ 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 +266,31 @@ void CategorySettingsActivity::render() const { renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); - // Draw selection highlight - renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); + // Calculate visible settings count and map selection + const int visibleCount = getVisibleSettingsCount(); + const int actualSelectedIndex = mapVisibleIndexToActualIndex(selectedSettingIndex); - // Draw all settings + // Draw selection highlight + 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 +305,31 @@ 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 = 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) { + if (SETTINGS.selectedSleepBmp[0] != '\0') { + valueText = SETTINGS.selectedSleepBmp; + if (valueText.length() > 20) { + valueText.replace(17, std::string::npos, "..."); + } + } 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/RefreshFrequencySelectionActivity.cpp b/src/activities/settings/RefreshFrequencySelectionActivity.cpp new file mode 100644 index 00000000..aa76c13b --- /dev/null +++ b/src/activities/settings/RefreshFrequencySelectionActivity.cpp @@ -0,0 +1,35 @@ +#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::REFRESH_FREQUENCY_COUNT; i++) { + options.push_back(CrossPointSettings::getRefreshFrequencyString(i)); + } +} + +void RefreshFrequencySelectionActivity::loadItems() { + 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..427a8c02 --- /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; + + protected: + void loadItems() override; + + 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..4de91470 --- /dev/null +++ b/src/activities/settings/ScreenMarginSelectionActivity.cpp @@ -0,0 +1,32 @@ +#include "ScreenMarginSelectionActivity.h" + +#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; + } + SETTINGS.screenMargin = static_cast(index); + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No options available") { + for (uint8_t i = 0; i < CrossPointSettings::SCREEN_MARGIN_COUNT; i++) { + options.push_back(CrossPointSettings::getScreenMarginString(i)); + } +} + +void ScreenMarginSelectionActivity::loadItems() { + if (SETTINGS.screenMargin < options.size()) { + selectorIndex = SETTINGS.screenMargin; + } else { + selectorIndex = 0; + } +} diff --git a/src/activities/settings/ScreenMarginSelectionActivity.h b/src/activities/settings/ScreenMarginSelectionActivity.h new file mode 100644 index 00000000..3b381da7 --- /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; + + protected: + void loadItems() override; + + 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 7316db05..37217b1e 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -2,34 +2,39 @@ #include #include +#include +#include + +#include "CalibreSettingsActivity.h" #include "CategorySettingsActivity.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" +#include "SleepBmpSelectionActivity.h" #include "fontIds.h" const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; namespace { -constexpr int displaySettingsCount = 6; +constexpr int displaySettingsCount = 7; 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("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}), 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), @@ -56,6 +61,40 @@ const SettingInfo systemSettings[systemSettingsCount] = { 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(); @@ -65,10 +104,10 @@ void SettingsActivity::onEnter() { Activity::onEnter(); renderingMutex = xSemaphoreCreateMutex(); + hasSleepBmpsCached = checkSleepBmps(); + // Reset selection to first category selectedCategoryIndex = 0; - - // Trigger first update updateRequired = true; xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask", diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 821dda42..eeddc8c6 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -16,7 +16,8 @@ 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; static constexpr int categoryCount = 4; diff --git a/src/activities/settings/SleepBmpSelectionActivity.cpp b/src/activities/settings/SleepBmpSelectionActivity.cpp new file mode 100644 index 00000000..29f8fad5 --- /dev/null +++ b/src/activities/settings/SleepBmpSelectionActivity.cpp @@ -0,0 +1,110 @@ +#include "SleepBmpSelectionActivity.h" + +#include + +#include +#include +#include + +#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); }); + }); +} +} // namespace + +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(); + + 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") { + 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); + } + + // Add "Random" as first option, then sorted BMP files + files.emplace_back("Random"); + files.insert(files.end(), bmpFiles.begin(), bmpFiles.end()); +} + +void SleepBmpSelectionActivity::loadItems() { + loadFiles(); + + // Set initial selection based on saved setting + 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; + } + } + } +} + +void SleepBmpSelectionActivity::onExit() { + ListSelectionActivity::onExit(); + files.clear(); +} diff --git a/src/activities/settings/SleepBmpSelectionActivity.h b/src/activities/settings/SleepBmpSelectionActivity.h new file mode 100644 index 00000000..d0c07fb7 --- /dev/null +++ b/src/activities/settings/SleepBmpSelectionActivity.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include + +#include "../ListSelectionActivity.h" + +class SleepBmpSelectionActivity final : public ListSelectionActivity { + std::vector files; + void loadFiles(); + + protected: + void loadItems() override; + + public: + explicit SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack); + void onExit() override; +}; diff --git a/src/activities/settings/SleepScreenSelectionActivity.cpp b/src/activities/settings/SleepScreenSelectionActivity.cpp new file mode 100644 index 00000000..ffc1fcd8 --- /dev/null +++ b/src/activities/settings/SleepScreenSelectionActivity.cpp @@ -0,0 +1,32 @@ +#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; + } + SETTINGS.sleepScreen = static_cast(index); + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No options available") { + for (uint8_t i = 0; i < CrossPointSettings::SLEEP_SCREEN_MODE_COUNT; i++) { + options.push_back(CrossPointSettings::getSleepScreenString(i)); + } +} + +void SleepScreenSelectionActivity::loadItems() { + 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..fa18c37d --- /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; + + protected: + void loadItems() override; + + 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..a1bad04f --- /dev/null +++ b/src/activities/settings/SleepTimeoutSelectionActivity.cpp @@ -0,0 +1,32 @@ +#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; + } + SETTINGS.sleepTimeout = static_cast(index); + SETTINGS.saveToFile(); + onBack(); + }, + onBack, "No options available") { + for (uint8_t i = 0; i < CrossPointSettings::SLEEP_TIMEOUT_COUNT; i++) { + options.push_back(CrossPointSettings::getSleepTimeoutString(i)); + } +} + +void SleepTimeoutSelectionActivity::loadItems() { + 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); +};