diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index edc2fc8..31a2c18 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -6,6 +6,9 @@ #include +// Initialize the static instance +CrossPointSettings CrossPointSettings::instance; + constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin"; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 01fc100..bf5a6ca 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -2,12 +2,31 @@ #include class CrossPointSettings { + private: + // Private constructor for singleton + CrossPointSettings() = default; + + // Static instance + static CrossPointSettings instance; + public: + // Delete copy constructor and assignment + CrossPointSettings(const CrossPointSettings&) = delete; + CrossPointSettings& operator=(const CrossPointSettings&) = delete; + // Sleep screen settings bool whiteSleepScreen = false; ~CrossPointSettings() = default; + // Get singleton instance + static CrossPointSettings& getInstance() { + return instance; + } + bool saveToFile() const; bool loadFromFile(); -}; \ No newline at end of file +}; + +// Helper macro to access settings +#define SETTINGS CrossPointSettings::getInstance() \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index bfb311f..22b0c74 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,6 +21,7 @@ #include "screens/EpubReaderScreen.h" #include "screens/FileSelectionScreen.h" #include "screens/FullScreenMessageScreen.h" +#include "screens/SettingsScreen.h" #include "screens/SleepScreen.h" #define SPI_FQ 40000000 @@ -42,7 +43,6 @@ InputManager inputManager; GfxRenderer renderer(einkDisplay); Screen* currentScreen; CrossPointState appState; -CrossPointSettings appSettings; // Fonts EpdFont bookerlyFont(&bookerly_2b); @@ -133,8 +133,8 @@ void waitForPowerRelease() { // Enter deep sleep mode void enterDeepSleep() { exitScreen(); - appSettings.saveToFile(); - enterNewScreen(new SleepScreen(renderer, inputManager, appSettings)); + SETTINGS.saveToFile(); + enterNewScreen(new SleepScreen(renderer, inputManager)); Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis()); delay(1000); // Allow Serial buffer to empty and display to update @@ -168,9 +168,14 @@ void onSelectEpubFile(const std::string& path) { } } +void onGoToSettings() { + exitScreen(); + enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome)); +} + void onGoHome() { exitScreen(); - enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile)); + enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); } void setup() { @@ -202,7 +207,7 @@ void setup() { // SD Card Initialization SD.begin(SD_SPI_CS, SPI, SPI_FQ); - appSettings.loadFromFile(); + SETTINGS.loadFromFile(); appState.loadFromFile(); if (!appState.openEpubPath.empty()) { auto epub = loadEpub(appState.openEpubPath); @@ -216,7 +221,7 @@ void setup() { } exitScreen(); - enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile)); + enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); diff --git a/src/screens/FileSelectionScreen.cpp b/src/screens/FileSelectionScreen.cpp index a911335..c006ba7 100644 --- a/src/screens/FileSelectionScreen.cpp +++ b/src/screens/FileSelectionScreen.cpp @@ -91,11 +91,16 @@ void FileSelectionScreen::handleInput() { } else { onSelect(basepath + files[selectorIndex]); } - } else if (inputManager.wasPressed(InputManager::BTN_BACK) && basepath != "/") { - basepath = basepath.substr(0, basepath.rfind('/')); - if (basepath.empty()) basepath = "/"; - loadFiles(); - updateRequired = true; + } else if (inputManager.wasPressed(InputManager::BTN_BACK)) { + if (basepath != "/") { + basepath = basepath.substr(0, basepath.rfind('/')); + if (basepath.empty()) basepath = "/"; + loadFiles(); + updateRequired = true; + } else { + // At root level, go to settings + onSettingsOpen(); + } } else if (prevPressed) { selectorIndex = (selectorIndex + files.size() - 1) % files.size(); updateRequired = true; @@ -123,6 +128,10 @@ void FileSelectionScreen::render() const { const auto pageWidth = GfxRenderer::getScreenWidth(); renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD); + // Help text + renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, + "Press BACK for Settings"); + if (files.empty()) { renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); } else { diff --git a/src/screens/FileSelectionScreen.h b/src/screens/FileSelectionScreen.h index f0edc3b..00947a5 100644 --- a/src/screens/FileSelectionScreen.h +++ b/src/screens/FileSelectionScreen.h @@ -17,6 +17,7 @@ class FileSelectionScreen final : public Screen { int selectorIndex = 0; bool updateRequired = false; const std::function onSelect; + const std::function onSettingsOpen; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -25,8 +26,9 @@ class FileSelectionScreen final : public Screen { public: explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, - const std::function& onSelect) - : Screen(renderer, inputManager), onSelect(onSelect) {} + const std::function& onSelect, + const std::function& onSettingsOpen) + : Screen(renderer, inputManager), onSelect(onSelect), onSettingsOpen(onSettingsOpen) {} void onEnter() override; void onExit() override; void handleInput() override; diff --git a/src/screens/SettingsScreen.cpp b/src/screens/SettingsScreen.cpp new file mode 100644 index 0000000..e16da05 --- /dev/null +++ b/src/screens/SettingsScreen.cpp @@ -0,0 +1,141 @@ +#include "SettingsScreen.h" + +#include + +#include "CrossPointSettings.h" +#include "config.h" + +// Define the static settings list +const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = { + {"White Splash Screen", &CrossPointSettings::whiteSleepScreen} +}; + +void SettingsScreen::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void SettingsScreen::onEnter() { + renderingMutex = xSemaphoreCreateMutex(); + + + + // Reset selection to first item + selectedSettingIndex = 0; + + // Trigger first update + updateRequired = true; + + xTaskCreate(&SettingsScreen::taskTrampoline, "SettingsScreenTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void SettingsScreen::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 SettingsScreen::handleInput() { + // Check for Confirm button to toggle setting + if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { + // Toggle the current setting + toggleCurrentSetting(); + + // Trigger a redraw of the entire screen + updateRequired = true; + return; // Return early to prevent further processing + } + + // Check for Back button to exit settings + if (inputManager.wasPressed(InputManager::BTN_BACK)) { + // Save settings and exit + SETTINGS.saveToFile(); + onGoHome(); + return; + } + + // Handle UP/DOWN navigation for multiple settings + if (inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT)) { + // Move selection up + if (selectedSettingIndex > 0) { + selectedSettingIndex--; + updateRequired = true; + } + } else if (inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT)) { + // Move selection down + if (selectedSettingIndex < settingsCount - 1) { + selectedSettingIndex++; + updateRequired = true; + } + } +} + +void SettingsScreen::toggleCurrentSetting() { + // Validate index + if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { + return; + } + + // Toggle the boolean value using the member pointer + bool currentValue = SETTINGS.*(settingsList[selectedSettingIndex].valuePtr); + SETTINGS.*(settingsList[selectedSettingIndex].valuePtr) = !currentValue; + + // Save settings when they change + SETTINGS.saveToFile(); +} + +void SettingsScreen::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void SettingsScreen::render() const { + renderer.clearScreen(); + + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageHeight = GfxRenderer::getScreenHeight(); + + // Draw header + renderer.drawCenteredText(READER_FONT_ID, 10, "Settings", true, BOLD); + + // We always have at least one setting + + // Draw all settings + for (int i = 0; i < settingsCount; i++) { + const int settingY = 60 + i * 30; // 30 pixels between settings + + // Draw selection indicator for the selected setting + if (i == selectedSettingIndex) { + renderer.drawText(UI_FONT_ID, 5, settingY, ">"); + } + + // Draw setting name and value + renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name); + bool value = SETTINGS.*(settingsList[i].valuePtr); + renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF"); + } + + // Draw help text + renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 40, + "Press OK to toggle, BACK to save & exit"); + + // Always use standard refresh for settings screen + renderer.displayBuffer(); +} diff --git a/src/screens/SettingsScreen.h b/src/screens/SettingsScreen.h new file mode 100644 index 0000000..e431f3b --- /dev/null +++ b/src/screens/SettingsScreen.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include + +#include +#include + +#include "Screen.h" + +class CrossPointSettings; + +// Structure to hold setting information +struct SettingInfo { + const char* name; // Display name of the setting + bool CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings +}; + +class SettingsScreen final : public Screen { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + int selectedSettingIndex = 0; // Currently selected setting + const std::function onGoHome; + + // Static settings list + static constexpr int settingsCount = 1; // Number of settings + static const SettingInfo settingsList[settingsCount]; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void toggleCurrentSetting(); + + public: + explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, + const std::function& onGoHome) + : Screen(renderer, inputManager), onGoHome(onGoHome) {} + void onEnter() override; + void onExit() override; + void handleInput() override; +}; diff --git a/src/screens/SleepScreen.cpp b/src/screens/SleepScreen.cpp index a64f500..e1ed0d7 100644 --- a/src/screens/SleepScreen.cpp +++ b/src/screens/SleepScreen.cpp @@ -16,7 +16,7 @@ void SleepScreen::onEnter() { renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); // Apply white screen if enabled in settings - if (!settings.whiteSleepScreen) { + if (!SETTINGS.whiteSleepScreen) { renderer.invertScreen(); } diff --git a/src/screens/SleepScreen.h b/src/screens/SleepScreen.h index f0dea52..918ac0c 100644 --- a/src/screens/SleepScreen.h +++ b/src/screens/SleepScreen.h @@ -5,10 +5,7 @@ class CrossPointSettings; class SleepScreen final : public Screen { public: - explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager, const CrossPointSettings& settings) - : Screen(renderer, inputManager), settings(settings) {} + explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) + : Screen(renderer, inputManager) {} void onEnter() override; - - private: - const CrossPointSettings& settings; };