feat: add sleep screen selection

This commit is contained in:
dpoulter 2026-01-27 09:47:38 +02:00
parent 5c739fa530
commit 77ae2a8dc0
16 changed files with 607 additions and 198 deletions

View File

@ -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]);
}

View File

@ -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;

View File

@ -0,0 +1,162 @@
#include "ListSelectionActivity.h"
#include <GfxRenderer.h>
#include "MappedInputManager.h"
#include "fontIds.h"
void ListSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<ListSelectionActivity*>(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<int>(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();
}

View File

@ -0,0 +1,88 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include <vector>
#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<size_t()> getItemCount;
std::function<std::string(size_t)> getItemText;
std::function<void(size_t)> onItemSelected;
std::function<void()> onBack;
std::function<void(size_t, int, int, bool)> 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<size_t()> getItemCount,
std::function<std::string(size_t)> getItemText,
std::function<void(size_t)> onItemSelected,
std::function<void()> 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<void(size_t, int, int, bool)> renderer) {
customRenderItem = renderer;
}
};

View File

@ -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<uint8_t>(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;

View File

@ -0,0 +1,37 @@
#include "RefreshFrequencySelectionActivity.h"
#include <cstring>
#include "CrossPointSettings.h"
RefreshFrequencySelectionActivity::RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& 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<uint8_t>(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)
}
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include "../ListSelectionActivity.h"
class RefreshFrequencySelectionActivity final : public ListSelectionActivity {
std::vector<std::string> options; // Refresh frequency options
protected:
void loadItems() override; // Called by base class onEnter
public:
explicit RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack);
};

View File

@ -0,0 +1,47 @@
#include "ScreenMarginSelectionActivity.h"
#include <cstring>
#include <sstream>
#include "CrossPointSettings.h"
ScreenMarginSelectionActivity::ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& 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<uint8_t>((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"
}
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include "../ListSelectionActivity.h"
class ScreenMarginSelectionActivity final : public ListSelectionActivity {
std::vector<std::string> options; // Screen margin options
protected:
void loadItems() override; // Called by base class onEnter
public:
explicit ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack);
};

View File

@ -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

View File

@ -1,25 +1,15 @@
#include "SleepBmpSelectionActivity.h"
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#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<std::string>& 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<std::string>& strs) {
}
} // namespace
void SleepBmpSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<SleepBmpSelectionActivity*>(param);
self->displayTaskLoop();
}
SleepBmpSelectionActivity::SleepBmpSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& 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<int>(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();
}

View File

@ -1,34 +1,20 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include <vector>
#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<std::string> 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<void()> 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<void()>& onBack)
: Activity("SleepBmpSelection", renderer, mappedInput), onBack(onBack) {}
void onEnter() override;
const std::function<void()>& onBack);
void onExit() override;
void loop() override;
};

View File

@ -0,0 +1,37 @@
#include "SleepScreenSelectionActivity.h"
#include <cstring>
#include "CrossPointSettings.h"
SleepScreenSelectionActivity::SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& 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<uint8_t>(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"
}
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include "../ListSelectionActivity.h"
class SleepScreenSelectionActivity final : public ListSelectionActivity {
std::vector<std::string> options; // Sleep screen mode options
protected:
void loadItems() override; // Called by base class onEnter
public:
explicit SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack);
};

View File

@ -0,0 +1,37 @@
#include "SleepTimeoutSelectionActivity.h"
#include <cstring>
#include "CrossPointSettings.h"
SleepTimeoutSelectionActivity::SleepTimeoutSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& 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<uint8_t>(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)
}
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include "../ListSelectionActivity.h"
class SleepTimeoutSelectionActivity final : public ListSelectionActivity {
std::vector<std::string> options;
protected:
void loadItems() override;
public:
explicit SleepTimeoutSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack);
};