mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Merge acddb852bc into f67c544e16
This commit is contained in:
commit
5591707746
@ -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];
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
155
src/activities/ListSelectionActivity.cpp
Normal file
155
src/activities/ListSelectionActivity.cpp
Normal 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();
|
||||
}
|
||||
78
src/activities/ListSelectionActivity.h
Normal file
78
src/activities/ListSelectionActivity.h
Normal 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; }
|
||||
};
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
protected:
|
||||
void loadItems() override;
|
||||
|
||||
public:
|
||||
explicit RefreshFrequencySelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onBack);
|
||||
};
|
||||
32
src/activities/settings/ScreenMarginSelectionActivity.cpp
Normal file
32
src/activities/settings/ScreenMarginSelectionActivity.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
protected:
|
||||
void loadItems() override;
|
||||
|
||||
public:
|
||||
explicit ScreenMarginSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onBack);
|
||||
};
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
110
src/activities/settings/SleepBmpSelectionActivity.cpp
Normal file
110
src/activities/settings/SleepBmpSelectionActivity.cpp
Normal 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();
|
||||
}
|
||||
19
src/activities/settings/SleepBmpSelectionActivity.h
Normal file
19
src/activities/settings/SleepBmpSelectionActivity.h
Normal 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;
|
||||
};
|
||||
32
src/activities/settings/SleepScreenSelectionActivity.cpp
Normal file
32
src/activities/settings/SleepScreenSelectionActivity.cpp
Normal 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"
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
protected:
|
||||
void loadItems() override;
|
||||
|
||||
public:
|
||||
explicit SleepScreenSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onBack);
|
||||
};
|
||||
32
src/activities/settings/SleepTimeoutSelectionActivity.cpp
Normal file
32
src/activities/settings/SleepTimeoutSelectionActivity.cpp
Normal 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)
|
||||
}
|
||||
}
|
||||
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