diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 12656a47..8bc6105b 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -334,3 +334,5 @@ STR_FOOTNOTES: "Footnotes" STR_NO_FOOTNOTES: "No footnotes on this page" STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Take screenshot" +STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " +STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 0f612dd4..52c506d7 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -250,3 +250,12 @@ void RenderLock::unlock() { isLocked = false; } } + +/** + * + * Checks if renderingMutex is busy. + * + * @return true if renderingMutex is busy, otherwise false. + * + */ +bool RenderLock::peek() { return xQueuePeek(activityManager.renderingMutex, NULL, 0) != pdTRUE; }; diff --git a/src/activities/ActivityResult.h b/src/activities/ActivityResult.h index dc947004..4062fce9 100644 --- a/src/activities/ActivityResult.h +++ b/src/activities/ActivityResult.h @@ -20,6 +20,7 @@ struct KeyboardResult { struct MenuResult { int action = -1; uint8_t orientation = 0; + uint8_t pageTurnOption = 0; }; struct ChapterResult { diff --git a/src/activities/RenderLock.h b/src/activities/RenderLock.h index eeda2cf6..5fb7c3df 100644 --- a/src/activities/RenderLock.h +++ b/src/activities/RenderLock.h @@ -13,4 +13,5 @@ class RenderLock { RenderLock& operator=(const RenderLock&) = delete; ~RenderLock(); void unlock(); + static bool peek(); }; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index f5c54cda..cc0a100b 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -26,6 +26,8 @@ namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() constexpr unsigned long skipChapterMs = 700; constexpr unsigned long goHomeMs = 1000; +// pages per minute, first item is 1 to prevent division by zero if accessed +const std::vector PAGE_TURN_LABELS = {1, 1, 3, 6, 12}; int clampPercent(int percent) { if (percent < 0) { @@ -126,6 +128,32 @@ void EpubReaderActivity::loop() { return; } + if (automaticPageTurnActive) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || + mappedInput.wasReleased(MappedInputManager::Button::Back)) { + automaticPageTurnActive = false; + // updates chapter title space to indicate page turn disabled + requestUpdate(); + return; + } + + if (!section) { + requestUpdate(); + return; + } + + // Skips page turn if renderingMutex is busy + if (RenderLock::peek()) { + lastPageTurnTime = millis(); + return; + } + + if ((millis() - lastPageTurnTime) >= pageTurnDuration) { + pageTurn(true); + return; + } + } + // Enter reader menu activity. if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const int currentPage = section ? section->currentPage + 1 : 0; @@ -143,6 +171,7 @@ void EpubReaderActivity::loop() { // Always apply orientation change even if the menu was cancelled const auto& menu = std::get(result.data); applyOrientation(menu.orientation); + toggleAutoPageTurn(menu.pageTurnOption); if (!result.isCancelled) { onReaderMenuConfirm(static_cast(menu.action)); } @@ -194,6 +223,7 @@ void EpubReaderActivity::loop() { const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; if (skipChapter) { + lastPageTurnTime = millis(); // We don't want to delete the section mid-render, so grab the semaphore { RenderLock lock(*this); @@ -212,31 +242,9 @@ void EpubReaderActivity::loop() { } if (prevTriggered) { - if (section->currentPage > 0) { - section->currentPage--; - } else if (currentSpineIndex > 0) { - // We don't want to delete the section mid-render, so grab the semaphore - { - RenderLock lock(*this); - nextPageNumber = UINT16_MAX; - currentSpineIndex--; - section.reset(); - } - } - requestUpdate(); + pageTurn(false); } else { - if (section->currentPage < section->pageCount - 1) { - section->currentPage++; - } else { - // We don't want to delete the section mid-render, so grab the semaphore - { - RenderLock lock(*this); - nextPageNumber = 0; - currentSpineIndex++; - section.reset(); - } - } - requestUpdate(); + pageTurn(true); } } @@ -452,6 +460,61 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) { } } +void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) { + if (selectedPageTurnOption == 0 || selectedPageTurnOption >= PAGE_TURN_LABELS.size()) { + automaticPageTurnActive = false; + return; + } + + lastPageTurnTime = millis(); + // calculates page turn duration by dividing by number of pages + pageTurnDuration = (1UL * 60 * 1000) / PAGE_TURN_LABELS[selectedPageTurnOption]; + automaticPageTurnActive = true; + + const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); + // resets cached section so that space is reserved for auto page turn indicator when None or progress bar only + if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { + // Preserve current reading position so we can restore after reflow. + RenderLock lock(*this); + if (section) { + cachedSpineIndex = currentSpineIndex; + cachedChapterTotalPageCount = section->pageCount; + nextPageNumber = section->currentPage; + } + section.reset(); + } +} + +void EpubReaderActivity::pageTurn(bool isForwardTurn) { + if (isForwardTurn) { + if (section->currentPage < section->pageCount - 1) { + section->currentPage++; + } else { + // We don't want to delete the section mid-render, so grab the semaphore + { + RenderLock lock(*this); + nextPageNumber = 0; + currentSpineIndex++; + section.reset(); + } + } + } else { + if (section->currentPage > 0) { + section->currentPage--; + } else if (currentSpineIndex > 0) { + // We don't want to delete the section mid-render, so grab the semaphore + { + RenderLock lock(*this); + nextPageNumber = UINT16_MAX; + currentSpineIndex--; + section.reset(); + } + } + } + lastPageTurnTime = millis(); + requestUpdate(); +} + // TODO: Failure handling void EpubReaderActivity::render(RenderLock&& lock) { if (!epub) { @@ -472,6 +535,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { renderer.clearScreen(); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); renderer.displayBuffer(); + automaticPageTurnActive = false; return; } @@ -482,8 +546,18 @@ void EpubReaderActivity::render(RenderLock&& lock) { orientedMarginTop += SETTINGS.screenMargin; orientedMarginLeft += SETTINGS.screenMargin; orientedMarginRight += SETTINGS.screenMargin; - orientedMarginBottom += - std::max(SETTINGS.screenMargin, static_cast(UITheme::getInstance().getStatusBarHeight())); + + const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); + + // reserves space for automatic page turn indicator when no status bar or progress bar only + if (automaticPageTurnActive && + (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight())) { + orientedMarginBottom += + std::max(SETTINGS.screenMargin, + static_cast(statusBarHeight + UITheme::getInstance().getMetrics().statusBarVerticalMargin)); + } else { + orientedMarginBottom += std::max(SETTINGS.screenMargin, statusBarHeight); + } if (!section) { const auto filepath = epub->getSpineItem(currentSpineIndex).href; @@ -546,6 +620,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD); renderStatusBar(); renderer.displayBuffer(); + automaticPageTurnActive = false; return; } @@ -554,6 +629,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD); renderStatusBar(); renderer.displayBuffer(); + automaticPageTurnActive = false; return; } @@ -564,7 +640,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { section->clearCache(); section.reset(); requestUpdate(); // Try again after clearing cache - // TODO: prevent infinite loop if the page keeps failing to load for some reason + // TODO: prevent infinite loop if the page keeps failing to load for some reason + automaticPageTurnActive = false; return; } @@ -669,23 +746,34 @@ void EpubReaderActivity::renderStatusBar() const { const float sectionChapterProg = (pageCount > 0) ? (static_cast(currentPage) / pageCount) : 0; const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100; - const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); std::string title; - if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { - if (tocIndex == -1) { - title = tr(STR_UNNAMED); - } else { + int textYOffset = 0; + + if (automaticPageTurnActive) { + title = tr(STR_AUTO_TURN_ENABLED) + std::to_string(60 * 1000 / pageTurnDuration); + + // calculates textYOffset when rendering title in status bar + const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); + + // offsets text if no status bar or progress bar only + if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { + textYOffset += UITheme::getInstance().getMetrics().statusBarVerticalMargin; + } + + } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { + title = tr(STR_UNNAMED); + const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); + if (tocIndex != -1) { const auto tocItem = epub->getTocItem(tocIndex); title = tocItem.title; } + } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::BOOK_TITLE) { title = epub->getTitle(); - } else { - title = ""; } - GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title); + GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset); } void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 0d1c4567..91c6f049 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -14,6 +14,8 @@ class EpubReaderActivity final : public Activity { int pagesUntilFullRefresh = 0; int cachedSpineIndex = 0; int cachedChapterTotalPageCount = 0; + unsigned long lastPageTurnTime = 0UL; + unsigned long pageTurnDuration = 0UL; // Signals that the next render should reposition within the newly loaded section // based on a cross-book percentage jump. bool pendingPercentJump = false; @@ -21,6 +23,7 @@ class EpubReaderActivity final : public Activity { float pendingSpineProgress = 0.0f; bool pendingScreenshot = false; bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit + bool automaticPageTurnActive = false; // Footnote support std::vector currentPageFootnotes; @@ -40,6 +43,8 @@ class EpubReaderActivity final : public Activity { void jumpToPercent(int percent); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); void applyOrientation(uint8_t orientation); + void toggleAutoPageTurn(uint8_t selectedPageTurnOption); + void pageTurn(bool isForwardTurn); // Footnote navigation void navigateToHref(const std::string& href, bool savePosition = false); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 5afb77e4..1d95d9b7 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -21,12 +21,13 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { std::vector items; - items.reserve(9); + items.reserve(10); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); if (hasFootnotes) { items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); } items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}); + items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); @@ -64,13 +65,19 @@ void EpubReaderMenuActivity::loop() { return; } - setResult(MenuResult{static_cast(selectedAction), pendingOrientation}); + if (selectedAction == MenuAction::AUTO_PAGE_TURN) { + selectedPageTurnOption = (selectedPageTurnOption + 1) % pageTurnLabels.size(); + requestUpdate(); + return; + } + + setResult(MenuResult{static_cast(selectedAction), pendingOrientation, selectedPageTurnOption}); finish(); return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; result.isCancelled = true; - result.data = MenuResult{-1, pendingOrientation}; + result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption}; setResult(std::move(result)); finish(); return; @@ -133,6 +140,13 @@ void EpubReaderMenuActivity::render(RenderLock&&) { const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); } + + if (menuItems[i].action == MenuAction::AUTO_PAGE_TURN) { + // Render current page turn value on the right edge of the content area. + const auto value = pageTurnLabels[selectedPageTurnOption]; + const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); + renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); + } } // Footer / Hints diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 4748c2f1..9ddba93d 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -15,6 +15,7 @@ class EpubReaderMenuActivity final : public Activity { SELECT_CHAPTER, FOOTNOTES, GO_TO_PERCENT, + AUTO_PAGE_TURN, ROTATE_SCREEN, SCREENSHOT, DISPLAY_QR, @@ -48,8 +49,10 @@ class EpubReaderMenuActivity final : public Activity { ButtonNavigator buttonNavigator; std::string title = "Reader Menu"; uint8_t pendingOrientation = 0; + uint8_t selectedPageTurnOption = 0; const std::vector orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}; + const std::vector pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"}; int currentPage = 0; int totalPages = 0; int bookProgressPercent = 0; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index b41f2ba1..994492ee 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -431,8 +431,10 @@ void TxtReaderActivity::renderPage() { void TxtReaderActivity::renderStatusBar() const { const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; - std::string title = txt->getTitle(); - + std::string title; + if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { + title = txt->getTitle(); + } GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title); } diff --git a/src/activities/settings/StatusBarSettingsActivity.cpp b/src/activities/settings/StatusBarSettingsActivity.cpp index e347e7a1..6ff0b34d 100644 --- a/src/activities/settings/StatusBarSettingsActivity.cpp +++ b/src/activities/settings/StatusBarSettingsActivity.cpp @@ -156,7 +156,7 @@ void StatusBarSettingsActivity::render(RenderLock&&) { std::string title; if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::BOOK_TITLE) { title = tr(STR_EXAMPLE_BOOK); - } else { + } else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { title = tr(STR_EXAMPLE_CHAPTER); } diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 8a8a559e..e29db872 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -103,3 +103,10 @@ int UITheme::getStatusBarHeight() { return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) + (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0); } + +int UITheme::getProgressBarHeight() { + const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); + const bool showProgressBar = + SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; + return (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0); +} diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 0478fd3e..5befe279 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -23,6 +23,7 @@ class UITheme { static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight); static UIIcon getFileIcon(std::string filename); static int getStatusBarHeight(); + static int getProgressBarHeight(); private: const ThemeMetrics* currentMetrics; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 1020117b..53d82a99 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -629,7 +629,8 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou } void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, - const int pageCount, std::string title, const int paddingBottom) const { + const int pageCount, std::string title, const int paddingBottom, + const int textYOffset) const { auto metrics = UITheme::getInstance().getMetrics(); int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, @@ -637,8 +638,7 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c // Draw Progress Text const auto screenHeight = renderer.getScreenHeight(); - const auto textY = - screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4; + auto textY = screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4; int progressTextWidth = 0; if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) { @@ -688,7 +688,8 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c } // Draw Title - if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) { + if (!title.empty()) { + textY -= textYOffset; // Centered chapter title text // Page width minus existing content with 30px padding on each side const int rendererableScreenWidth = diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index f11ce104..212f6e82 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -136,7 +136,8 @@ class BaseTheme { virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const; virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const; virtual void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, - const int pageCount, std::string title, const int paddingBottom = 0) const; + const int pageCount, std::string title, const int paddingBottom = 0, + const int textYOffset = 0) const; virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const;