From e6dd49dabe0bee55d82647fa06183e5b61bdcd43 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Mon, 19 Jan 2026 22:03:17 +0100 Subject: [PATCH 1/3] Adding Ebook menu, go home, clear cache options. The chapter selection is moved to the menu on pos 1. --- src/activities/reader/EpubReaderActivity.cpp | 109 ++++++++++++++---- .../EpubReaderChapterSelectionActivity.cpp | 16 ++- .../reader/EpubReaderMenuActivity.cpp | 100 ++++++++++++++++ .../reader/EpubReaderMenuActivity.h | 50 ++++++++ 4 files changed, 249 insertions(+), 26 deletions(-) create mode 100644 src/activities/reader/EpubReaderMenuActivity.cpp create mode 100644 src/activities/reader/EpubReaderMenuActivity.h diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index d70a15c..2d7bbb4 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -8,6 +8,7 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" #include "EpubReaderChapterSelectionActivity.h" +#include "EpubReaderMenuActivity.h" #include "MappedInputManager.h" #include "ScreenComponents.h" #include "fontIds.h" @@ -121,30 +122,98 @@ void EpubReaderActivity::loop() { const int currentPage = section ? section->currentPage : 0; const int totalPages = section ? section->pageCount : 0; exitActivity(); - enterNewActivity(new EpubReaderChapterSelectionActivity( - this->renderer, this->mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, - [this] { + enterNewActivity(new EpubReaderMenuActivity( + this->renderer, this->mappedInput, + [this]() { // On Back exitActivity(); updateRequired = true; }, - [this](const int newSpineIndex) { - if (currentSpineIndex != newSpineIndex) { - currentSpineIndex = newSpineIndex; - nextPageNumber = 0; - section.reset(); + [this](EpubReaderMenuActivity::MenuAction action) { // On Select + switch (action) { + case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { + // Calculate values BEFORE we start destroying things + const int currentP = section ? section->currentPage : 0; + const int totalP = section ? section->pageCount : 0; + const int spineIdx = currentSpineIndex; + const std::string path = epub->getPath(); + + xSemaphoreTake(renderingMutex, portMAX_DELAY); + + // 1. Close the menu + exitActivity(); + + // 2. Open the Chapter Selector + enterNewActivity(new EpubReaderChapterSelectionActivity( + this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, + [this] { + exitActivity(); + updateRequired = true; + }, + [this](const int newSpineIndex) { + if (currentSpineIndex != newSpineIndex) { + currentSpineIndex = newSpineIndex; + nextPageNumber = 0; + section.reset(); + } + exitActivity(); + updateRequired = true; + }, + [this](const int newSpineIndex, const int newPage) { + if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { + currentSpineIndex = newSpineIndex; + nextPageNumber = newPage; + section.reset(); + } + exitActivity(); + updateRequired = true; + })); + + xSemaphoreGive(renderingMutex); + break; + break; + } + case EpubReaderMenuActivity::MenuAction::GO_HOME: { + // 2. Trigger the reader's "Go Home" callback + if (onGoHome) { + onGoHome(); + } + + break; + } + case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + section.reset(); + if (epub) { + // 2. BACKUP: Read current progress + // We use the current variables that track our position + uint16_t backupSpine = currentSpineIndex; + uint16_t backupPage = nextPageNumber; + + // 3. WIPE: Clear the cache directory + epub->clearCache(); + + // 4. RESTORE: Re-setup the directory and rewrite the progress file + epub->setupCacheDir(); + + FsFile f; + if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { + uint8_t data[4]; + data[0] = backupSpine & 0xFF; + data[1] = (backupSpine >> 8) & 0xFF; + data[2] = backupPage & 0xFF; + data[3] = (backupPage >> 8) & 0xFF; + f.write(data, 4); + f.close(); + Serial.println("[ERS] Progress restored after cache clear"); + } + } + exitActivity(); + updateRequired = true; + xSemaphoreGive(renderingMutex); + if (onGoHome) onGoHome(); + break; + } } - exitActivity(); - updateRequired = true; - }, - [this](const int newSpineIndex, const int newPage) { - // Handle sync position - if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { - currentSpineIndex = newSpineIndex; - nextPageNumber = newPage; - section.reset(); - } - exitActivity(); - updateRequired = true; })); xSemaphoreGive(renderingMutex); } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index ad4dd2f..f838bf0 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -188,22 +188,26 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const auto pageStartIndex = selectorIndex / pageItems * pageItems; renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); - for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) { - const int displayY = 60 + (itemIndex % pageItems) * 30; + // for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) + // { + for (int i = 0; i < pageItems; i++) { + int itemIndex = pageStartIndex + i; + if (itemIndex >= totalItems) break; + const int displayY = 60 + i * 30; const bool isSelected = (itemIndex == selectorIndex); if (isSyncItem(itemIndex)) { - // Draw sync option (at top or bottom) renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected); } else { - // Draw TOC item (account for top sync offset) const int tocIndex = tocIndexFromItemIndex(itemIndex); auto item = epub->getTocItem(tocIndex); + const int indentSize = 20 + (item.level - 1) * 15; const std::string chapterName = renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize); - renderer.drawText(UI_10_FONT_ID, indentSize, 60 + (tocIndex % pageItems) * 30, chapterName.c_str(), - tocIndex != selectorIndex); + + // FIX: Use displayY here instead of recalculating based on tocIndex + renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected); } } diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp new file mode 100644 index 0000000..cac6f4a --- /dev/null +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -0,0 +1,100 @@ +#include "EpubReaderMenuActivity.h" +#include +#include "fontIds.h" + +void EpubReaderMenuActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + renderingMutex = xSemaphoreCreateMutex(); + updateRequired = true; + + xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubMenuTask", 4096, this, 1, &displayTaskHandle); +} + +void EpubReaderMenuActivity::onExit() { + ActivityWithSubactivity::onExit(); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void EpubReaderMenuActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void EpubReaderMenuActivity::displayTaskLoop() { + while (true) { + if (updateRequired && !subActivity) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + renderScreen(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void EpubReaderMenuActivity::loop() { + if (subActivity) { + subActivity->loop(); + return; + } + + // Use local variables for items we need to check after potential deletion + if (mappedInput.wasReleased(MappedInputManager::Button::Up) || + mappedInput.wasReleased(MappedInputManager::Button::Left) ) { + selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size(); + updateRequired = true; + } else if (mappedInput.wasReleased(MappedInputManager::Button::Down) || + mappedInput.wasReleased(MappedInputManager::Button::Right)) { + selectedIndex = (selectedIndex + 1) % menuItems.size(); + updateRequired = true; + } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + // 1. Capture the callback and action locally + auto actionCallback = onAction; + auto selectedAction = menuItems[selectedIndex].action; + + // 2. Execute the callback + actionCallback(selectedAction); + + // 3. CRITICAL: Return immediately. 'this' is likely deleted now. + return; + } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onBack(); + return; // Also return here just in case + } +} + +void EpubReaderMenuActivity::renderScreen() { + renderer.clearScreen(); + const auto pageWidth = renderer.getScreenWidth(); + + // Title + renderer.drawCenteredText(UI_12_FONT_ID, 20, "Reader Menu", true, EpdFontFamily::BOLD); + // renderer.fillRect(0, 60 + (selectedIndex % menuItems.size()) * 30 - 2, pageWidth - 1, 30); + + // Menu Items + constexpr int startY = 80; + constexpr int lineHeight = 40; + + for (size_t i = 0; i < menuItems.size(); ++i) { + const int displayY = startY + (i * lineHeight); + const bool isSelected = (static_cast(i) == selectedIndex); + + if (isSelected) { + renderer.fillRect(10, displayY - 5, pageWidth - 20, lineHeight, true); + } + + renderer.drawText(UI_12_FONT_ID, 30, displayY + 5, menuItems[i].label.c_str(), !isSelected); + } + + // Footer / Hints + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h new file mode 100644 index 0000000..79ed4de --- /dev/null +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "MappedInputManager.h" +#include "../ActivityWithSubactivity.h" + +class EpubReaderMenuActivity final : public ActivityWithSubactivity { + public: + enum class MenuAction { SELECT_CHAPTER, GO_HOME, DELETE_CACHE }; + + explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack, + const std::function& onAction) + : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), + onBack(onBack), + onAction(onAction) {} + + void onEnter() override; + void onExit() override; + void loop() override; + + private: + struct MenuItem { + MenuAction action; + std::string label; + }; + + const std::vector menuItems = { + {MenuAction::SELECT_CHAPTER, "Select Chapter"}, + {MenuAction::GO_HOME, "Go Home"}, + {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; + + int selectedIndex = 0; + bool updateRequired = false; + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + + const std::function onBack; + const std::function onAction; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void renderScreen(); +}; From 95b7f8008c73a0ce6a0113d5b2d6864d74fd2652 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Mon, 19 Jan 2026 22:46:45 +0100 Subject: [PATCH 2/3] Refactor saving progress to dedicated function. --- src/activities/reader/EpubReaderActivity.cpp | 47 +++++++++----------- src/activities/reader/EpubReaderActivity.h | 1 + 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 2d7bbb4..d8a4c6d 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -182,31 +182,21 @@ void EpubReaderActivity::loop() { } case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { xSemaphoreTake(renderingMutex, portMAX_DELAY); - section.reset(); if (epub) { - // 2. BACKUP: Read current progress - // We use the current variables that track our position - uint16_t backupSpine = currentSpineIndex; - uint16_t backupPage = nextPageNumber; + // 2. BACKUP: Read current progress + // We use the current variables that track our position + uint16_t backupSpine = currentSpineIndex; + uint16_t backupPage = section->currentPage; - // 3. WIPE: Clear the cache directory - epub->clearCache(); + section.reset(); + // 3. WIPE: Clear the cache directory + epub->clearCache(); - // 4. RESTORE: Re-setup the directory and rewrite the progress file - epub->setupCacheDir(); + // 4. RESTORE: Re-setup the directory and rewrite the progress file + epub->setupCacheDir(); - FsFile f; - if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { - uint8_t data[4]; - data[0] = backupSpine & 0xFF; - data[1] = (backupSpine >> 8) & 0xFF; - data[2] = backupPage & 0xFF; - data[3] = (backupPage >> 8) & 0xFF; - f.write(data, 4); - f.close(); - Serial.println("[ERS] Progress restored after cache clear"); - } - } + saveProgress(backupSpine, backupPage); + } exitActivity(); updateRequired = true; xSemaphoreGive(renderingMutex); @@ -440,19 +430,24 @@ void EpubReaderActivity::renderScreen() { renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); } + saveProgress(currentSpineIndex, section->currentPage); +} +void EpubReaderActivity::saveProgress(int spineIndex, int page) { FsFile f; if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; - data[0] = currentSpineIndex & 0xFF; - data[1] = (currentSpineIndex >> 8) & 0xFF; - data[2] = section->currentPage & 0xFF; - data[3] = (section->currentPage >> 8) & 0xFF; + data[0] = spineIndex & 0xFF; + data[1] = (spineIndex >> 8) & 0xFF; + data[2] = page & 0xFF; + data[3] = (page >> 8) & 0xFF; f.write(data, 4); f.close(); + Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, page); + } else { + Serial.printf("[ERS] Could not save progress!\n"); } } - void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 63d4887..3f245e0 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -25,6 +25,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity { void renderContents(std::unique_ptr page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; + void saveProgress(int spineIndex, int page); public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, From 7fe6dda88ada843d370205349cb586a409e0c061 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 20 Jan 2026 08:04:30 +0100 Subject: [PATCH 3/3] Formatting fixes --- .../reader/EpubReaderMenuActivity.cpp | 8 ++++--- .../reader/EpubReaderMenuActivity.h | 23 ++++++++----------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index cac6f4a..2503575 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -1,5 +1,7 @@ #include "EpubReaderMenuActivity.h" + #include + #include "fontIds.h" void EpubReaderMenuActivity::onEnter() { @@ -46,11 +48,11 @@ void EpubReaderMenuActivity::loop() { // Use local variables for items we need to check after potential deletion if (mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left) ) { + mappedInput.wasReleased(MappedInputManager::Button::Left)) { selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size(); updateRequired = true; } else if (mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right)) { + mappedInput.wasReleased(MappedInputManager::Button::Right)) { selectedIndex = (selectedIndex + 1) % menuItems.size(); updateRequired = true; } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -65,7 +67,7 @@ void EpubReaderMenuActivity::loop() { return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onBack(); - return; // Also return here just in case + return; // Also return here just in case } } diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 79ed4de..f9ca767 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -3,23 +3,21 @@ #include #include #include -#include -#include -#include -#include "MappedInputManager.h" +#include +#include +#include + #include "../ActivityWithSubactivity.h" +#include "MappedInputManager.h" class EpubReaderMenuActivity final : public ActivityWithSubactivity { public: enum class MenuAction { SELECT_CHAPTER, GO_HOME, DELETE_CACHE }; explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack, - const std::function& onAction) - : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), - onBack(onBack), - onAction(onAction) {} + const std::function& onBack, const std::function& onAction) + : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), onBack(onBack), onAction(onAction) {} void onEnter() override; void onExit() override; @@ -31,10 +29,9 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { std::string label; }; - const std::vector menuItems = { - {MenuAction::SELECT_CHAPTER, "Select Chapter"}, - {MenuAction::GO_HOME, "Go Home"}, - {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; + const std::vector menuItems = {{MenuAction::SELECT_CHAPTER, "Select Chapter"}, + {MenuAction::GO_HOME, "Go Home"}, + {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; int selectedIndex = 0; bool updateRequired = false;