diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 8b4bb9a..d1855cc 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -73,10 +73,8 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { coverImageItem = opfParser.items.at(opfParser.coverItemId); } - if (opfParser.items.count("ncx")) { - tocNcxItem = opfParser.items.at("ncx"); - } else if (opfParser.items.count("ncxtoc")) { - tocNcxItem = opfParser.items.at("ncxtoc"); + if (!opfParser.tocNcxPath.empty()) { + tocNcxItem = opfParser.tocNcxPath; } for (auto& spineRef : opfParser.spineRefs) { @@ -150,6 +148,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; @@ -257,6 +267,8 @@ bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { int Epub::getSpineItemsCount() const { return spine.size(); } +size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); } + std::string& Epub::getSpineItem(const int spineIndex) { if (spineIndex < 0 || spineIndex >= spine.size()) { Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex); @@ -304,3 +316,14 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const { Serial.printf("[%lu] [EBP] TOC item not found\n", millis()); return -1; } + +size_t Epub::getBookSize() const { return getCumulativeSpineItemSize(getSpineItemsCount() - 1); } + +// Calculate progress in book +uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) { + size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0; + 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 765eacc..1f2dfa9 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -20,6 +20,8 @@ class Epub { std::string filepath; // the spine of the EPUB file 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; // the base path for items in the EPUB file @@ -51,8 +53,12 @@ class Epub { bool getItemSize(const std::string& itemHref, size_t* size) const; std::string& getSpineItem(int spineIndex); 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; + + size_t getBookSize() const; + uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead); }; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 09abd08..01bb3ac 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -3,7 +3,9 @@ #include #include +namespace { constexpr uint8_t PAGE_FILE_VERSION = 3; +} void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 9c05208..7c9d241 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -9,7 +9,9 @@ #include "Page.h" #include "parsers/ChapterHtmlSlimParser.h" +namespace { constexpr uint8_t SECTION_FILE_VERSION = 5; +} void Section::onPageComplete(std::unique_ptr page) { const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 667f679..472d76c 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -3,6 +3,10 @@ #include #include +namespace { +constexpr const char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml"; +} + bool ContentOpfParser::setup() { parser = XML_ParserCreate(nullptr); if (!parser) { @@ -111,16 +115,28 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) { std::string itemId; std::string href; + std::string mediaType; for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], "id") == 0) { itemId = atts[i + 1]; } else if (strcmp(atts[i], "href") == 0) { href = self->baseContentPath + atts[i + 1]; + } else if (strcmp(atts[i], "media-type") == 0) { + mediaType = atts[i + 1]; } } self->items[itemId] = href; + + if (mediaType == MEDIA_TYPE_NCX) { + if (self->tocNcxPath.empty()) { + self->tocNcxPath = href; + } else { + Serial.printf("[%lu] [COF] Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s\n", millis(), + href.c_str()); + } + } return; } 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/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp new file mode 100644 index 0000000..19b30c0 --- /dev/null +++ b/src/activities/home/HomeActivity.cpp @@ -0,0 +1,103 @@ +#include "HomeActivity.h" + +#include +#include + +#include "config.h" + +namespace { +constexpr int menuItemCount = 2; +} + +void HomeActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void HomeActivity::onEnter() { + renderingMutex = xSemaphoreCreateMutex(); + + selectorIndex = 0; + + // Trigger first update + updateRequired = true; + + xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void HomeActivity::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 HomeActivity::loop() { + const bool prevPressed = + inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT); + const bool nextPressed = + inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT); + + if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { + if (selectorIndex == 0) { + onReaderOpen(); + } else if (selectorIndex == 1) { + onSettingsOpen(); + } + } else if (prevPressed) { + selectorIndex = (selectorIndex + menuItemCount - 1) % menuItemCount; + updateRequired = true; + } else if (nextPressed) { + selectorIndex = (selectorIndex + 1) % menuItemCount; + updateRequired = true; + } +} + +void HomeActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void HomeActivity::render() const { + renderer.clearScreen(); + + const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageHeight = GfxRenderer::getScreenHeight(); + renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD); + + // Draw selection + renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30); + renderer.drawText(UI_FONT_ID, 20, 60, "Read", selectorIndex != 0); + renderer.drawText(UI_FONT_ID, 20, 90, "Settings", selectorIndex != 1); + + renderer.drawRect(25, pageHeight - 40, 106, 40); + renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Back")) / 2, pageHeight - 35, "Back"); + + renderer.drawRect(130, pageHeight - 40, 106, 40); + renderer.drawText(UI_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_FONT_ID, "Confirm")) / 2, pageHeight - 35, + "Confirm"); + + renderer.drawRect(245, pageHeight - 40, 106, 40); + renderer.drawText(UI_FONT_ID, 245 + (105 - renderer.getTextWidth(UI_FONT_ID, "Left")) / 2, pageHeight - 35, "Left"); + + renderer.drawRect(350, pageHeight - 40, 106, 40); + renderer.drawText(UI_FONT_ID, 350 + (105 - renderer.getTextWidth(UI_FONT_ID, "Right")) / 2, pageHeight - 35, "Right"); + + renderer.displayBuffer(); +} 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/OnScreenKeyboard.cpp b/src/activities/network/OnScreenKeyboard.cpp similarity index 100% rename from src/screens/OnScreenKeyboard.cpp rename to src/activities/network/OnScreenKeyboard.cpp diff --git a/src/screens/OnScreenKeyboard.h b/src/activities/network/OnScreenKeyboard.h similarity index 100% rename from src/screens/OnScreenKeyboard.h rename to src/activities/network/OnScreenKeyboard.h diff --git a/src/screens/WifiScreen.cpp b/src/activities/network/WifiScreen.cpp similarity index 99% rename from src/screens/WifiScreen.cpp rename to src/activities/network/WifiScreen.cpp index f1ab171..5487569 100644 --- a/src/screens/WifiScreen.cpp +++ b/src/activities/network/WifiScreen.cpp @@ -227,7 +227,7 @@ void WifiScreen::checkConnectionStatus() { } } -void WifiScreen::handleInput() { +void WifiScreen::loop() { // Check scan progress if (state == WifiScreenState::SCANNING) { processWifiScanResults(); diff --git a/src/screens/WifiScreen.h b/src/activities/network/WifiScreen.h similarity index 94% rename from src/screens/WifiScreen.h rename to src/activities/network/WifiScreen.h index 8d267e1..a0a607c 100644 --- a/src/screens/WifiScreen.h +++ b/src/activities/network/WifiScreen.h @@ -10,7 +10,7 @@ #include #include "OnScreenKeyboard.h" -#include "Screen.h" +#include "../Activity.h" // Structure to hold WiFi network information struct WifiNetworkInfo { @@ -32,7 +32,7 @@ enum class WifiScreenState { FORGET_PROMPT // Asking user if they want to forget the network }; -class WifiScreen final : public Screen { +class WifiScreen final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; bool updateRequired = false; @@ -86,8 +86,8 @@ class WifiScreen final : public Screen { public: explicit WifiScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoBack) - : Screen(renderer, inputManager), onGoBack(onGoBack) {} + : Activity(renderer, inputManager), onGoBack(onGoBack) {} void onEnter() override; void onExit() override; - void handleInput() override; + void loop() override; }; diff --git a/src/screens/EpubReaderScreen.cpp b/src/activities/reader/EpubReaderActivity.cpp similarity index 88% rename from src/screens/EpubReaderScreen.cpp rename to src/activities/reader/EpubReaderActivity.cpp index 4e13f3d..bf2eaf7 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,23 +6,25 @@ #include "Battery.h" #include "CrossPointSettings.h" -#include "EpubReaderChapterSelectionScreen.h" +#include "EpubReaderChapterSelectionActivity.h" #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; } @@ -44,7 +46,7 @@ void EpubReaderScreen::onEnter() { // Trigger first update updateRequired = true; - xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask", + xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask", 8192, // Stack size this, // Parameters 1, // Priority @@ -52,7 +54,7 @@ void EpubReaderScreen::onEnter() { ); } -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) { @@ -65,22 +67,22 @@ 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 chapter selection screen + // Enter chapter selection 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 EpubReaderChapterSelectionScreen( + subAcitivity.reset(new EpubReaderChapterSelectionActivity( this->renderer, this->inputManager, epub, currentSpineIndex, [this] { - subScreen->onExit(); - subScreen.reset(); + subAcitivity->onExit(); + subAcitivity.reset(); updateRequired = true; }, [this](const int newSpineIndex) { @@ -89,16 +91,16 @@ void EpubReaderScreen::handleInput() { nextPageNumber = 0; section.reset(); } - subScreen->onExit(); - subScreen.reset(); + subAcitivity->onExit(); + subAcitivity.reset(); updateRequired = true; })); - subScreen->onEnter(); + subAcitivity->onEnter(); xSemaphoreGive(renderingMutex); } if (inputManager.wasPressed(InputManager::BTN_BACK)) { - onGoHome(); + onGoBack(); return; } @@ -119,7 +121,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 @@ -165,7 +167,7 @@ void EpubReaderScreen::handleInput() { } } -void EpubReaderScreen::displayTaskLoop() { +void EpubReaderActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -178,7 +180,7 @@ void EpubReaderScreen::displayTaskLoop() { } // TODO: Failure handling -void EpubReaderScreen::renderScreen() { +void EpubReaderActivity::renderScreen() { if (!epub) { return; } @@ -285,12 +287,12 @@ void EpubReaderScreen::renderScreen() { f.close(); } -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--; @@ -322,10 +324,16 @@ 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 - const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount); + 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()); diff --git a/src/screens/EpubReaderScreen.h b/src/activities/reader/EpubReaderActivity.h similarity index 59% rename from src/screens/EpubReaderScreen.h rename to src/activities/reader/EpubReaderActivity.h index 4ef8bef..8eeddc1 100644 --- a/src/screens/EpubReaderScreen.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -5,19 +5,19 @@ #include #include -#include "Screen.h" +#include "../Activity.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; bool updateRequired = false; - const std::function onGoHome; + const std::function onGoBack; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -26,10 +26,10 @@ class EpubReaderScreen final : public Screen { void renderStatusBar() const; public: - explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr epub, - const std::function& onGoHome) - : Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {} + 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 84% rename from src/screens/EpubReaderChapterSelectionScreen.cpp rename to src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 26688a4..f4dfc37 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,12 +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() { +void EpubReaderChapterSelectionActivity::onEnter() { if (!epub) { return; } @@ -23,7 +23,7 @@ void EpubReaderChapterSelectionScreen::onEnter() { // Trigger first update updateRequired = true; - xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask", + xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask", 2048, // Stack size this, // Parameters 1, // Priority @@ -31,7 +31,7 @@ void EpubReaderChapterSelectionScreen::onEnter() { ); } -void EpubReaderChapterSelectionScreen::onExit() { +void EpubReaderChapterSelectionActivity::onExit() { // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { @@ -42,7 +42,7 @@ void EpubReaderChapterSelectionScreen::onExit() { renderingMutex = nullptr; } -void EpubReaderChapterSelectionScreen::handleInput() { +void EpubReaderChapterSelectionActivity::loop() { const bool prevReleased = inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); const bool nextReleased = @@ -72,7 +72,7 @@ void EpubReaderChapterSelectionScreen::handleInput() { } } -void EpubReaderChapterSelectionScreen::displayTaskLoop() { +void EpubReaderChapterSelectionActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -84,7 +84,7 @@ void EpubReaderChapterSelectionScreen::displayTaskLoop() { } } -void EpubReaderChapterSelectionScreen::renderScreen() { +void EpubReaderChapterSelectionActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/screens/EpubReaderChapterSelectionScreen.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h similarity index 57% rename from src/screens/EpubReaderChapterSelectionScreen.h rename to src/activities/reader/EpubReaderChapterSelectionActivity.h index 8cac4cb..b25ef6f 100644 --- a/src/screens/EpubReaderChapterSelectionScreen.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -6,9 +6,9 @@ #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; @@ -23,16 +23,16 @@ class EpubReaderChapterSelectionScreen final : public Screen { void renderScreen(); 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; + void loop() override; }; diff --git a/src/screens/FileSelectionScreen.cpp b/src/activities/reader/FileSelectionActivity.cpp similarity index 86% rename from src/screens/FileSelectionScreen.cpp rename to src/activities/reader/FileSelectionActivity.cpp index 09f2efc..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 = @@ -98,8 +98,8 @@ void FileSelectionScreen::handleInput() { loadFiles(); updateRequired = true; } else { - // At root level, go to settings - onSettingsOpen(); + // At root level, go back home + onGoHome(); } } else if (prevPressed) { selectorIndex = (selectorIndex + files.size() - 1) % files.size(); @@ -110,7 +110,7 @@ void FileSelectionScreen::handleInput() { } } -void FileSelectionScreen::displayTaskLoop() { +void FileSelectionActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -122,14 +122,14 @@ void FileSelectionScreen::displayTaskLoop() { } } -void FileSelectionScreen::render() const { +void FileSelectionActivity::render() const { renderer.clearScreen(); 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"); + renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home"); if (files.empty()) { renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); diff --git a/src/screens/FileSelectionScreen.h b/src/activities/reader/FileSelectionActivity.h similarity index 56% rename from src/screens/FileSelectionScreen.h rename to src/activities/reader/FileSelectionActivity.h index 00947a5..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 = "/"; @@ -17,7 +17,7 @@ class FileSelectionScreen final : public Screen { int selectorIndex = 0; bool updateRequired = false; const std::function onSelect; - const std::function onSettingsOpen; + const std::function onGoHome; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -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& onSettingsOpen) - : Screen(renderer, inputManager), onSelect(onSelect), onSettingsOpen(onSettingsOpen) {} + 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 66% rename from src/screens/SettingsScreen.cpp rename to src/activities/settings/SettingsActivity.cpp index 3a8518f..100996d 100644 --- a/src/screens/SettingsScreen.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -1,4 +1,4 @@ -#include "SettingsScreen.h" +#include "SettingsActivity.h" #include @@ -7,17 +7,16 @@ // Define the static settings list -const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = { - {"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen}, - {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing}, - {"WiFi", SettingType::ACTION, nullptr}}; +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 @@ -26,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 @@ -34,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) { @@ -45,10 +44,10 @@ void SettingsScreen::onExit() { renderingMutex = nullptr; } -void SettingsScreen::handleInput() { +void SettingsActivity::loop() { // Handle actions with early return if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { - activateCurrentSetting(); + toggleCurrentSetting(); updateRequired = true; return; } @@ -73,7 +72,7 @@ void SettingsScreen::handleInput() { } } -void SettingsScreen::activateCurrentSetting() { +void SettingsActivity::toggleCurrentSetting() { // Validate index if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { return; @@ -81,31 +80,6 @@ void SettingsScreen::activateCurrentSetting() { const auto& setting = settingsList[selectedSettingIndex]; - if (setting.type == SettingType::TOGGLE) { - toggleCurrentSetting(); - // Trigger a redraw of the entire screen - updateRequired = true; - } else if (setting.type == SettingType::ACTION) { - // Handle action settings - if (std::string(setting.name) == "WiFi") { - onGoWifi(); - } - } -} - -void SettingsScreen::toggleCurrentSetting() { - // Validate index - if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { - return; - } - - const auto& setting = settingsList[selectedSettingIndex]; - - // Only toggle if it's a toggle type and has a value pointer - if (setting.type != SettingType::TOGGLE || setting.valuePtr == nullptr) { - return; - } - // Toggle the boolean value using the member pointer bool currentValue = SETTINGS.*(setting.valuePtr); SETTINGS.*(setting.valuePtr) = !currentValue; @@ -114,7 +88,7 @@ void SettingsScreen::toggleCurrentSetting() { SETTINGS.saveToFile(); } -void SettingsScreen::displayTaskLoop() { +void SettingsActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; @@ -126,7 +100,7 @@ void SettingsScreen::displayTaskLoop() { } } -void SettingsScreen::render() const { +void SettingsActivity::render() const { renderer.clearScreen(); const auto pageWidth = GfxRenderer::getScreenWidth(); @@ -149,12 +123,10 @@ void SettingsScreen::render() const { // Draw setting name renderer.drawText(UI_FONT_ID, 20, settingY, settingsList[i].name); - // Draw value based on setting type - if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { + // Draw value (all settings are toggles now) + if (settingsList[i].valuePtr != nullptr) { bool value = SETTINGS.*(settingsList[i].valuePtr); renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF"); - } else if (settingsList[i].type == SettingType::ACTION) { - renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, ">"); } } diff --git a/src/screens/SettingsScreen.h b/src/activities/settings/SettingsActivity.h similarity index 57% rename from src/screens/SettingsScreen.h rename to src/activities/settings/SettingsActivity.h index af66f2b..d1d5b99 100644 --- a/src/screens/SettingsScreen.h +++ b/src/activities/settings/SettingsActivity.h @@ -8,43 +8,36 @@ #include #include -#include "Screen.h" +#include "../Activity.h" class CrossPointSettings; -// Enum to distinguish setting types -enum class SettingType { TOGGLE, ACTION }; - // Structure to hold setting information struct SettingInfo { const char* name; // Display name of the setting - SettingType type; // Type of setting - uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE) + 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; int selectedSettingIndex = 0; // Currently selected setting const std::function onGoHome; - const std::function onGoWifi; // Static settings list - static constexpr int settingsCount = 3; // Number of settings + 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(); - void activateCurrentSetting(); public: - explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function& onGoHome, - const std::function& onGoWifi) - : Screen(renderer, inputManager), onGoHome(onGoHome), onGoWifi(onGoWifi) {} + 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 97b856a..81598d4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,15 +17,13 @@ #include "Battery.h" #include "CrossPointSettings.h" #include "CrossPointState.h" -#include "CrossPointWebServer.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/SettingsScreen.h" -#include "screens/SleepScreen.h" -#include "screens/WifiScreen.h" #define SPI_FQ 40000000 // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) @@ -44,8 +42,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); @@ -69,31 +66,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 @@ -137,8 +119,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 @@ -153,40 +135,20 @@ void enterDeepSleep() { } void onGoHome(); -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), onGoHome)); - } else { - exitScreen(); - enterNewScreen( - new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH)); - delay(2000); - onGoHome(); - } -} - -void onGoToSettings(); - -void onGoToWifi() { - exitScreen(); - enterNewScreen(new WifiScreen(renderer, inputManager, onGoToSettings)); +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, onGoToWifi)); + exitActivity(); + enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome)); } void onGoHome() { - exitScreen(); - enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); + exitActivity(); + enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings)); } void setup() { @@ -212,28 +174,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); } - exitScreen(); - enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); - // Ensure we're not still holding the power button before leaving setup waitForPowerRelease(); } @@ -248,11 +202,6 @@ void loop() { lastMemPrint = millis(); } - // Handle web server clients if WiFi is connected - if (WiFi.status() == WL_CONNECTED) { - crossPointWebServer.handleClient(); - } - inputManager.update(); // Check for any user activity (button press or release) @@ -274,7 +223,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/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/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; -};