diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index c8118a2..9c05208 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -9,7 +9,7 @@ #include "Page.h" #include "parsers/ChapterHtmlSlimParser.h" -constexpr uint8_t SECTION_FILE_VERSION = 4; +constexpr uint8_t SECTION_FILE_VERSION = 5; void Section::onPageComplete(std::unique_ptr page) { const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; @@ -24,7 +24,8 @@ void Section::onPageComplete(std::unique_ptr page) { } void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop, - const int marginRight, const int marginBottom, const int marginLeft) const { + const int marginRight, const int marginBottom, const int marginLeft, + const bool extraParagraphSpacing) const { std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str()); serialization::writePod(outputFile, SECTION_FILE_VERSION); serialization::writePod(outputFile, fontId); @@ -33,12 +34,14 @@ void Section::writeCacheMetadata(const int fontId, const float lineCompression, serialization::writePod(outputFile, marginRight); serialization::writePod(outputFile, marginBottom); serialization::writePod(outputFile, marginLeft); + serialization::writePod(outputFile, extraParagraphSpacing); serialization::writePod(outputFile, pageCount); outputFile.close(); } bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop, - const int marginRight, const int marginBottom, const int marginLeft) { + const int marginRight, const int marginBottom, const int marginLeft, + const bool extraParagraphSpacing) { if (!SD.exists(cachePath.c_str())) { return false; } @@ -63,15 +66,18 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft; float fileLineCompression; + bool fileExtraParagraphSpacing; serialization::readPod(inputFile, fileFontId); serialization::readPod(inputFile, fileLineCompression); serialization::readPod(inputFile, fileMarginTop); serialization::readPod(inputFile, fileMarginRight); serialization::readPod(inputFile, fileMarginBottom); serialization::readPod(inputFile, fileMarginLeft); + serialization::readPod(inputFile, fileExtraParagraphSpacing); if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop || - marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) { + marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft || + extraParagraphSpacing != fileExtraParagraphSpacing) { inputFile.close(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); clearCache(); @@ -107,7 +113,8 @@ bool Section::clearCache() const { } bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop, - const int marginRight, const int marginBottom, const int marginLeft) { + const int marginRight, const int marginBottom, const int marginLeft, + const bool extraParagraphSpacing) { const auto localPath = epub->getSpineItem(spineIndex); // TODO: Should we get rid of this file all together? @@ -128,7 +135,7 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath; ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, - marginBottom, marginLeft, + marginBottom, marginLeft, extraParagraphSpacing, [this](std::unique_ptr page) { this->onPageComplete(std::move(page)); }); success = visitor.parseAndBuildPages(); @@ -138,7 +145,7 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, return false; } - writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft); + writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, extraParagraphSpacing); return true; } diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 4c98fbe..35a17df 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -13,7 +13,7 @@ class Section { std::string cachePath; void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, - int marginLeft) const; + int marginLeft, bool extraParagraphSpacing) const; void onPageComplete(std::unique_ptr page); public: @@ -26,10 +26,10 @@ class Section { } ~Section() = default; bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, - int marginLeft); + int marginLeft, bool extraParagraphSpacing); void setupCacheDir() const; bool clearCache() const; bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, - int marginLeft); + int marginLeft, bool extraParagraphSpacing); std::unique_ptr loadPageFromSD() const; }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index c22fcf2..3c1f5ca 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -274,6 +274,8 @@ void ChapterHtmlSlimParser::makePages() { currentTextBlock->layoutAndExtractLines( renderer, fontId, marginLeft + marginRight, [this](const std::shared_ptr& textBlock) { addLineToPage(textBlock); }); - // Extra paragrpah spacing - currentPageNextY += lineHeight / 2; + // Extra paragraph spacing if enabled + if (extraParagraphSpacing) { + currentPageNextY += lineHeight / 2; + } } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 9791f50..67ed5a1 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -35,6 +35,7 @@ class ChapterHtmlSlimParser { int marginRight; int marginBottom; int marginLeft; + bool extraParagraphSpacing; void startNewTextBlock(TextBlock::BLOCK_STYLE style); void makePages(); @@ -46,7 +47,8 @@ class ChapterHtmlSlimParser { public: explicit ChapterHtmlSlimParser(const char* filepath, GfxRenderer& renderer, const int fontId, const float lineCompression, const int marginTop, const int marginRight, - const int marginBottom, const int marginLeft, + const int marginBottom, const int marginLeft, + const bool extraParagraphSpacing, const std::function)>& completePageFn) : filepath(filepath), renderer(renderer), @@ -56,6 +58,7 @@ class ChapterHtmlSlimParser { marginRight(marginRight), marginBottom(marginBottom), marginLeft(marginLeft), + extraParagraphSpacing(extraParagraphSpacing), completePageFn(completePageFn) {} ~ChapterHtmlSlimParser() = default; bool parseAndBuildPages(); diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp new file mode 100644 index 0000000..d5de7a9 --- /dev/null +++ b/src/CrossPointSettings.cpp @@ -0,0 +1,65 @@ +#include "CrossPointSettings.h" + +#include +#include +#include + +#include +#include + +// Initialize the static instance +CrossPointSettings CrossPointSettings::instance; + +constexpr uint8_t SETTINGS_FILE_VERSION = 1; +constexpr uint8_t SETTINGS_COUNT = 2; +constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin"; + +bool CrossPointSettings::saveToFile() const { + // Make sure the directory exists + SD.mkdir("/.crosspoint"); + + std::ofstream outputFile(SETTINGS_FILE); + serialization::writePod(outputFile, SETTINGS_FILE_VERSION); + serialization::writePod(outputFile, SETTINGS_COUNT); + serialization::writePod(outputFile, whiteSleepScreen); + serialization::writePod(outputFile, extraParagraphSpacing); + outputFile.close(); + + Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); + return true; +} + +bool CrossPointSettings::loadFromFile() { + if (!SD.exists(SETTINGS_FILE + 3)) { // +3 to skip "/sd" prefix + Serial.printf("[%lu] [CPS] Settings file does not exist, using defaults\n", millis()); + return false; + } + + std::ifstream inputFile(SETTINGS_FILE); + + uint8_t version; + serialization::readPod(inputFile, version); + if (version != SETTINGS_FILE_VERSION) { + Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version); + inputFile.close(); + return false; + } + + uint8_t fileSettingsCount = 0; + serialization::readPod(inputFile, fileSettingsCount); + + // load settings that exist + switch (fileSettingsCount) { + case 1: + serialization::readPod(inputFile, whiteSleepScreen); + break; + case 2: + serialization::readPod(inputFile, whiteSleepScreen); + serialization::readPod(inputFile, extraParagraphSpacing); + break; + } + + inputFile.close(); + Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis()); + return true; +} diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h new file mode 100644 index 0000000..43aba9f --- /dev/null +++ b/src/CrossPointSettings.h @@ -0,0 +1,36 @@ +#pragma once +#include +#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 + uint8_t whiteSleepScreen = 0; + + // Text rendering settings + uint8_t extraParagraphSpacing = 1; + + ~CrossPointSettings() = default; + + // Get singleton instance + static CrossPointSettings& getInstance() { + return instance; + } + + bool saveToFile() const; + bool loadFromFile(); +}; + +// Helper macro to access settings +#define SETTINGS CrossPointSettings::getInstance() diff --git a/src/main.cpp b/src/main.cpp index f13e21c..7d9286b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,12 +14,14 @@ #include #include "Battery.h" +#include "CrossPointSettings.h" #include "CrossPointState.h" #include "config.h" #include "screens/BootLogoScreen.h" #include "screens/EpubReaderScreen.h" #include "screens/FileSelectionScreen.h" #include "screens/FullScreenMessageScreen.h" +#include "screens/SettingsScreen.h" #include "screens/SleepScreen.h" #define SPI_FQ 40000000 @@ -58,9 +60,9 @@ EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font); // Power button timing // Time required to confirm boot from sleep -constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000; +constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 500; // Time required to enter sleep mode -constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000; +constexpr unsigned long POWER_BUTTON_SLEEP_MS = 500; std::unique_ptr loadEpub(const std::string& path) { if (!SD.exists(path.c_str())) { @@ -165,9 +167,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() { @@ -199,6 +206,7 @@ void setup() { // SD Card Initialization SD.begin(SD_SPI_CS, SPI, SPI_FQ); + SETTINGS.loadFromFile(); appState.loadFromFile(); if (!appState.openEpubPath.empty()) { auto epub = loadEpub(appState.openEpubPath); @@ -212,7 +220,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/EpubReaderScreen.cpp b/src/screens/EpubReaderScreen.cpp index d0a3103..e914d07 100644 --- a/src/screens/EpubReaderScreen.cpp +++ b/src/screens/EpubReaderScreen.cpp @@ -5,6 +5,7 @@ #include #include "Battery.h" +#include "CrossPointSettings.h" #include "EpubReaderChapterSelectionScreen.h" #include "config.h" @@ -205,7 +206,7 @@ void EpubReaderScreen::renderScreen() { Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); section = std::unique_ptr
(new Section(epub, currentSpineIndex, renderer)); if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, - marginLeft)) { + marginLeft, SETTINGS.extraParagraphSpacing)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); { @@ -228,7 +229,7 @@ void EpubReaderScreen::renderScreen() { section->setupCacheDir(); if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, - marginLeft)) { + marginLeft, SETTINGS.extraParagraphSpacing)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; 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..da744b9 --- /dev/null +++ b/src/screens/SettingsScreen.cpp @@ -0,0 +1,143 @@ +#include "SettingsScreen.h" + +#include + +#include "CrossPointSettings.h" +#include "config.h" + +// Define the static settings list + +const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = { + {"White Sleep Screen", &CrossPointSettings::whiteSleepScreen}, + {"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing} +}; + +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 - 30, + "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..c7d6a5e --- /dev/null +++ b/src/screens/SettingsScreen.h @@ -0,0 +1,43 @@ +#pragma once +#include +#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 + uint8_t 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 = 2; // 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 cbcb411..e1ed0d7 100644 --- a/src/screens/SleepScreen.cpp +++ b/src/screens/SleepScreen.cpp @@ -4,6 +4,7 @@ #include "config.h" #include "images/CrossLarge.h" +#include "CrossPointSettings.h" void SleepScreen::onEnter() { const auto pageWidth = GfxRenderer::getScreenWidth(); @@ -13,6 +14,11 @@ void SleepScreen::onEnter() { renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128); renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); - renderer.invertScreen(); + + // Apply white screen if enabled in settings + if (!SETTINGS.whiteSleepScreen) { + renderer.invertScreen(); + } + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); }