diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 83832a1..6d14d31 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -152,6 +152,18 @@ bool Epub::load() { return false; } + // determine size of spine items + size_t spineItemsCount = getSpineItemsCount(); + size_t spineItemsSize = 0; + for (size_t i = 0; i < spineItemsCount; i++) { + std::string spineItem = getSpineItem(i); + size_t s = 0; + getItemSize(spineItem, &s); + spineItemsSize += s; + cumulativeSpineItemSize.emplace_back(spineItemsSize); + } + Serial.printf("[%lu] [EBP] Book size: %u\n", millis(), spineItemsSize); + Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); return true; @@ -262,14 +274,11 @@ int Epub::getSpineItemsCount() const { return spine.size() + virtualCount; } -std::string Epub::getSpineItem(const int spineIndex) const { - if (spineIndex < 0) { - Serial.printf("[%lu] [EBP] getSpineItem index:%d is negative\n", millis(), spineIndex); - return ""; - } +size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); } +std::string Epub::getSpineItem(const int spineIndex) const { // Normal spine item - if (spineIndex < static_cast(spine.size())) { + if (spineIndex >= 0 && spineIndex < static_cast(spine.size())) { return contentBasePath + spine.at(spineIndex).second; } @@ -282,7 +291,10 @@ std::string Epub::getSpineItem(const int spineIndex) const { } Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex); - return ""; + + // Return empty string instead of reference to avoid issues + static std::string emptyString = ""; + return emptyString; } EpubTocEntry& Epub::getTocItem(const int tocTndex) { @@ -401,4 +413,14 @@ int Epub::findVirtualSpineIndex(const std::string& filename) const { } } return -1; -} \ No newline at end of file +} +size_t Epub::getBookSize() const { return getCumulativeSpineItemSize(getSpineItemsCount() - 1); } + +// Calculate progress in book +uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const { + size_t prevChapterSize = getCumulativeSpineItemSize(currentSpineIndex - 1); + size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize; + size_t bookSize = getBookSize(); + size_t sectionProgSize = currentSpineRead * curChapterSize; + return round(static_cast(prevChapterSize + sectionProgSize) / bookSize * 100.0); +} diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 92913d4..d36e0a7 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -16,6 +16,9 @@ class Epub { std::string tocNcxItem; std::string filepath; std::vector> spine; + // the file size of the spine items (proxy to book progress) + std::vector cumulativeSpineItemSize; + // the toc of the EPUB file std::vector toc; std::string contentBasePath; std::string cachePath; @@ -51,11 +54,10 @@ class Epub { bool trailingNullByte = false) const; bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; bool getItemSize(const std::string& itemHref, size_t* size) const; - std::string getSpineItem(int index) const; int getSpineItemsCount() const; - - EpubTocEntry& getTocItem(int tocTndex); + size_t getCumulativeSpineItemSize(const int spineIndex) const; + EpubTocEntry& getTocItem(int tocIndex); int getTocItemsCount() const; int getSpineIndexForTocIndex(int tocIndex) const; int getTocIndexForSpineIndex(int spineIndex) const; @@ -66,4 +68,7 @@ class Epub { int addVirtualSpineItem(const std::string& path); bool isVirtualSpineItem(int spineIndex) const; int findVirtualSpineIndex(const std::string& filename) const; -}; \ No newline at end of file + + size_t getBookSize() const; + uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const; +}; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 52fccfc..2beacff 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -3,7 +3,9 @@ #include #include -constexpr uint8_t PAGE_FILE_VERSION = 6; // Incremented +namespace { +constexpr uint8_t PAGE_FILE_VERSION = 3; +} void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } @@ -81,4 +83,4 @@ std::unique_ptr Page::deserialize(std::istream& is) { } return page; -} \ No newline at end of file +} diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index c4f57e4..3bbcb88 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -10,7 +10,9 @@ #include "Page.h" #include "parsers/ChapterHtmlSlimParser.h" -constexpr uint8_t SECTION_FILE_VERSION = 6; +namespace { +constexpr uint8_t SECTION_FILE_VERSION = 5; +} // Helper function to write XML-escaped text directly to file static bool writeEscapedXml(File& file, const char* text) { diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 8d3a40e..1f8c3bd 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -10,9 +10,11 @@ // Initialize the static instance CrossPointSettings CrossPointSettings::instance; +namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_COUNT = 2; constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin"; +} // namespace bool CrossPointSettings::saveToFile() const { // Make sure the directory exists diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index b0df9d9..dd96593 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -6,8 +6,12 @@ #include +namespace { constexpr uint8_t STATE_FILE_VERSION = 1; constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin"; +} // namespace + +CrossPointState CrossPointState::instance; bool CrossPointState::saveToFile() const { std::ofstream outputFile(STATE_FILE); diff --git a/src/CrossPointState.h b/src/CrossPointState.h index 35b928d..f060a0c 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -3,11 +3,20 @@ #include class CrossPointState { + // Static instance + static CrossPointState instance; + public: std::string openEpubPath; ~CrossPointState() = default; + // Get singleton instance + static CrossPointState& getInstance() { return instance; } + bool saveToFile() const; bool loadFromFile(); }; + +// Helper macro to access settings +#define APP_STATE CrossPointState::getInstance() diff --git a/src/activities/Activity.h b/src/activities/Activity.h new file mode 100644 index 0000000..28017f7 --- /dev/null +++ b/src/activities/Activity.h @@ -0,0 +1,18 @@ +#pragma once +#include + +class GfxRenderer; + +class Activity { + protected: + GfxRenderer& renderer; + InputManager& inputManager; + + public: + explicit Activity(GfxRenderer& renderer, InputManager& inputManager) + : renderer(renderer), inputManager(inputManager) {} + virtual ~Activity() = default; + virtual void onEnter() {} + virtual void onExit() {} + virtual void loop() {} +}; diff --git a/src/activities/ActivityWithSubactivity.cpp b/src/activities/ActivityWithSubactivity.cpp new file mode 100644 index 0000000..56dccd9 --- /dev/null +++ b/src/activities/ActivityWithSubactivity.cpp @@ -0,0 +1,21 @@ +#include "ActivityWithSubactivity.h" + +void ActivityWithSubactivity::exitActivity() { + if (subActivity) { + subActivity->onExit(); + subActivity.reset(); + } +} + +void ActivityWithSubactivity::enterNewActivity(Activity* activity) { + subActivity.reset(activity); + subActivity->onEnter(); +} + +void ActivityWithSubactivity::loop() { + if (subActivity) { + subActivity->loop(); + } +} + +void ActivityWithSubactivity::onExit() { exitActivity(); } diff --git a/src/activities/ActivityWithSubactivity.h b/src/activities/ActivityWithSubactivity.h new file mode 100644 index 0000000..b3a6873 --- /dev/null +++ b/src/activities/ActivityWithSubactivity.h @@ -0,0 +1,17 @@ +#pragma once +#include + +#include "Activity.h" + +class ActivityWithSubactivity : public Activity { + protected: + std::unique_ptr subActivity = nullptr; + void exitActivity(); + void enterNewActivity(Activity* activity); + + public: + explicit ActivityWithSubactivity(GfxRenderer& renderer, InputManager& inputManager) + : Activity(renderer, inputManager) {} + void loop() override; + void onExit() override; +}; diff --git a/src/screens/BootLogoScreen.cpp b/src/activities/boot_sleep/BootActivity.cpp similarity index 90% rename from src/screens/BootLogoScreen.cpp rename to src/activities/boot_sleep/BootActivity.cpp index 0ec2c10..a76cb7c 100644 --- a/src/screens/BootLogoScreen.cpp +++ b/src/activities/boot_sleep/BootActivity.cpp @@ -1,11 +1,11 @@ -#include "BootLogoScreen.h" +#include "BootActivity.h" #include #include "config.h" #include "images/CrossLarge.h" -void BootLogoScreen::onEnter() { +void BootActivity::onEnter() { const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageHeight = GfxRenderer::getScreenHeight(); diff --git a/src/activities/boot_sleep/BootActivity.h b/src/activities/boot_sleep/BootActivity.h new file mode 100644 index 0000000..dd647ce --- /dev/null +++ b/src/activities/boot_sleep/BootActivity.h @@ -0,0 +1,8 @@ +#pragma once +#include "../Activity.h" + +class BootActivity final : public Activity { + public: + explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {} + void onEnter() override; +}; diff --git a/src/screens/SleepScreen.cpp b/src/activities/boot_sleep/SleepActivity.cpp similarity index 92% rename from src/screens/SleepScreen.cpp rename to src/activities/boot_sleep/SleepActivity.cpp index b280125..2abe91e 100644 --- a/src/screens/SleepScreen.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -1,4 +1,4 @@ -#include "SleepScreen.h" +#include "SleepActivity.h" #include @@ -6,7 +6,7 @@ #include "config.h" #include "images/CrossLarge.h" -void SleepScreen::onEnter() { +void SleepActivity::onEnter() { const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageHeight = GfxRenderer::getScreenHeight(); diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h new file mode 100644 index 0000000..1626481 --- /dev/null +++ b/src/activities/boot_sleep/SleepActivity.h @@ -0,0 +1,8 @@ +#pragma once +#include "../Activity.h" + +class SleepActivity final : public Activity { + public: + explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {} + void onEnter() override; +}; diff --git a/src/screens/HomeScreen.cpp b/src/activities/home/HomeActivity.cpp similarity index 86% rename from src/screens/HomeScreen.cpp rename to src/activities/home/HomeActivity.cpp index 168e0fc..19b30c0 100644 --- a/src/screens/HomeScreen.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -1,16 +1,20 @@ -#include "HomeScreen.h" +#include "HomeActivity.h" #include #include #include "config.h" -void HomeScreen::taskTrampoline(void* param) { - auto* self = static_cast(param); +namespace { +constexpr int menuItemCount = 2; +} + +void HomeActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } -void HomeScreen::onEnter() { +void HomeActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); selectorIndex = 0; @@ -18,7 +22,7 @@ void HomeScreen::onEnter() { // Trigger first update updateRequired = true; - xTaskCreate(&HomeScreen::taskTrampoline, "HomeScreenTask", + xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask", 2048, // Stack size this, // Parameters 1, // Priority @@ -26,7 +30,7 @@ void HomeScreen::onEnter() { ); } -void HomeScreen::onExit() { +void HomeActivity::onExit() { // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { @@ -37,7 +41,7 @@ void HomeScreen::onExit() { renderingMutex = nullptr; } -void HomeScreen::handleInput() { +void HomeActivity::loop() { const bool prevPressed = inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT); const bool nextPressed = @@ -45,7 +49,7 @@ void HomeScreen::handleInput() { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (selectorIndex == 0) { - onFileSelectionOpen(); + onReaderOpen(); } else if (selectorIndex == 1) { onSettingsOpen(); } @@ -58,7 +62,7 @@ void HomeScreen::handleInput() { } } -void HomeScreen::displayTaskLoop() { +void HomeActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -70,7 +74,7 @@ void HomeScreen::displayTaskLoop() { } } -void HomeScreen::render() const { +void HomeActivity::render() const { renderer.clearScreen(); const auto pageWidth = GfxRenderer::getScreenWidth(); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h new file mode 100644 index 0000000..7f6ac4d --- /dev/null +++ b/src/activities/home/HomeActivity.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include + +#include + +#include "../Activity.h" + +class HomeActivity final : public Activity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + int selectorIndex = 0; + bool updateRequired = false; + const std::function onReaderOpen; + const std::function onSettingsOpen; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + + public: + explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function& onReaderOpen, + const std::function& onSettingsOpen) + : Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {} + void onEnter() override; + void onExit() override; + void loop() override; +}; diff --git a/src/screens/EpubReaderScreen.cpp b/src/activities/reader/EpubReaderActivity.cpp similarity index 84% rename from src/screens/EpubReaderScreen.cpp rename to src/activities/reader/EpubReaderActivity.cpp index 70cf4f9..de5c838 100644 --- a/src/screens/EpubReaderScreen.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1,4 +1,4 @@ -#include "EpubReaderScreen.h" +#include "EpubReaderActivity.h" #include #include @@ -6,25 +6,31 @@ #include "Battery.h" #include "CrossPointSettings.h" -#include "EpubReaderChapterSelectionScreen.h" -#include "EpubReaderFootnotesScreen.h" -#include "EpubReaderMenuScreen.h" +#include "EpubReaderChapterSelectionActivity.h" +#include "EpubReaderFootnotesActivity.h" +#include "EpubReaderMenuActivity.h" + +// Note: Vous devrez créer ces nouveaux fichiers: +// - EpubReaderMenuActivity.h/cpp (basé sur EpubReaderMenuScreen) +// - EpubReaderFootnotesActivity.h/cpp (basé sur EpubReaderFootnotesScreen) #include "config.h" -constexpr int PAGES_PER_REFRESH = 15; -constexpr unsigned long SKIP_CHAPTER_MS = 700; +namespace { +constexpr int pagesPerRefresh = 15; +constexpr unsigned long skipChapterMs = 700; constexpr float lineCompression = 0.95f; constexpr int marginTop = 8; constexpr int marginRight = 10; constexpr int marginBottom = 22; constexpr int marginLeft = 10; +} // namespace -void EpubReaderScreen::taskTrampoline(void* param) { - auto* self = static_cast(param); +void EpubReaderActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } -void EpubReaderScreen::onEnter() { +void EpubReaderActivity::onEnter() { if (!epub) { return; } @@ -45,12 +51,15 @@ void EpubReaderScreen::onEnter() { // Trigger first update updateRequired = true; - xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask", - 24576, // 32768 - this, 1, &displayTaskHandle); + xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask", + 8192, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); } -void EpubReaderScreen::onExit() { +void EpubReaderActivity::onExit() { // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { @@ -63,47 +72,36 @@ void EpubReaderScreen::onExit() { epub.reset(); } -void EpubReaderScreen::handleInput() { - // Pass input responsibility to sub screen if exists - if (subScreen) { - subScreen->handleInput(); +void EpubReaderActivity::loop() { + // Pass input responsibility to sub activity if exists + if (subAcitivity) { + subAcitivity->loop(); return; } - // Enter Menu selection screen - if (inputManager.wasPressed(InputManager::BTN_BACK)) { - if (isViewingFootnote) { - restoreSavedPosition(); - updateRequired = true; - return; - } else { - onGoBack(); - return; - } - } + // Enter menu activity if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { - // Don't start screen transition while rendering + // Don't start activity transition while rendering xSemaphoreTake(renderingMutex, portMAX_DELAY); - - subScreen.reset(new EpubReaderMenuScreen( + subAcitivity.reset(new EpubReaderMenuActivity( this->renderer, this->inputManager, [this] { - // onGoBack - return to reading - subScreen->onExit(); - subScreen.reset(); + // onGoBack from menu + subAcitivity->onExit(); + subAcitivity.reset(); updateRequired = true; }, - [this](EpubReaderMenuScreen::MenuOption option) { + [this](EpubReaderMenuActivity::MenuOption option) { // onSelectOption - handle menu choice - if (option == EpubReaderMenuScreen::CHAPTERS) { + if (option == EpubReaderMenuActivity::CHAPTERS) { // Show chapter selection - subScreen->onExit(); - subScreen.reset(new EpubReaderChapterSelectionScreen( + subAcitivity->onExit(); + subAcitivity.reset(new EpubReaderChapterSelectionActivity( this->renderer, this->inputManager, epub, currentSpineIndex, [this] { // onGoBack from chapter selection - subScreen->onExit(); - subScreen.reset(); + subAcitivity->onExit(); + subAcitivity.reset(); updateRequired = true; }, [this](const int newSpineIndex) { @@ -113,42 +111,46 @@ void EpubReaderScreen::handleInput() { nextPageNumber = 0; section.reset(); } - subScreen->onExit(); - subScreen.reset(); + subAcitivity->onExit(); + subAcitivity.reset(); updateRequired = true; })); - subScreen->onEnter(); - } else if (option == EpubReaderMenuScreen::FOOTNOTES) { + subAcitivity->onEnter(); + } else if (option == EpubReaderMenuActivity::FOOTNOTES) { // Show footnotes page with current page notes - subScreen->onExit(); - - subScreen.reset(new EpubReaderFootnotesScreen( + subAcitivity->onExit(); + subAcitivity.reset(new EpubReaderFootnotesActivity( this->renderer, this->inputManager, currentPageFootnotes, // Pass collected footnotes (reference) [this] { // onGoBack from footnotes - subScreen->onExit(); - subScreen.reset(); + subAcitivity->onExit(); + subAcitivity.reset(); updateRequired = true; }, [this](const char* href) { // onSelectFootnote - navigate to the footnote location navigateToHref(href, true); // true = save current position - subScreen->onExit(); - subScreen.reset(); + subAcitivity->onExit(); + subAcitivity.reset(); updateRequired = true; })); - subScreen->onEnter(); + subAcitivity->onEnter(); } })); - - subScreen->onEnter(); + subAcitivity->onEnter(); xSemaphoreGive(renderingMutex); } if (inputManager.wasPressed(InputManager::BTN_BACK)) { - onGoBack(); - return; + if (isViewingFootnote) { + restoreSavedPosition(); + updateRequired = true; + return; + } else { + onGoBack(); + return; + } } const bool prevReleased = @@ -168,7 +170,7 @@ void EpubReaderScreen::handleInput() { return; } - const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS; + const bool skipChapter = inputManager.getHeldTime() > skipChapterMs; if (skipChapter) { // We don't want to delete the section mid-render, so grab the semaphore @@ -214,7 +216,7 @@ void EpubReaderScreen::handleInput() { } } -void EpubReaderScreen::displayTaskLoop() { +void EpubReaderActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -226,7 +228,7 @@ void EpubReaderScreen::displayTaskLoop() { } } -void EpubReaderScreen::renderScreen() { +void EpubReaderActivity::renderScreen() { if (!epub) { return; } @@ -346,12 +348,12 @@ void EpubReaderScreen::renderScreen() { } } -void EpubReaderScreen::renderContents(std::unique_ptr page) { +void EpubReaderActivity::renderContents(std::unique_ptr page) { page->render(renderer, READER_FONT_ID); renderStatusBar(); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); - pagesUntilFullRefresh = PAGES_PER_REFRESH; + pagesUntilFullRefresh = pagesPerRefresh; } else { renderer.displayBuffer(); pagesUntilFullRefresh--; @@ -383,17 +385,23 @@ void EpubReaderScreen::renderContents(std::unique_ptr page) { renderer.restoreBwBuffer(); } -void EpubReaderScreen::renderStatusBar() const { +void EpubReaderActivity::renderStatusBar() const { constexpr auto textY = 776; + + // Calculate progress in book + float sectionChapterProg = static_cast(section->currentPage) / section->pageCount; + uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg); + // Right aligned text for progress counter - char progressBuf[32]; // Use fixed buffer instead of std::string - snprintf(progressBuf, sizeof(progressBuf), "%d / %d", section->currentPage + 1, section->pageCount); - const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressBuf); - renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY, progressBuf); + const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) + + " " + std::to_string(bookProgress) + "%"; + const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); + renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY, + progress.c_str()); // Left aligned battery icon and percentage const uint16_t percentage = battery.readPercentage(); - char percentageBuf[8]; // Use fixed buffer instead of std::string + char percentageBuf[8]; snprintf(percentageBuf, sizeof(percentageBuf), "%d%%", percentage); const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageBuf); renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, textY, percentageBuf); @@ -448,7 +456,7 @@ void EpubReaderScreen::renderStatusBar() const { } } -void EpubReaderScreen::navigateToHref(const char* href, bool savePosition) { +void EpubReaderActivity::navigateToHref(const char* href, bool savePosition) { if (!epub || !href) return; // Save current position if requested @@ -546,8 +554,7 @@ void EpubReaderScreen::navigateToHref(const char* href, bool savePosition) { Serial.printf("[%lu] [ERS] Navigated to spine index: %d\n", millis(), targetSpineIndex); } -// Method to restore saved position -void EpubReaderScreen::restoreSavedPosition() { +void EpubReaderActivity::restoreSavedPosition() { if (savedSpineIndex >= 0 && savedPageNumber >= 0) { Serial.printf("[%lu] [ERS] Restoring position: spine %d, page %d\n", millis(), savedSpineIndex, savedPageNumber); diff --git a/src/screens/EpubReaderScreen.h b/src/activities/reader/EpubReaderActivity.h similarity index 68% rename from src/screens/EpubReaderScreen.h rename to src/activities/reader/EpubReaderActivity.h index 14bc012..497fb14 100644 --- a/src/screens/EpubReaderScreen.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -5,15 +5,15 @@ #include #include -#include "EpubReaderFootnotesScreen.h" -#include "Screen.h" +#include "../Activity.h" +#include "EpubReaderFootnotesActivity.h" -class EpubReaderScreen final : public Screen { +class EpubReaderActivity final : public Activity { std::shared_ptr epub; std::unique_ptr
section = nullptr; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; - std::unique_ptr subScreen = nullptr; + std::unique_ptr subAcitivity = nullptr; int currentSpineIndex = 0; int nextPageNumber = 0; int pagesUntilFullRefresh = 0; @@ -36,10 +36,10 @@ class EpubReaderScreen final : public Screen { void restoreSavedPosition(); public: - explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr epub, - const std::function& onGoBack) - : Screen(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {} + explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr epub, + const std::function& onGoBack) + : Activity(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {} void onEnter() override; void onExit() override; - void handleInput() override; + void loop() override; }; diff --git a/src/screens/EpubReaderChapterSelectionScreen.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp similarity index 87% rename from src/screens/EpubReaderChapterSelectionScreen.cpp rename to src/activities/reader/EpubReaderChapterSelectionActivity.cpp index d5ebefe..b1c519c 100644 --- a/src/screens/EpubReaderChapterSelectionScreen.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -1,4 +1,4 @@ -#include "EpubReaderChapterSelectionScreen.h" +#include "EpubReaderChapterSelectionActivity.h" #include #include @@ -8,52 +8,12 @@ constexpr int PAGE_ITEMS = 24; constexpr int SKIP_PAGE_MS = 700; -void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) { - auto* self = static_cast(param); +void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } -void EpubReaderChapterSelectionScreen::onEnter() { - if (!epub) { - return; - } - - renderingMutex = xSemaphoreCreateMutex(); - - // Build filtered chapter list (excluding footnote pages) - buildFilteredChapterList(); - - // Find the index in filtered list that corresponds to currentSpineIndex - selectorIndex = 0; - for (size_t i = 0; i < filteredSpineIndices.size(); i++) { - if (filteredSpineIndices[i] == currentSpineIndex) { - selectorIndex = i; - break; - } - } - - // Trigger first update - updateRequired = true; - xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask", - 2048, // Stack size - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle - ); -} - -void EpubReaderChapterSelectionScreen::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 EpubReaderChapterSelectionScreen::buildFilteredChapterList() { +void EpubReaderChapterSelectionActivity::buildFilteredChapterList() { filteredSpineIndices.clear(); for (int i = 0; i < epub->getSpineItemsCount(); i++) { @@ -77,7 +37,47 @@ void EpubReaderChapterSelectionScreen::buildFilteredChapterList() { epub->getSpineItemsCount()); } -void EpubReaderChapterSelectionScreen::handleInput() { +void EpubReaderChapterSelectionActivity::onEnter() { + if (!epub) { + return; + } + + renderingMutex = xSemaphoreCreateMutex(); + + // Build filtered chapter list (excluding footnote pages) + buildFilteredChapterList(); + + // Find the index in filtered list that corresponds to currentSpineIndex + selectorIndex = 0; + for (size_t i = 0; i < filteredSpineIndices.size(); i++) { + if (filteredSpineIndices[i] == currentSpineIndex) { + selectorIndex = i; + break; + } + } + + // Trigger first update + updateRequired = true; + xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void EpubReaderChapterSelectionActivity::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 EpubReaderChapterSelectionActivity::loop() { const bool prevReleased = inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); const bool nextReleased = @@ -110,7 +110,7 @@ void EpubReaderChapterSelectionScreen::handleInput() { } } -void EpubReaderChapterSelectionScreen::displayTaskLoop() { +void EpubReaderChapterSelectionActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -122,7 +122,7 @@ void EpubReaderChapterSelectionScreen::displayTaskLoop() { } } -void EpubReaderChapterSelectionScreen::renderScreen() { +void EpubReaderChapterSelectionActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); @@ -151,4 +151,4 @@ void EpubReaderChapterSelectionScreen::renderScreen() { } renderer.displayBuffer(); -} \ No newline at end of file +} diff --git a/src/screens/EpubReaderChapterSelectionScreen.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h similarity index 62% rename from src/screens/EpubReaderChapterSelectionScreen.h rename to src/activities/reader/EpubReaderChapterSelectionActivity.h index 9e0f30d..f627682 100644 --- a/src/screens/EpubReaderChapterSelectionScreen.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -7,9 +7,9 @@ #include #include -#include "Screen.h" +#include "../Activity.h" -class EpubReaderChapterSelectionScreen final : public Screen { +class EpubReaderChapterSelectionActivity final : public Activity { std::shared_ptr epub; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; @@ -28,16 +28,16 @@ class EpubReaderChapterSelectionScreen final : public Screen { void buildFilteredChapterList(); public: - explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, - const std::shared_ptr& epub, const int currentSpineIndex, - const std::function& onGoBack, - const std::function& onSelectSpineIndex) - : Screen(renderer, inputManager), + explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, InputManager& inputManager, + const std::shared_ptr& epub, const int currentSpineIndex, + const std::function& onGoBack, + const std::function& onSelectSpineIndex) + : Activity(renderer, inputManager), epub(epub), currentSpineIndex(currentSpineIndex), onGoBack(onGoBack), onSelectSpineIndex(onSelectSpineIndex) {} void onEnter() override; void onExit() override; - void handleInput() override; -}; \ No newline at end of file + void loop() override; +}; diff --git a/src/screens/EpubReaderFootnotesScreen.cpp b/src/activities/reader/EpubReaderFootnotesActivity.cpp similarity index 87% rename from src/screens/EpubReaderFootnotesScreen.cpp rename to src/activities/reader/EpubReaderFootnotesActivity.cpp index 0d80173..83e950c 100644 --- a/src/screens/EpubReaderFootnotesScreen.cpp +++ b/src/activities/reader/EpubReaderFootnotesActivity.cpp @@ -1,19 +1,19 @@ -#include "EpubReaderFootnotesScreen.h" +#include "EpubReaderFootnotesActivity.h" #include #include "config.h" -void EpubReaderFootnotesScreen::onEnter() { +void EpubReaderFootnotesActivity::onEnter() { selectedIndex = 0; render(); } -void EpubReaderFootnotesScreen::onExit() { +void EpubReaderFootnotesActivity::onExit() { // Nothing to clean up } -void EpubReaderFootnotesScreen::handleInput() { +void EpubReaderFootnotesActivity::loop() { if (inputManager.wasPressed(InputManager::BTN_BACK)) { onGoBack(); return; @@ -23,8 +23,6 @@ void EpubReaderFootnotesScreen::handleInput() { const FootnoteEntry* entry = footnotes.getEntry(selectedIndex); if (entry) { Serial.printf("[%lu] [FNS] Selected footnote: %s -> %s\n", millis(), entry->number, entry->href); - - // Appeler le callback - EpubReaderScreen gère la navigation onSelectFootnote(entry->href); } return; @@ -51,7 +49,7 @@ void EpubReaderFootnotesScreen::handleInput() { } } -void EpubReaderFootnotesScreen::render() { +void EpubReaderFootnotesActivity::render() { renderer.clearScreen(); constexpr int startY = 50; @@ -88,4 +86,4 @@ void EpubReaderFootnotesScreen::render() { "UP/DOWN: Select CONFIRM: Go to footnote BACK: Return"); renderer.displayBuffer(); -} \ No newline at end of file +} diff --git a/src/screens/EpubReaderFootnotesScreen.h b/src/activities/reader/EpubReaderFootnotesActivity.h similarity index 72% rename from src/screens/EpubReaderFootnotesScreen.h rename to src/activities/reader/EpubReaderFootnotesActivity.h index 13d72f1..7542633 100644 --- a/src/screens/EpubReaderFootnotesScreen.h +++ b/src/activities/reader/EpubReaderFootnotesActivity.h @@ -4,7 +4,7 @@ #include #include "../../lib/Epub/Epub/FootnoteEntry.h" -#include "Screen.h" +#include "../Activity.h" class FootnotesData { private: @@ -36,17 +36,17 @@ class FootnotesData { } }; -class EpubReaderFootnotesScreen final : public Screen { +class EpubReaderFootnotesActivity final : public Activity { const FootnotesData& footnotes; const std::function onGoBack; const std::function onSelectFootnote; int selectedIndex; public: - EpubReaderFootnotesScreen(GfxRenderer& renderer, InputManager& inputManager, const FootnotesData& footnotes, - const std::function& onGoBack, - const std::function& onSelectFootnote) - : Screen(renderer, inputManager), + EpubReaderFootnotesActivity(GfxRenderer& renderer, InputManager& inputManager, const FootnotesData& footnotes, + const std::function& onGoBack, + const std::function& onSelectFootnote) + : Activity(renderer, inputManager), footnotes(footnotes), onGoBack(onGoBack), onSelectFootnote(onSelectFootnote), @@ -54,8 +54,8 @@ class EpubReaderFootnotesScreen final : public Screen { void onEnter() override; void onExit() override; - void handleInput() override; + void loop() override; private: void render(); -}; \ No newline at end of file +}; diff --git a/src/screens/EpubReaderMenuScreen.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp similarity index 83% rename from src/screens/EpubReaderMenuScreen.cpp rename to src/activities/reader/EpubReaderMenuActivity.cpp index 992b943..8b192a9 100644 --- a/src/screens/EpubReaderMenuScreen.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -1,7 +1,4 @@ -// -// Created by jlaunay on 13/12/2025. -// -#include "EpubReaderMenuScreen.h" +#include "EpubReaderMenuActivity.h" #include @@ -9,18 +6,18 @@ constexpr int MENU_ITEMS_COUNT = 2; -void EpubReaderMenuScreen::taskTrampoline(void* param) { - auto* self = static_cast(param); +void EpubReaderMenuActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } -void EpubReaderMenuScreen::onEnter() { +void EpubReaderMenuActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); selectorIndex = 0; // Trigger first update updateRequired = true; - xTaskCreate(&EpubReaderMenuScreen::taskTrampoline, "EpubReaderMenuTask", + xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubReaderMenuTask", 2048, // Stack size this, // Parameters 1, // Priority @@ -28,7 +25,7 @@ void EpubReaderMenuScreen::onEnter() { ); } -void EpubReaderMenuScreen::onExit() { +void EpubReaderMenuActivity::onExit() { // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { @@ -39,7 +36,7 @@ void EpubReaderMenuScreen::onExit() { renderingMutex = nullptr; } -void EpubReaderMenuScreen::handleInput() { +void EpubReaderMenuActivity::loop() { const bool prevReleased = inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); const bool nextReleased = @@ -58,7 +55,7 @@ void EpubReaderMenuScreen::handleInput() { } } -void EpubReaderMenuScreen::displayTaskLoop() { +void EpubReaderMenuActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -70,7 +67,7 @@ void EpubReaderMenuScreen::displayTaskLoop() { } } -void EpubReaderMenuScreen::renderScreen() { +void EpubReaderMenuActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); @@ -94,4 +91,4 @@ void EpubReaderMenuScreen::renderScreen() { } renderer.displayBuffer(); -} \ No newline at end of file +} diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h new file mode 100644 index 0000000..7583835 --- /dev/null +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include + +#include "../Activity.h" + +class EpubReaderMenuActivity final : public Activity { + public: + enum MenuOption { CHAPTERS, FOOTNOTES }; + + private: + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + int selectorIndex = 0; + bool updateRequired = false; + const std::function onGoBack; + const std::function onSelectOption; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void renderScreen(); + + public: + explicit EpubReaderMenuActivity(GfxRenderer& renderer, InputManager& inputManager, + const std::function& onGoBack, + const std::function& onSelectOption) + : Activity(renderer, inputManager), onGoBack(onGoBack), onSelectOption(onSelectOption) {} + + void onEnter() override; + void onExit() override; + void loop() override; +}; diff --git a/src/screens/FileSelectionScreen.cpp b/src/activities/reader/FileSelectionActivity.cpp similarity index 88% rename from src/screens/FileSelectionScreen.cpp rename to src/activities/reader/FileSelectionActivity.cpp index ce4d000..9e665cb 100644 --- a/src/screens/FileSelectionScreen.cpp +++ b/src/activities/reader/FileSelectionActivity.cpp @@ -1,4 +1,4 @@ -#include "FileSelectionScreen.h" +#include "FileSelectionActivity.h" #include #include @@ -15,12 +15,12 @@ void sortFileList(std::vector& strs) { }); } -void FileSelectionScreen::taskTrampoline(void* param) { - auto* self = static_cast(param); +void FileSelectionActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } -void FileSelectionScreen::loadFiles() { +void FileSelectionActivity::loadFiles() { files.clear(); selectorIndex = 0; auto root = SD.open(basepath.c_str()); @@ -42,7 +42,7 @@ void FileSelectionScreen::loadFiles() { sortFileList(files); } -void FileSelectionScreen::onEnter() { +void FileSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); basepath = "/"; @@ -52,7 +52,7 @@ void FileSelectionScreen::onEnter() { // Trigger first update updateRequired = true; - xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask", + xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask", 2048, // Stack size this, // Parameters 1, // Priority @@ -60,7 +60,7 @@ void FileSelectionScreen::onEnter() { ); } -void FileSelectionScreen::onExit() { +void FileSelectionActivity::onExit() { // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { @@ -72,7 +72,7 @@ void FileSelectionScreen::onExit() { files.clear(); } -void FileSelectionScreen::handleInput() { +void FileSelectionActivity::loop() { const bool prevPressed = inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT); const bool nextPressed = @@ -110,7 +110,7 @@ void FileSelectionScreen::handleInput() { } } -void FileSelectionScreen::displayTaskLoop() { +void FileSelectionActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -122,7 +122,7 @@ void FileSelectionScreen::displayTaskLoop() { } } -void FileSelectionScreen::render() const { +void FileSelectionActivity::render() const { renderer.clearScreen(); const auto pageWidth = GfxRenderer::getScreenWidth(); diff --git a/src/screens/FileSelectionScreen.h b/src/activities/reader/FileSelectionActivity.h similarity index 60% rename from src/screens/FileSelectionScreen.h rename to src/activities/reader/FileSelectionActivity.h index aaa076d..ff1a922 100644 --- a/src/screens/FileSelectionScreen.h +++ b/src/activities/reader/FileSelectionActivity.h @@ -7,9 +7,9 @@ #include #include -#include "Screen.h" +#include "../Activity.h" -class FileSelectionScreen final : public Screen { +class FileSelectionActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; std::string basepath = "/"; @@ -25,11 +25,11 @@ class FileSelectionScreen final : public Screen { void loadFiles(); public: - explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, - const std::function& onSelect, - const std::function& onGoHome) - : Screen(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {} + explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager, + const std::function& onSelect, + const std::function& onGoHome) + : Activity(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {} void onEnter() override; void onExit() override; - void handleInput() override; + void loop() override; }; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp new file mode 100644 index 0000000..099d7e2 --- /dev/null +++ b/src/activities/reader/ReaderActivity.cpp @@ -0,0 +1,68 @@ +#include "ReaderActivity.h" + +#include + +#include "CrossPointState.h" +#include "Epub.h" +#include "EpubReaderActivity.h" +#include "FileSelectionActivity.h" +#include "activities/util/FullScreenMessageActivity.h" + +std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { + if (!SD.exists(path.c_str())) { + Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); + return nullptr; + } + + auto epub = std::unique_ptr(new Epub(path, "/.crosspoint")); + if (epub->load()) { + return epub; + } + + Serial.printf("[%lu] [ ] Failed to load epub\n", millis()); + return nullptr; +} + +void ReaderActivity::onSelectEpubFile(const std::string& path) { + exitActivity(); + enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Loading...")); + + auto epub = loadEpub(path); + if (epub) { + APP_STATE.openEpubPath = path; + APP_STATE.saveToFile(); + onGoToEpubReader(std::move(epub)); + } else { + exitActivity(); + enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Failed to load epub", REGULAR, + EInkDisplay::HALF_REFRESH)); + delay(2000); + onGoToFileSelection(); + } +} + +void ReaderActivity::onGoToFileSelection() { + exitActivity(); + enterNewActivity(new FileSelectionActivity( + renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack)); +} + +void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { + exitActivity(); + enterNewActivity(new EpubReaderActivity(renderer, inputManager, std::move(epub), [this] { onGoToFileSelection(); })); +} + +void ReaderActivity::onEnter() { + if (initialEpubPath.empty()) { + onGoToFileSelection(); + return; + } + + auto epub = loadEpub(initialEpubPath); + if (!epub) { + onGoBack(); + return; + } + + onGoToEpubReader(std::move(epub)); +} diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h new file mode 100644 index 0000000..a68cd89 --- /dev/null +++ b/src/activities/reader/ReaderActivity.h @@ -0,0 +1,24 @@ +#pragma once +#include + +#include "../ActivityWithSubactivity.h" + +class Epub; + +class ReaderActivity final : public ActivityWithSubactivity { + std::string initialEpubPath; + const std::function onGoBack; + static std::unique_ptr loadEpub(const std::string& path); + + void onSelectEpubFile(const std::string& path); + void onGoToFileSelection(); + void onGoToEpubReader(std::unique_ptr epub); + + public: + explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialEpubPath, + const std::function& onGoBack) + : ActivityWithSubactivity(renderer, inputManager), + initialEpubPath(std::move(initialEpubPath)), + onGoBack(onGoBack) {} + void onEnter() override; +}; diff --git a/src/screens/SettingsScreen.cpp b/src/activities/settings/SettingsActivity.cpp similarity index 87% rename from src/screens/SettingsScreen.cpp rename to src/activities/settings/SettingsActivity.cpp index e970752..3b287d6 100644 --- a/src/screens/SettingsScreen.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -1,4 +1,4 @@ -#include "SettingsScreen.h" +#include "SettingsActivity.h" #include @@ -7,16 +7,16 @@ // Define the static settings list -const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = { +const SettingInfo SettingsActivity::settingsList[settingsCount] = { {"White Sleep Screen", &CrossPointSettings::whiteSleepScreen}, {"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}}; -void SettingsScreen::taskTrampoline(void* param) { - auto* self = static_cast(param); +void SettingsActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } -void SettingsScreen::onEnter() { +void SettingsActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); // Reset selection to first item @@ -25,7 +25,7 @@ void SettingsScreen::onEnter() { // Trigger first update updateRequired = true; - xTaskCreate(&SettingsScreen::taskTrampoline, "SettingsScreenTask", + xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask", 2048, // Stack size this, // Parameters 1, // Priority @@ -33,7 +33,7 @@ void SettingsScreen::onEnter() { ); } -void SettingsScreen::onExit() { +void SettingsActivity::onExit() { // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { @@ -44,7 +44,7 @@ void SettingsScreen::onExit() { renderingMutex = nullptr; } -void SettingsScreen::handleInput() { +void SettingsActivity::loop() { // Handle actions with early return if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { toggleCurrentSetting(); @@ -70,7 +70,7 @@ void SettingsScreen::handleInput() { } } -void SettingsScreen::toggleCurrentSetting() { +void SettingsActivity::toggleCurrentSetting() { // Validate index if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { return; @@ -84,7 +84,7 @@ void SettingsScreen::toggleCurrentSetting() { SETTINGS.saveToFile(); } -void SettingsScreen::displayTaskLoop() { +void SettingsActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -96,7 +96,7 @@ void SettingsScreen::displayTaskLoop() { } } -void SettingsScreen::render() const { +void SettingsActivity::render() const { renderer.clearScreen(); const auto pageWidth = GfxRenderer::getScreenWidth(); diff --git a/src/screens/SettingsScreen.h b/src/activities/settings/SettingsActivity.h similarity index 77% rename from src/screens/SettingsScreen.h rename to src/activities/settings/SettingsActivity.h index 8de45b8..b7ace22 100644 --- a/src/screens/SettingsScreen.h +++ b/src/activities/settings/SettingsActivity.h @@ -7,7 +7,7 @@ #include #include -#include "Screen.h" +#include "../Activity.h" class CrossPointSettings; @@ -17,7 +17,7 @@ struct SettingInfo { uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings }; -class SettingsScreen final : public Screen { +class SettingsActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; bool updateRequired = false; @@ -34,9 +34,9 @@ class SettingsScreen final : public Screen { void toggleCurrentSetting(); public: - explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoHome) - : Screen(renderer, inputManager), onGoHome(onGoHome) {} + explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoHome) + : Activity(renderer, inputManager), onGoHome(onGoHome) {} void onEnter() override; void onExit() override; - void handleInput() override; + void loop() override; }; diff --git a/src/screens/FullScreenMessageScreen.cpp b/src/activities/util/FullScreenMessageActivity.cpp similarity index 79% rename from src/screens/FullScreenMessageScreen.cpp rename to src/activities/util/FullScreenMessageActivity.cpp index 19787a7..a56952f 100644 --- a/src/screens/FullScreenMessageScreen.cpp +++ b/src/activities/util/FullScreenMessageActivity.cpp @@ -1,10 +1,10 @@ -#include "FullScreenMessageScreen.h" +#include "FullScreenMessageActivity.h" #include #include "config.h" -void FullScreenMessageScreen::onEnter() { +void FullScreenMessageActivity::onEnter() { const auto height = renderer.getLineHeight(UI_FONT_ID); const auto top = (GfxRenderer::getScreenHeight() - height) / 2; diff --git a/src/activities/util/FullScreenMessageActivity.h b/src/activities/util/FullScreenMessageActivity.h new file mode 100644 index 0000000..3180ddb --- /dev/null +++ b/src/activities/util/FullScreenMessageActivity.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +#include +#include + +#include "../Activity.h" + +class FullScreenMessageActivity final : public Activity { + std::string text; + EpdFontStyle style; + EInkDisplay::RefreshMode refreshMode; + + public: + explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text, + const EpdFontStyle style = REGULAR, + const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) + : Activity(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {} + void onEnter() override; +}; diff --git a/src/main.cpp b/src/main.cpp index 00a4b45..39c5bd0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,14 +16,13 @@ #include "Battery.h" #include "CrossPointSettings.h" #include "CrossPointState.h" +#include "activities/boot_sleep/BootActivity.h" +#include "activities/boot_sleep/SleepActivity.h" +#include "activities/home/HomeActivity.h" +#include "activities/reader/ReaderActivity.h" +#include "activities/settings/SettingsActivity.h" +#include "activities/util/FullScreenMessageActivity.h" #include "config.h" -#include "screens/BootLogoScreen.h" -#include "screens/EpubReaderScreen.h" -#include "screens/FileSelectionScreen.h" -#include "screens/FullScreenMessageScreen.h" -#include "screens/HomeScreen.h" -#include "screens/SettingsScreen.h" -#include "screens/SleepScreen.h" #define SPI_FQ 40000000 // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) @@ -42,8 +41,7 @@ EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); InputManager inputManager; GfxRenderer renderer(einkDisplay); -Screen* currentScreen; -CrossPointState appState; +Activity* currentActivity; // Fonts EpdFont bookerlyFont(&bookerly_2b); @@ -67,31 +65,16 @@ constexpr unsigned long POWER_BUTTON_SLEEP_MS = 500; // Auto-sleep timeout (10 minutes of inactivity) constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000; -std::unique_ptr loadEpub(const std::string& path) { - if (!SD.exists(path.c_str())) { - Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); - return nullptr; - } - - auto epub = std::unique_ptr(new Epub(path, "/.crosspoint")); - if (epub->load()) { - return epub; - } - - Serial.printf("[%lu] [ ] Failed to load epub\n", millis()); - return nullptr; -} - -void exitScreen() { - if (currentScreen) { - currentScreen->onExit(); - delete currentScreen; +void exitActivity() { + if (currentActivity) { + currentActivity->onExit(); + delete currentActivity; } } -void enterNewScreen(Screen* screen) { - currentScreen = screen; - currentScreen->onEnter(); +void enterNewActivity(Activity* activity) { + currentActivity = activity; + currentActivity->onEnter(); } // Verify long press on wake-up from deep sleep @@ -135,8 +118,8 @@ void waitForPowerRelease() { // Enter deep sleep mode void enterDeepSleep() { - exitScreen(); - enterNewScreen(new SleepScreen(renderer, inputManager)); + exitActivity(); + enterNewActivity(new SleepActivity(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 @@ -151,39 +134,20 @@ void enterDeepSleep() { } void onGoHome(); -void onGoToFileSelection(); -void onSelectEpubFile(const std::string& path) { - exitScreen(); - enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading...")); - - auto epub = loadEpub(path); - if (epub) { - appState.openEpubPath = path; - appState.saveToFile(); - exitScreen(); - enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoToFileSelection)); - } else { - exitScreen(); - enterNewScreen( - new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH)); - delay(2000); - onGoToFileSelection(); - } -} - -void onGoToFileSelection() { - exitScreen(); - enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoHome)); +void onGoToReader(const std::string& initialEpubPath) { + exitActivity(); + enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome)); } +void onGoToReaderHome() { onGoToReader(std::string()); } void onGoToSettings() { - exitScreen(); - enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome)); + exitActivity(); + enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome)); } void onGoHome() { - exitScreen(); - enterNewScreen(new HomeScreen(renderer, inputManager, onGoToFileSelection, onGoToSettings)); + exitActivity(); + enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings)); } void setup() { @@ -209,27 +173,20 @@ void setup() { renderer.insertFont(SMALL_FONT_ID, smallFontFamily); Serial.printf("[%lu] [ ] Fonts setup\n", millis()); - exitScreen(); - enterNewScreen(new BootLogoScreen(renderer, inputManager)); + exitActivity(); + enterNewActivity(new BootActivity(renderer, inputManager)); // SD Card Initialization SD.begin(SD_SPI_CS, SPI, SPI_FQ); SETTINGS.loadFromFile(); - appState.loadFromFile(); - if (!appState.openEpubPath.empty()) { - auto epub = loadEpub(appState.openEpubPath); - if (epub) { - exitScreen(); - enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome)); - // Ensure we're not still holding the power button before leaving setup - waitForPowerRelease(); - return; - } + APP_STATE.loadFromFile(); + if (APP_STATE.openEpubPath.empty()) { + onGoHome(); + } else { + onGoToReader(APP_STATE.openEpubPath); } - onGoHome(); - // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); } @@ -265,7 +222,7 @@ void loop() { return; } - if (currentScreen) { - currentScreen->handleInput(); + if (currentActivity) { + currentActivity->loop(); } } diff --git a/src/screens/BootLogoScreen.h b/src/screens/BootLogoScreen.h deleted file mode 100644 index 503afac..0000000 --- a/src/screens/BootLogoScreen.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include "Screen.h" - -class BootLogoScreen final : public Screen { - public: - explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {} - void onEnter() override; -}; diff --git a/src/screens/EpubReaderMenuScreen.h b/src/screens/EpubReaderMenuScreen.h deleted file mode 100644 index a462b24..0000000 --- a/src/screens/EpubReaderMenuScreen.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// Created by jlaunay on 13/12/2025. -// - -#ifndef CROSSPOINT_READER_EPUBREADERMENUSCREEN_H -#define CROSSPOINT_READER_EPUBREADERMENUSCREEN_H -#pragma once -#include -#include -#include - -#include "Screen.h" - -class EpubReaderMenuScreen final : public Screen { - public: - enum MenuOption { CHAPTERS, FOOTNOTES }; - - private: - TaskHandle_t displayTaskHandle = nullptr; - SemaphoreHandle_t renderingMutex = nullptr; - int selectorIndex = 0; - bool updateRequired = false; - const std::function onGoBack; - const std::function onSelectOption; - - static void taskTrampoline(void* param); - [[noreturn]] void displayTaskLoop(); - void renderScreen(); - - public: - explicit EpubReaderMenuScreen(GfxRenderer& renderer, InputManager& inputManager, - const std::function& onGoBack, - const std::function& onSelectOption) - : Screen(renderer, inputManager), onGoBack(onGoBack), onSelectOption(onSelectOption) {} - - void onEnter() override; - void onExit() override; - void handleInput() override; -}; -#endif // CROSSPOINT_READER_EPUBREADERMENUSCREEN_H diff --git a/src/screens/FullScreenMessageScreen.h b/src/screens/FullScreenMessageScreen.h deleted file mode 100644 index e90abb6..0000000 --- a/src/screens/FullScreenMessageScreen.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include -#include - -#include -#include - -#include "Screen.h" - -class FullScreenMessageScreen final : public Screen { - std::string text; - EpdFontStyle style; - EInkDisplay::RefreshMode refreshMode; - - public: - explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text, - const EpdFontStyle style = REGULAR, - const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) - : Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {} - void onEnter() override; -}; diff --git a/src/screens/HomeScreen.h b/src/screens/HomeScreen.h deleted file mode 100644 index dbdd21b..0000000 --- a/src/screens/HomeScreen.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include -#include -#include - -#include - -#include "Screen.h" - -class HomeScreen final : public Screen { - TaskHandle_t displayTaskHandle = nullptr; - SemaphoreHandle_t renderingMutex = nullptr; - int selectorIndex = 0; - bool updateRequired = false; - const std::function onFileSelectionOpen; - const std::function onSettingsOpen; - - static constexpr int menuItemCount = 2; - - static void taskTrampoline(void* param); - [[noreturn]] void displayTaskLoop(); - void render() const; - - public: - explicit HomeScreen(GfxRenderer& renderer, InputManager& inputManager, - const std::function& onFileSelectionOpen, const std::function& onSettingsOpen) - : Screen(renderer, inputManager), onFileSelectionOpen(onFileSelectionOpen), onSettingsOpen(onSettingsOpen) {} - void onEnter() override; - void onExit() override; - void handleInput() override; -}; diff --git a/src/screens/Screen.h b/src/screens/Screen.h deleted file mode 100644 index 7e4b866..0000000 --- a/src/screens/Screen.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include - -class GfxRenderer; - -class Screen { - protected: - GfxRenderer& renderer; - InputManager& inputManager; - - public: - explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {} - virtual ~Screen() = default; - virtual void onEnter() {} - virtual void onExit() {} - virtual void handleInput() {} -}; diff --git a/src/screens/SleepScreen.h b/src/screens/SleepScreen.h deleted file mode 100644 index b280087..0000000 --- a/src/screens/SleepScreen.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include "Screen.h" - -class SleepScreen final : public Screen { - public: - explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {} - void onEnter() override; -};