Compare commits

...

20 Commits

Author SHA1 Message Date
Daniel Poulter
5f7c178c76
Merge acddb852bc into 78d6e5931c 2026-02-03 17:09:39 -05:00
Jake Kenneally
78d6e5931c
fix: Correct debugging_monitor.py script instructions (#676)
Some checks failed
CI / build (push) Has been cancelled
## Summary

**What is the goal of this PR?**
- Minor correction to the `debugging_monitor.py` script instructions

**What changes are included?**
- `pyserial` should be installed, NOT `serial`, which is a [different
lib](https://pypi.org/project/serial/)
- Added macOS serial port

## Additional Context

- Just a minor docs update. I can confirm the debugging script is
working great on macOS

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-02-04 00:33:20 +03:00
Luke Stein
dac11c3fdd
fix: Correct instruction text to match actual button text (#672)
## Summary

* Instruction text says "Press OK to scan again" but button label is
actually "Connect" (not OK)
* Corrects instruction text

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-04 00:32:52 +03:00
dpoulter
acddb852bc feat: add sleep screen selection 2026-02-01 14:44:11 +02:00
dpoulter
bda8102720 feat: add sleep screen selection 2026-02-01 14:39:37 +02:00
dpoulter
4ccd6e09d8 feat: add sleep screen selection 2026-02-01 14:34:58 +02:00
dpoulter
81cca95dfe feat: add sleep screen selection 2026-02-01 12:15:08 +02:00
dpoulter
c5e5b043a9 Merge upstream/master into feature/sleep-bmp-selection
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-01 12:08:36 +02:00
dpoulter
2d8e283b7b feat: add sleep screen selection 2026-02-01 12:05:15 +02:00
dpoulter
78ebcf7856 feat: add sleep screen selection 2026-02-01 12:03:01 +02:00
dpoulter
d4036bf1f7 feat: add sleep screen selection 2026-02-01 11:08:35 +02:00
dpoulter
da142c362e feat: add sleep screen selection 2026-02-01 09:55:00 +02:00
dpoulter
c3ce6e11da Merge master into feature/sleep-bmp-selection
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-01 08:57:37 +02:00
dpoulter
77ae2a8dc0 feat: add sleep screen selection 2026-01-27 09:47:38 +02:00
dpoulter
5c739fa530 feat: add sleep screen selection 2026-01-23 08:26:16 +02:00
dpoulter
0958b2e1d3 feat: add sleep screen selection 2026-01-21 21:07:50 +02:00
dpoulter
ab906c499b feat: add sleep screen selection 2026-01-21 20:36:51 +02:00
dpoulter
a481ec1e1c feat: add sleep screen selection 2026-01-21 16:03:22 +02:00
dpoulter
d83fa6953c feat: add sleep screen selection 2026-01-21 15:58:51 +02:00
dpoulter
3e71e74c66 Add sleep BMP selection feature
- Add selectedSleepBmp field to CrossPointSettings to store selected BMP filename
- Create SleepBmpSelectionActivity for selecting BMP files from /sleep folder
- Add conditional 'Select Sleep BMP' setting that only shows when /sleep folder has BMPs
- Modify SleepActivity to use selected BMP instead of random selection when set
- Cache sleep BMP check result to avoid mutex conflicts during rendering
2026-01-08 17:38:24 +01:00
24 changed files with 970 additions and 51 deletions

View File

@ -102,13 +102,18 @@ After flashing the new features, its recommended to capture detailed logs fro
First, make sure all required Python packages are installed:
```python
python3 -m pip install serial colorama matplotlib
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
Minor adjustments may be required for Windows.
## Internals

View File

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

View File

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

View File

@ -0,0 +1,155 @@
#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;
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,78 @@
#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
*/
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;
// 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; }
};

View File

@ -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<std::string> files;
char name[500];
// collect all valid BMP files
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
if (file.isDirectory()) {
file.close();

View File

@ -1,4 +1,6 @@
#pragma once
#include <SDCardManager.h>
#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;
};

View File

@ -520,7 +520,7 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again");
} else {
// Calculate how many networks we can display
constexpr int startY = 60;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
#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::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)
}
}

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;
protected:
void loadItems() override;
public:
explicit RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack);
};

View File

@ -0,0 +1,32 @@
#include "ScreenMarginSelectionActivity.h"
#include <cstring>
#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;
}
SETTINGS.screenMargin = static_cast<uint8_t>(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;
}
}

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;
protected:
void loadItems() override;
public:
explicit ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack);
};

View File

@ -2,34 +2,39 @@
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <cstring>
#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<SettingsActivity*>(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",

View File

@ -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<void()> onGoHome;
static constexpr int categoryCount = 4;

View File

@ -0,0 +1,110 @@
#include "SleepBmpSelectionActivity.h"
#include <SDCardManager.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#include "../../../lib/GfxRenderer/Bitmap.h"
#include "CrossPointSettings.h"
namespace {
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),
[](const char& char1, const char& char2) { return std::tolower(char1) < std::tolower(char2); });
});
}
} // namespace
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();
std::vector<std::string> 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();
}

View File

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

View File

@ -0,0 +1,32 @@
#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;
}
SETTINGS.sleepScreen = static_cast<uint8_t>(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"
}
}

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;
protected:
void loadItems() override;
public:
explicit SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack);
};

View File

@ -0,0 +1,32 @@
#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;
}
SETTINGS.sleepTimeout = static_cast<uint8_t>(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)
}
}

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