A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Auto Page Turn for Epub Reader (#1219)

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
- Implements auto page turn feature for epub reader in the reader
submenu

* **What changes are included?**
- added auto page turn feature in epub reader in the submenu
- currently there are 5 settings, `OFF, 1, 3, 6, 12` pages per minute

## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
- Replacement PR for #723
- when auto turn is enabled, space reserved for chapter title will be
used to indicate auto page turn being active
- Back and Confirm button is used to disable it

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**Partially (mainly code
reviews)**_

authored by

GenesiaW and committed by
GitHub
3b4f2a11 09cef707

+181 -46
+2
lib/I18n/translations/english.yaml
··· 334 334 STR_NO_FOOTNOTES: "No footnotes on this page" 335 335 STR_LINK: "[link]" 336 336 STR_SCREENSHOT_BUTTON: "Take screenshot" 337 + STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " 338 + STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
+9
src/activities/ActivityManager.cpp
··· 250 250 isLocked = false; 251 251 } 252 252 } 253 + 254 + /** 255 + * 256 + * Checks if renderingMutex is busy. 257 + * 258 + * @return true if renderingMutex is busy, otherwise false. 259 + * 260 + */ 261 + bool RenderLock::peek() { return xQueuePeek(activityManager.renderingMutex, NULL, 0) != pdTRUE; };
+1
src/activities/ActivityResult.h
··· 20 20 struct MenuResult { 21 21 int action = -1; 22 22 uint8_t orientation = 0; 23 + uint8_t pageTurnOption = 0; 23 24 }; 24 25 25 26 struct ChapterResult {
+1
src/activities/RenderLock.h
··· 13 13 RenderLock& operator=(const RenderLock&) = delete; 14 14 ~RenderLock(); 15 15 void unlock(); 16 + static bool peek(); 16 17 };
+123 -35
src/activities/reader/EpubReaderActivity.cpp
··· 26 26 // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() 27 27 constexpr unsigned long skipChapterMs = 700; 28 28 constexpr unsigned long goHomeMs = 1000; 29 + // pages per minute, first item is 1 to prevent division by zero if accessed 30 + const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12}; 29 31 30 32 int clampPercent(int percent) { 31 33 if (percent < 0) { ··· 126 128 return; 127 129 } 128 130 131 + if (automaticPageTurnActive) { 132 + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || 133 + mappedInput.wasReleased(MappedInputManager::Button::Back)) { 134 + automaticPageTurnActive = false; 135 + // updates chapter title space to indicate page turn disabled 136 + requestUpdate(); 137 + return; 138 + } 139 + 140 + if (!section) { 141 + requestUpdate(); 142 + return; 143 + } 144 + 145 + // Skips page turn if renderingMutex is busy 146 + if (RenderLock::peek()) { 147 + lastPageTurnTime = millis(); 148 + return; 149 + } 150 + 151 + if ((millis() - lastPageTurnTime) >= pageTurnDuration) { 152 + pageTurn(true); 153 + return; 154 + } 155 + } 156 + 129 157 // Enter reader menu activity. 130 158 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 131 159 const int currentPage = section ? section->currentPage + 1 : 0; ··· 143 171 // Always apply orientation change even if the menu was cancelled 144 172 const auto& menu = std::get<MenuResult>(result.data); 145 173 applyOrientation(menu.orientation); 174 + toggleAutoPageTurn(menu.pageTurnOption); 146 175 if (!result.isCancelled) { 147 176 onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action)); 148 177 } ··· 194 223 const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; 195 224 196 225 if (skipChapter) { 226 + lastPageTurnTime = millis(); 197 227 // We don't want to delete the section mid-render, so grab the semaphore 198 228 { 199 229 RenderLock lock(*this); ··· 212 242 } 213 243 214 244 if (prevTriggered) { 215 - if (section->currentPage > 0) { 216 - section->currentPage--; 217 - } else if (currentSpineIndex > 0) { 218 - // We don't want to delete the section mid-render, so grab the semaphore 219 - { 220 - RenderLock lock(*this); 221 - nextPageNumber = UINT16_MAX; 222 - currentSpineIndex--; 223 - section.reset(); 224 - } 225 - } 226 - requestUpdate(); 245 + pageTurn(false); 227 246 } else { 228 - if (section->currentPage < section->pageCount - 1) { 229 - section->currentPage++; 230 - } else { 231 - // We don't want to delete the section mid-render, so grab the semaphore 232 - { 233 - RenderLock lock(*this); 234 - nextPageNumber = 0; 235 - currentSpineIndex++; 236 - section.reset(); 237 - } 238 - } 239 - requestUpdate(); 247 + pageTurn(true); 240 248 } 241 249 } 242 250 ··· 452 460 } 453 461 } 454 462 463 + void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) { 464 + if (selectedPageTurnOption == 0 || selectedPageTurnOption >= PAGE_TURN_LABELS.size()) { 465 + automaticPageTurnActive = false; 466 + return; 467 + } 468 + 469 + lastPageTurnTime = millis(); 470 + // calculates page turn duration by dividing by number of pages 471 + pageTurnDuration = (1UL * 60 * 1000) / PAGE_TURN_LABELS[selectedPageTurnOption]; 472 + automaticPageTurnActive = true; 473 + 474 + const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); 475 + // resets cached section so that space is reserved for auto page turn indicator when None or progress bar only 476 + if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { 477 + // Preserve current reading position so we can restore after reflow. 478 + RenderLock lock(*this); 479 + if (section) { 480 + cachedSpineIndex = currentSpineIndex; 481 + cachedChapterTotalPageCount = section->pageCount; 482 + nextPageNumber = section->currentPage; 483 + } 484 + section.reset(); 485 + } 486 + } 487 + 488 + void EpubReaderActivity::pageTurn(bool isForwardTurn) { 489 + if (isForwardTurn) { 490 + if (section->currentPage < section->pageCount - 1) { 491 + section->currentPage++; 492 + } else { 493 + // We don't want to delete the section mid-render, so grab the semaphore 494 + { 495 + RenderLock lock(*this); 496 + nextPageNumber = 0; 497 + currentSpineIndex++; 498 + section.reset(); 499 + } 500 + } 501 + } else { 502 + if (section->currentPage > 0) { 503 + section->currentPage--; 504 + } else if (currentSpineIndex > 0) { 505 + // We don't want to delete the section mid-render, so grab the semaphore 506 + { 507 + RenderLock lock(*this); 508 + nextPageNumber = UINT16_MAX; 509 + currentSpineIndex--; 510 + section.reset(); 511 + } 512 + } 513 + } 514 + lastPageTurnTime = millis(); 515 + requestUpdate(); 516 + } 517 + 455 518 // TODO: Failure handling 456 519 void EpubReaderActivity::render(RenderLock&& lock) { 457 520 if (!epub) { ··· 472 535 renderer.clearScreen(); 473 536 renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); 474 537 renderer.displayBuffer(); 538 + automaticPageTurnActive = false; 475 539 return; 476 540 } 477 541 ··· 482 546 orientedMarginTop += SETTINGS.screenMargin; 483 547 orientedMarginLeft += SETTINGS.screenMargin; 484 548 orientedMarginRight += SETTINGS.screenMargin; 485 - orientedMarginBottom += 486 - std::max(SETTINGS.screenMargin, static_cast<uint8_t>(UITheme::getInstance().getStatusBarHeight())); 549 + 550 + const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); 551 + 552 + // reserves space for automatic page turn indicator when no status bar or progress bar only 553 + if (automaticPageTurnActive && 554 + (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight())) { 555 + orientedMarginBottom += 556 + std::max(SETTINGS.screenMargin, 557 + static_cast<uint8_t>(statusBarHeight + UITheme::getInstance().getMetrics().statusBarVerticalMargin)); 558 + } else { 559 + orientedMarginBottom += std::max(SETTINGS.screenMargin, statusBarHeight); 560 + } 487 561 488 562 if (!section) { 489 563 const auto filepath = epub->getSpineItem(currentSpineIndex).href; ··· 546 620 renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD); 547 621 renderStatusBar(); 548 622 renderer.displayBuffer(); 623 + automaticPageTurnActive = false; 549 624 return; 550 625 } 551 626 ··· 554 629 renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD); 555 630 renderStatusBar(); 556 631 renderer.displayBuffer(); 632 + automaticPageTurnActive = false; 557 633 return; 558 634 } 559 635 ··· 564 640 section->clearCache(); 565 641 section.reset(); 566 642 requestUpdate(); // Try again after clearing cache 567 - // TODO: prevent infinite loop if the page keeps failing to load for some reason 643 + // TODO: prevent infinite loop if the page keeps failing to load for some reason 644 + automaticPageTurnActive = false; 568 645 return; 569 646 } 570 647 ··· 669 746 const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0; 670 747 const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100; 671 748 672 - const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); 673 749 std::string title; 674 750 675 - if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { 676 - if (tocIndex == -1) { 677 - title = tr(STR_UNNAMED); 678 - } else { 751 + int textYOffset = 0; 752 + 753 + if (automaticPageTurnActive) { 754 + title = tr(STR_AUTO_TURN_ENABLED) + std::to_string(60 * 1000 / pageTurnDuration); 755 + 756 + // calculates textYOffset when rendering title in status bar 757 + const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); 758 + 759 + // offsets text if no status bar or progress bar only 760 + if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { 761 + textYOffset += UITheme::getInstance().getMetrics().statusBarVerticalMargin; 762 + } 763 + 764 + } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { 765 + title = tr(STR_UNNAMED); 766 + const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); 767 + if (tocIndex != -1) { 679 768 const auto tocItem = epub->getTocItem(tocIndex); 680 769 title = tocItem.title; 681 770 } 771 + 682 772 } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::BOOK_TITLE) { 683 773 title = epub->getTitle(); 684 - } else { 685 - title = ""; 686 774 } 687 775 688 - GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title); 776 + GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset); 689 777 } 690 778 691 779 void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
+5
src/activities/reader/EpubReaderActivity.h
··· 14 14 int pagesUntilFullRefresh = 0; 15 15 int cachedSpineIndex = 0; 16 16 int cachedChapterTotalPageCount = 0; 17 + unsigned long lastPageTurnTime = 0UL; 18 + unsigned long pageTurnDuration = 0UL; 17 19 // Signals that the next render should reposition within the newly loaded section 18 20 // based on a cross-book percentage jump. 19 21 bool pendingPercentJump = false; ··· 21 23 float pendingSpineProgress = 0.0f; 22 24 bool pendingScreenshot = false; 23 25 bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit 26 + bool automaticPageTurnActive = false; 24 27 25 28 // Footnote support 26 29 std::vector<FootnoteEntry> currentPageFootnotes; ··· 40 43 void jumpToPercent(int percent); 41 44 void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); 42 45 void applyOrientation(uint8_t orientation); 46 + void toggleAutoPageTurn(uint8_t selectedPageTurnOption); 47 + void pageTurn(bool isForwardTurn); 43 48 44 49 // Footnote navigation 45 50 void navigateToHref(const std::string& href, bool savePosition = false);
+17 -3
src/activities/reader/EpubReaderMenuActivity.cpp
··· 21 21 22 22 std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { 23 23 std::vector<MenuItem> items; 24 - items.reserve(9); 24 + items.reserve(10); 25 25 items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); 26 26 if (hasFootnotes) { 27 27 items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); 28 28 } 29 29 items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}); 30 + items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); 30 31 items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); 31 32 items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); 32 33 items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); ··· 64 65 return; 65 66 } 66 67 67 - setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation}); 68 + if (selectedAction == MenuAction::AUTO_PAGE_TURN) { 69 + selectedPageTurnOption = (selectedPageTurnOption + 1) % pageTurnLabels.size(); 70 + requestUpdate(); 71 + return; 72 + } 73 + 74 + setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption}); 68 75 finish(); 69 76 return; 70 77 } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 71 78 ActivityResult result; 72 79 result.isCancelled = true; 73 - result.data = MenuResult{-1, pendingOrientation}; 80 + result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption}; 74 81 setResult(std::move(result)); 75 82 finish(); 76 83 return; ··· 130 137 if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { 131 138 // Render current orientation value on the right edge of the content area. 132 139 const char* value = I18N.get(orientationLabels[pendingOrientation]); 140 + const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); 141 + renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); 142 + } 143 + 144 + if (menuItems[i].action == MenuAction::AUTO_PAGE_TURN) { 145 + // Render current page turn value on the right edge of the content area. 146 + const auto value = pageTurnLabels[selectedPageTurnOption]; 133 147 const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); 134 148 renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); 135 149 }
+3
src/activities/reader/EpubReaderMenuActivity.h
··· 15 15 SELECT_CHAPTER, 16 16 FOOTNOTES, 17 17 GO_TO_PERCENT, 18 + AUTO_PAGE_TURN, 18 19 ROTATE_SCREEN, 19 20 SCREENSHOT, 20 21 DISPLAY_QR, ··· 48 49 ButtonNavigator buttonNavigator; 49 50 std::string title = "Reader Menu"; 50 51 uint8_t pendingOrientation = 0; 52 + uint8_t selectedPageTurnOption = 0; 51 53 const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, 52 54 StrId::STR_LANDSCAPE_CCW}; 55 + const std::vector<const char*> pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"}; 53 56 int currentPage = 0; 54 57 int totalPages = 0; 55 58 int bookProgressPercent = 0;
+4 -2
src/activities/reader/TxtReaderActivity.cpp
··· 431 431 432 432 void TxtReaderActivity::renderStatusBar() const { 433 433 const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; 434 - std::string title = txt->getTitle(); 435 - 434 + std::string title; 435 + if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { 436 + title = txt->getTitle(); 437 + } 436 438 GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title); 437 439 } 438 440
+1 -1
src/activities/settings/StatusBarSettingsActivity.cpp
··· 156 156 std::string title; 157 157 if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::BOOK_TITLE) { 158 158 title = tr(STR_EXAMPLE_BOOK); 159 - } else { 159 + } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { 160 160 title = tr(STR_EXAMPLE_CHAPTER); 161 161 } 162 162
+7
src/components/UITheme.cpp
··· 103 103 return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) + 104 104 (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0); 105 105 } 106 + 107 + int UITheme::getProgressBarHeight() { 108 + const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); 109 + const bool showProgressBar = 110 + SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; 111 + return (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0); 112 + }
+1
src/components/UITheme.h
··· 23 23 static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight); 24 24 static UIIcon getFileIcon(std::string filename); 25 25 static int getStatusBarHeight(); 26 + static int getProgressBarHeight(); 26 27 27 28 private: 28 29 const ThemeMetrics* currentMetrics;
+5 -4
src/components/themes/BaseTheme.cpp
··· 629 629 } 630 630 631 631 void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, 632 - const int pageCount, std::string title, const int paddingBottom) const { 632 + const int pageCount, std::string title, const int paddingBottom, 633 + const int textYOffset) const { 633 634 auto metrics = UITheme::getInstance().getMetrics(); 634 635 int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; 635 636 renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, ··· 637 638 638 639 // Draw Progress Text 639 640 const auto screenHeight = renderer.getScreenHeight(); 640 - const auto textY = 641 - screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4; 641 + auto textY = screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4; 642 642 int progressTextWidth = 0; 643 643 644 644 if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) { ··· 688 688 } 689 689 690 690 // Draw Title 691 - if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { 691 + if (!title.empty()) { 692 + textY -= textYOffset; 692 693 // Centered chapter title text 693 694 // Page width minus existing content with 30px padding on each side 694 695 const int rendererableScreenWidth =
+2 -1
src/components/themes/BaseTheme.h
··· 136 136 virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const; 137 137 virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const; 138 138 virtual void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, 139 - const int pageCount, std::string title, const int paddingBottom = 0) const; 139 + const int pageCount, std::string title, const int paddingBottom = 0, 140 + const int textYOffset = 0) const; 140 141 virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; 141 142 virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; 142 143 virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const;