mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +03:00
feat: add sleep screen selection
This commit is contained in:
parent
5c739fa530
commit
77ae2a8dc0
@ -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]);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
162
src/activities/ListSelectionActivity.cpp
Normal file
162
src/activities/ListSelectionActivity.cpp
Normal 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();
|
||||
}
|
||||
88
src/activities/ListSelectionActivity.h
Normal file
88
src/activities/ListSelectionActivity.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
17
src/activities/settings/RefreshFrequencySelectionActivity.h
Normal file
17
src/activities/settings/RefreshFrequencySelectionActivity.h
Normal 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);
|
||||
};
|
||||
47
src/activities/settings/ScreenMarginSelectionActivity.cpp
Normal file
47
src/activities/settings/ScreenMarginSelectionActivity.cpp
Normal 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"
|
||||
}
|
||||
}
|
||||
17
src/activities/settings/ScreenMarginSelectionActivity.h
Normal file
17
src/activities/settings/ScreenMarginSelectionActivity.h
Normal 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);
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
37
src/activities/settings/SleepScreenSelectionActivity.cpp
Normal file
37
src/activities/settings/SleepScreenSelectionActivity.cpp
Normal 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"
|
||||
}
|
||||
}
|
||||
17
src/activities/settings/SleepScreenSelectionActivity.h
Normal file
17
src/activities/settings/SleepScreenSelectionActivity.h
Normal 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);
|
||||
};
|
||||
37
src/activities/settings/SleepTimeoutSelectionActivity.cpp
Normal file
37
src/activities/settings/SleepTimeoutSelectionActivity.cpp
Normal 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)
|
||||
}
|
||||
}
|
||||
17
src/activities/settings/SleepTimeoutSelectionActivity.h
Normal file
17
src/activities/settings/SleepTimeoutSelectionActivity.h
Normal 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);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user