diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 06973c92..e4d72b0c 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -201,7 +201,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s * **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others. * **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others. -What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi. +What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi. --- diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 581a364f..cf67108b 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -123,9 +123,7 @@ bool Section::clearCache() const { bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, - const std::function& progressSetupFn, - const std::function& progressFn) { - constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB + const std::function& popupFn) { const auto localPath = epub->getSpineItem(spineIndex).href; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; @@ -171,11 +169,6 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize); - // Only show progress bar for larger chapters where rendering overhead is worth it - if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) { - progressSetupFn(); - } - if (!SdMan.openFileForWrite("SCT", filePath, file)) { return false; } @@ -186,8 +179,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c ChapterHtmlSlimParser visitor( tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled, - [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, - progressFn); + [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn); Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 5b726141..5fdf210a 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -33,7 +33,6 @@ class Section { bool clearCache() const; bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, - const std::function& progressSetupFn = nullptr, - const std::function& progressFn = nullptr); + const std::function& popupFn = nullptr); std::unique_ptr loadPageFromSectionFile(); }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 298c4ec6..ac1f537f 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -10,8 +10,8 @@ const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]); -// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it -constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB +// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it +constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); @@ -289,10 +289,10 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return false; } - // Get file size for progress calculation - const size_t totalSize = file.size(); - size_t bytesRead = 0; - int lastProgress = -1; + // Get file size to decide whether to show indexing popup. + if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) { + popupFn(); + } XML_SetUserData(parser, this); XML_SetElementHandler(parser, startElement, endElement); @@ -322,17 +322,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return false; } - // Update progress (call every 10% change to avoid too frequent updates) - // Only show progress for larger chapters where rendering overhead is worth it - bytesRead += len; - if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) { - const int progress = static_cast((bytesRead * 100) / totalSize); - if (lastProgress / 10 != progress / 10) { - lastProgress = progress; - progressFn(progress); - } - } - done = file.available() == 0; if (XML_ParseBuffer(parser, static_cast(len), done) == XML_STATUS_ERROR) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 2d8ebe5c..38202e6e 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -18,7 +18,7 @@ class ChapterHtmlSlimParser { const std::string& filepath; GfxRenderer& renderer; std::function)> completePageFn; - std::function progressFn; // Progress callback (0-100) + std::function popupFn; // Popup callback int depth = 0; int skipUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX; @@ -52,7 +52,7 @@ class ChapterHtmlSlimParser { const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, const std::function)>& completePageFn, - const std::function& progressFn = nullptr) + const std::function& popupFn = nullptr) : filepath(filepath), renderer(renderer), fontId(fontId), @@ -63,7 +63,7 @@ class ChapterHtmlSlimParser { viewportHeight(viewportHeight), hyphenationEnabled(hyphenationEnabled), completePageFn(completePageFn), - progressFn(progressFn) {} + popupFn(popupFn) {} ~ChapterHtmlSlimParser() = default; bool parseAndBuildPages(); void addLineToPage(std::shared_ptr line); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 733975f4..86ddc8fc 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -56,7 +56,7 @@ class GfxRenderer { int getScreenHeight() const; void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; // EXPERIMENTAL: Windowed update - display only a rectangular region - void displayWindow(int x, int y, int width, int height) const; + // void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; void clearScreen(uint8_t color = 0xFF) const; diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index ef47dfc5..72f7faf0 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -42,6 +42,38 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); } +ScreenComponents::PopupLayout ScreenComponents::drawPopup(const GfxRenderer& renderer, const char* message) { + constexpr int margin = 15; + constexpr int y = 60; + const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD); + const int textHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int w = textWidth + margin * 2; + const int h = textHeight + margin * 2; + const int x = (renderer.getScreenWidth() - w) / 2; + + renderer.fillRect(x - 2, y - 2, w + 4, h + 4, true); // frame thickness 2 + renderer.fillRect(x, y, w, h, false); + + const int textX = x + (w - textWidth) / 2; + const int textY = y + margin - 2; + renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return {x, y, w, h}; +} + +void ScreenComponents::fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, const int progress) { + constexpr int barHeight = 4; + const int barWidth = layout.width - 30; // twice the margin in drawPopup to match text width + const int barX = layout.x + (layout.width - barWidth) / 2; + const int barY = layout.y + layout.height - 10; + + int fillWidth = barWidth * progress / 100; + + renderer.fillRect(barX, barY, fillWidth, barHeight, true); + + renderer.displayBuffer(HalDisplay::FAST_REFRESH); +} + void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) { int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 15403f60..78ed5920 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -15,9 +15,20 @@ class ScreenComponents { public: static const int BOOK_PROGRESS_BAR_HEIGHT = 4; + struct PopupLayout { + int x; + int y; + int width; + int height; + }; + static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); + static PopupLayout drawPopup(const GfxRenderer& renderer, const char* message); + + static void fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, int progress); + // Draw a horizontal tab bar with underline indicator for selected tab // Returns the height of the tab bar (for positioning content below) static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector& tabs); diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index aace2095..7ffc5851 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -8,13 +8,15 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" +#include "ScreenComponents.h" #include "fontIds.h" #include "images/CrossLarge.h" #include "util/StringUtils.h" void SleepActivity::onEnter() { Activity::onEnter(); - renderPopup("Entering Sleep..."); + + ScreenComponents::drawPopup(renderer, "Entering Sleep..."); if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) { return renderBlankSleepScreen(); @@ -31,20 +33,6 @@ void SleepActivity::onEnter() { renderDefaultSleepScreen(); } -void SleepActivity::renderPopup(const char* message) const { - const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD); - constexpr int margin = 20; - const int x = (renderer.getScreenWidth() - textWidth - margin * 2) / 2; - constexpr int y = 117; - const int w = textWidth + margin * 2; - const int h = renderer.getLineHeight(UI_12_FONT_ID) + margin * 2; - // renderer.clearScreen(); - renderer.fillRect(x - 5, y - 5, w + 10, h + 10, true); - renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false); - renderer.drawText(UI_12_FONT_ID, x + margin, y + margin, message, true, EpdFontFamily::BOLD); - renderer.displayBuffer(); -} - void SleepActivity::renderCustomSleepScreen() const { // Check if we have a /sleep directory auto dir = SdMan.open("/sleep"); diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 283220ce..87df8ba1 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -10,7 +10,6 @@ class SleepActivity final : public Activity { void onEnter() override; private: - void renderPopup(const char* message) const; void renderDefaultSleepScreen() const; void renderCustomSleepScreen() const; void renderCoverSleepScreen() const; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 5c45223b..8bf83a93 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -266,9 +266,9 @@ void WifiSelectionActivity::checkConnectionStatus() { } if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { - connectionError = "Connection failed"; + connectionError = "Error: General failure"; if (status == WL_NO_SSID_AVAIL) { - connectionError = "Network not found"; + connectionError = "Error: Network not found"; } state = WifiSelectionState::CONNECTION_FAILED; updateRequired = true; @@ -278,7 +278,7 @@ void WifiSelectionActivity::checkConnectionStatus() { // Check for timeout if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { WiFi.disconnect(); - connectionError = "Connection timeout"; + connectionError = "Error: Connection timeout"; state = WifiSelectionState::CONNECTION_FAILED; updateRequired = true; return; @@ -689,7 +689,7 @@ void WifiSelectionActivity::renderForgetPrompt() const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 3) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network?", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + selectedSSID; if (ssidInfo.length() > 28) { @@ -697,7 +697,7 @@ void WifiSelectionActivity::renderForgetPrompt() const { } renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); - renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Remove saved password?"); + renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Forget network and remove saved password?"); // Draw Cancel/Forget network buttons const int buttonY = top + 80; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 58668c68..17d81632 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -130,31 +130,9 @@ 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] { - 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) { - // Handle sync position - if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { - currentSpineIndex = newSpineIndex; - nextPageNumber = newPage; - section.reset(); - } - exitActivity(); - updateRequired = true; - })); + enterNewActivity(new EpubReaderMenuActivity( + this->renderer, this->mappedInput, epub->getTitle(), [this]() { onReaderMenuBack(); }, + [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); xSemaphoreGive(renderingMutex); } @@ -242,6 +220,89 @@ void EpubReaderActivity::loop() { } } +void EpubReaderActivity::onReaderMenuBack() { + exitActivity(); + updateRequired = true; +} + +void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { + 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; + } + 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); + if (epub) { + // 2. BACKUP: Read current progress + // We use the current variables that track our position + uint16_t backupSpine = currentSpineIndex; + uint16_t backupPage = section->currentPage; + uint16_t backupPageCount = section->pageCount; + + section.reset(); + // 3. WIPE: Clear the cache directory + epub->clearCache(); + + // 4. RESTORE: Re-setup the directory and rewrite the progress file + epub->setupCacheDir(); + + saveProgress(backupSpine, backupPage, backupPageCount); + } + exitActivity(); + updateRequired = true; + xSemaphoreGive(renderingMutex); + if (onGoHome) onGoHome(); + break; + } + } +} + void EpubReaderActivity::displayTaskLoop() { while (true) { if (updateRequired) { @@ -308,49 +369,11 @@ void EpubReaderActivity::renderScreen() { viewportHeight, SETTINGS.hyphenationEnabled)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); - // Progress bar dimensions - constexpr int barWidth = 200; - constexpr int barHeight = 10; - constexpr int boxMargin = 20; - const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing..."); - const int boxWidthWithBar = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2; - const int boxWidthNoBar = textWidth + boxMargin * 2; - const int boxHeightWithBar = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3; - const int boxHeightNoBar = renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; - const int boxXWithBar = (renderer.getScreenWidth() - boxWidthWithBar) / 2; - const int boxXNoBar = (renderer.getScreenWidth() - boxWidthNoBar) / 2; - constexpr int boxY = 50; - const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2; - const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; - - // Always show "Indexing..." text first - { - renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false); - renderer.drawText(UI_12_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing..."); - renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10); - renderer.displayBuffer(); - pagesUntilFullRefresh = 0; - } - - // Setup callback - only called for chapters >= 50KB, redraws with progress bar - auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] { - renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false); - renderer.drawText(UI_12_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing..."); - renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10); - renderer.drawRect(barX, barY, barWidth, barHeight); - renderer.displayBuffer(); - }; - - // Progress callback to update progress bar - auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) { - const int fillWidth = (barWidth - 2) * progress / 100; - renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); - }; + const auto popupFn = [this]() { ScreenComponents::drawPopup(renderer, "Indexing..."); }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) { + viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; @@ -407,21 +430,26 @@ 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, section->pageCount); +} +void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) { FsFile f; if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[6]; data[0] = currentSpineIndex & 0xFF; data[1] = (currentSpineIndex >> 8) & 0xFF; - data[2] = section->currentPage & 0xFF; - data[3] = (section->currentPage >> 8) & 0xFF; - data[4] = section->pageCount & 0xFF; - data[5] = (section->pageCount >> 8) & 0xFF; + data[2] = currentPage & 0xFF; + data[3] = (currentPage >> 8) & 0xFF; + data[4] = pageCount & 0xFF; + data[5] = (pageCount >> 8) & 0xFF; f.write(data, 6); f.close(); + Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, currentPage); + } 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 ab4aff2d..ca7c0dc9 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -5,6 +5,7 @@ #include #include +#include "EpubReaderMenuActivity.h" #include "activities/ActivityWithSubactivity.h" class EpubReaderActivity final : public ActivityWithSubactivity { @@ -27,6 +28,9 @@ 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 currentPage, int pageCount); + void onReaderMenuBack(); + void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 1b35e143..a77b37d0 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -181,9 +181,7 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const int pageItems = getPageItems(); const int totalItems = getTotalItems(); - const std::string title = - renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Chapter", true, EpdFontFamily::BOLD); const auto pageStartIndex = selectorIndex / pageItems * pageItems; renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); @@ -208,8 +206,11 @@ void EpubReaderChapterSelectionActivity::renderScreen() { } } - const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // Skip button hints in landscape CW mode (they overlap content) + if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { + 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.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp new file mode 100644 index 00000000..5ce4881d --- /dev/null +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -0,0 +1,103 @@ +#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 + const std::string truncTitle = + renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - 40, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, truncTitle.c_str(), true, EpdFontFamily::BOLD); + + // Menu Items + constexpr int startY = 60; + constexpr int lineHeight = 30; + + 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(0, displayY, pageWidth - 1, lineHeight, true); + } + + renderer.drawText(UI_10_FONT_ID, 20, displayY, 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 00000000..bd253f81 --- /dev/null +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include +#include + +#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::string& title, + const std::function& onBack, const std::function& onAction) + : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), + title(title), + 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, "Go to 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; + std::string title = "Reader Menu"; + + const std::function onBack; + const std::function onAction; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void renderScreen(); +}; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index e9303de3..8e756278 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -207,28 +207,10 @@ void TxtReaderActivity::buildPageIndex() { size_t offset = 0; const size_t fileSize = txt->getFileSize(); - int lastProgressPercent = -1; Serial.printf("[%lu] [TRS] Building page index for %zu bytes...\n", millis(), fileSize); - // Progress bar dimensions (matching EpubReaderActivity style) - constexpr int barWidth = 200; - constexpr int barHeight = 10; - constexpr int boxMargin = 20; - const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing..."); - const int boxWidth = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2; - const int boxHeight = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3; - const int boxX = (renderer.getScreenWidth() - boxWidth) / 2; - constexpr int boxY = 50; - const int barX = boxX + (boxWidth - barWidth) / 2; - const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; - - // Draw initial progress box - renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); - renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Indexing..."); - renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); - renderer.drawRect(barX, barY, barWidth, barHeight); - renderer.displayBuffer(); + ScreenComponents::drawPopup(renderer, "Indexing..."); while (offset < fileSize) { std::vector tempLines; @@ -248,17 +230,6 @@ void TxtReaderActivity::buildPageIndex() { pageOffsets.push_back(offset); } - // Update progress bar every 10% (matching EpubReaderActivity logic) - int progressPercent = (offset * 100) / fileSize; - if (lastProgressPercent / 10 != progressPercent / 10) { - lastProgressPercent = progressPercent; - - // Fill progress bar - const int fillWidth = (barWidth - 2) * progressPercent / 100; - renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); - } - // Yield to other tasks periodically if (pageOffsets.size() % 20 == 0) { vTaskDelay(1); @@ -402,9 +373,6 @@ void TxtReaderActivity::renderScreen() { // Initialize reader if not done if (!initialized) { - renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Indexing...", true, EpdFontFamily::BOLD); - renderer.displayBuffer(); initializeReader(); } diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index b2cfecaa..ad806a30 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -149,8 +149,11 @@ void XtcReaderChapterSelectionActivity::renderScreen() { renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex); } - const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // Skip button hints in landscape CW mode (they overlap content) + if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { + 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/main.cpp b/src/main.cpp index f099cba1..89c4e13c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -329,7 +329,6 @@ void setup() { // Clear app state to avoid getting into a boot loop if the epub doesn't load const auto path = APP_STATE.openEpubPath; APP_STATE.openEpubPath = ""; - APP_STATE.lastSleepImage = 0; APP_STATE.saveToFile(); onGoToReader(path, MyLibraryActivity::Tab::Recent); }