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)**_
This commit is contained in:
GenesiaW
2026-02-28 03:42:41 +08:00
committed by GitHub
parent 09cef70709
commit 3b4f2a1129
14 changed files with 181 additions and 46 deletions

View File

@@ -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<int> 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<MenuResult>(result.data);
applyOrientation(menu.orientation);
toggleAutoPageTurn(menu.pageTurnOption);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(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<uint8_t>(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<uint8_t>(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<float>(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) {