- Fix fp4 fixed-point misuse in PlaceholderCoverGenerator (advanceX is 12.4 fixed-point, not pixels) causing only first letter of each word to render - Remove duplicate silentIndexNextChapterIfNeeded() call from loop() that blocked UI before render, preventing the indexing indicator from showing - Fix indexing icon Y position to align within the status bar - Add ignoreNextConfirmRelease to EpubReaderChapterSelectionActivity so long-press confirm release doesn't immediately select the first TOC item - Reload recent books after cache deletion in HomeActivity and clear stale ignoreNextConfirmRelease flag to fix "no open books" and double-press bugs Made-with: Cursor
1269 lines
49 KiB
C++
1269 lines
49 KiB
C++
#include "EpubReaderActivity.h"
|
|
|
|
#include <Epub/Page.h>
|
|
#include <Epub/blocks/TextBlock.h>
|
|
#include <FsHelpers.h>
|
|
#include <PlaceholderCoverGenerator.h>
|
|
#include <GfxRenderer.h>
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
#include <Logging.h>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "CrossPointState.h"
|
|
#include "activities/ActivityManager.h"
|
|
#include "activities/home/BookManageMenuActivity.h"
|
|
#include "DictionaryMenuActivity.h"
|
|
#include "DictionaryWordSelectActivity.h"
|
|
#include "EndOfBookMenuActivity.h"
|
|
#include "EpubReaderBookmarkSelectionActivity.h"
|
|
#include "EpubReaderChapterSelectionActivity.h"
|
|
#include "EpubReaderFootnotesActivity.h"
|
|
#include "EpubReaderPercentSelectionActivity.h"
|
|
#include "KOReaderCredentialStore.h"
|
|
#include "KOReaderSyncActivity.h"
|
|
#include "LookedUpWordsActivity.h"
|
|
#include "MappedInputManager.h"
|
|
#include "QrDisplayActivity.h"
|
|
#include "RecentBooksStore.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
#include "util/BookManager.h"
|
|
#include "util/BookmarkStore.h"
|
|
#include "util/Dictionary.h"
|
|
#include "util/ScreenshotUtil.h"
|
|
|
|
namespace {
|
|
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
|
constexpr unsigned long skipChapterMs = 700;
|
|
constexpr unsigned long goHomeMs = 1000;
|
|
constexpr unsigned long longPressConfirmMs = 700;
|
|
// 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};
|
|
|
|
// 8x8 1-bit hourglass icon for the indexing status bar indicator.
|
|
constexpr uint8_t kIndexingIcon[] = {0x00, 0x81, 0xC3, 0xE7, 0xE7, 0xC3, 0x81, 0x00};
|
|
constexpr int kIndexingIconSize = 8;
|
|
|
|
int clampPercent(int percent) {
|
|
if (percent < 0) {
|
|
return 0;
|
|
}
|
|
if (percent > 100) {
|
|
return 100;
|
|
}
|
|
return percent;
|
|
}
|
|
|
|
// Apply the logical reader orientation to the renderer.
|
|
// This centralizes orientation mapping so we don't duplicate switch logic elsewhere.
|
|
void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
|
switch (orientation) {
|
|
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
|
break;
|
|
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
|
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
|
break;
|
|
case CrossPointSettings::ORIENTATION::INVERTED:
|
|
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
|
break;
|
|
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
|
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void EpubReaderActivity::onEnter() {
|
|
Activity::onEnter();
|
|
|
|
if (!epub) {
|
|
return;
|
|
}
|
|
|
|
// Configure screen orientation based on settings
|
|
// NOTE: This affects layout math and must be applied before any render calls.
|
|
applyReaderOrientation(renderer, SETTINGS.orientation);
|
|
|
|
epub->setupCacheDir();
|
|
|
|
FsFile f;
|
|
if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
|
uint8_t data[6];
|
|
int dataSize = f.read(data, 6);
|
|
if (dataSize == 4 || dataSize == 6) {
|
|
currentSpineIndex = data[0] + (data[1] << 8);
|
|
nextPageNumber = data[2] + (data[3] << 8);
|
|
cachedSpineIndex = currentSpineIndex;
|
|
LOG_DBG("ERS", "Loaded cache: %d, %d", currentSpineIndex, nextPageNumber);
|
|
}
|
|
if (dataSize == 6) {
|
|
cachedChapterTotalPageCount = data[4] + (data[5] << 8);
|
|
}
|
|
f.close();
|
|
}
|
|
// We may want a better condition to detect if we are opening for the first time.
|
|
// This will trigger if the book is re-opened at Chapter 0.
|
|
if (currentSpineIndex == 0) {
|
|
int textSpineIndex = epub->getSpineIndexForTextReference();
|
|
if (textSpineIndex != 0) {
|
|
currentSpineIndex = textSpineIndex;
|
|
LOG_DBG("ERS", "Opened for first time, navigating to text reference at index %d", textSpineIndex);
|
|
}
|
|
}
|
|
|
|
// Prerender covers and thumbnails on first open so Home and Sleep screens are instant.
|
|
{
|
|
int totalSteps = 0;
|
|
if (!Storage.exists(epub->getCoverBmpPath(false).c_str())) totalSteps++;
|
|
if (!Storage.exists(epub->getCoverBmpPath(true).c_str())) totalSteps++;
|
|
for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) {
|
|
if (!Storage.exists(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]).c_str())) totalSteps++;
|
|
}
|
|
|
|
if (totalSteps > 0) {
|
|
Rect popupRect = GUI.drawPopup(renderer, "Preparing book...");
|
|
int completedSteps = 0;
|
|
|
|
auto updateProgress = [&]() {
|
|
completedSteps++;
|
|
GUI.fillPopupProgress(renderer, popupRect, completedSteps * 100 / totalSteps);
|
|
};
|
|
|
|
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) {
|
|
epub->generateCoverBmp(false);
|
|
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) {
|
|
if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(false), epub->getTitle(), epub->getAuthor(),
|
|
480, 800)) {
|
|
epub->generateInvalidFormatCoverBmp(false);
|
|
}
|
|
}
|
|
updateProgress();
|
|
}
|
|
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) {
|
|
epub->generateCoverBmp(true);
|
|
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) {
|
|
if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(true), epub->getTitle(), epub->getAuthor(),
|
|
480, 800)) {
|
|
epub->generateInvalidFormatCoverBmp(true);
|
|
}
|
|
}
|
|
updateProgress();
|
|
}
|
|
for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) {
|
|
if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) {
|
|
epub->generateThumbBmp(PRERENDER_THUMB_HEIGHTS[i]);
|
|
if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) {
|
|
const int thumbHeight = PRERENDER_THUMB_HEIGHTS[i];
|
|
const int thumbWidth = static_cast<int>(thumbHeight * 0.6);
|
|
if (!PlaceholderCoverGenerator::generate(epub->getThumbBmpPath(thumbHeight), epub->getTitle(),
|
|
epub->getAuthor(), thumbWidth, thumbHeight)) {
|
|
epub->generateInvalidFormatThumbBmp(thumbHeight);
|
|
}
|
|
}
|
|
updateProgress();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save current epub as last opened epub and add to recent books
|
|
APP_STATE.openEpubPath = epub->getPath();
|
|
APP_STATE.saveToFile();
|
|
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
|
|
|
|
// Trigger first update
|
|
requestUpdate();
|
|
}
|
|
|
|
void EpubReaderActivity::onExit() {
|
|
Activity::onExit();
|
|
|
|
// Reset orientation back to portrait for the rest of the UI
|
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
|
|
|
APP_STATE.readerActivityLoadCount = 0;
|
|
APP_STATE.saveToFile();
|
|
section.reset();
|
|
epub.reset();
|
|
}
|
|
|
|
void EpubReaderActivity::loop() {
|
|
if (!epub) {
|
|
// Should never happen
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
if (pendingEndOfBookMenu) {
|
|
pendingEndOfBookMenu = false;
|
|
endOfBookMenuOpened = true;
|
|
startActivityForResult(
|
|
std::make_unique<EndOfBookMenuActivity>(renderer, mappedInput, epub->getPath()),
|
|
[this](const ActivityResult& result) {
|
|
if (result.isCancelled) {
|
|
return;
|
|
}
|
|
const auto action = static_cast<EndOfBookMenuActivity::Action>(std::get<MenuResult>(result.data).action);
|
|
switch (action) {
|
|
case EndOfBookMenuActivity::Action::ARCHIVE:
|
|
BookManager::archiveBook(epub->getPath());
|
|
activityManager.goHome();
|
|
return;
|
|
case EndOfBookMenuActivity::Action::DELETE:
|
|
BookManager::deleteBook(epub->getPath());
|
|
activityManager.goHome();
|
|
return;
|
|
case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS: {
|
|
endOfBookMenuOpened = false;
|
|
RenderLock lock(*this);
|
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
|
nextPageNumber = UINT16_MAX;
|
|
section.reset();
|
|
break;
|
|
}
|
|
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: {
|
|
endOfBookMenuOpened = false;
|
|
RenderLock lock(*this);
|
|
currentSpineIndex = 0;
|
|
nextPageNumber = 0;
|
|
section.reset();
|
|
break;
|
|
}
|
|
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
|
|
activityManager.goHome();
|
|
return;
|
|
case EndOfBookMenuActivity::Action::CLOSE_MENU:
|
|
endOfBookMenuOpened = false;
|
|
break;
|
|
}
|
|
requestUpdate();
|
|
});
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Long press CONFIRM opens Table of Contents directly (skip menu)
|
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= longPressConfirmMs) {
|
|
ignoreNextConfirmRelease = true;
|
|
if (epub && epub->getTocItemsCount() > 0) {
|
|
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::TABLE_OF_CONTENTS);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Short press CONFIRM opens reader menu
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
if (ignoreNextConfirmRelease) {
|
|
ignoreNextConfirmRelease = false;
|
|
return;
|
|
}
|
|
const int currentPage = section ? section->currentPage + 1 : 0;
|
|
const int totalPages = section ? section->pageCount : 0;
|
|
float bookProgress = 0.0f;
|
|
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
|
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
|
}
|
|
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
|
const bool isBookmarked =
|
|
section ? BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage) : false;
|
|
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
|
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
|
SETTINGS.orientation, !currentPageFootnotes.empty(), isBookmarked, SETTINGS.fontSize,
|
|
epub->getCachePath()),
|
|
[this](const ActivityResult& result) {
|
|
// Always apply orientation and font size even if the menu was cancelled
|
|
const auto& menu = std::get<MenuResult>(result.data);
|
|
applyOrientation(menu.orientation);
|
|
applyFontSize(menu.fontSize);
|
|
toggleAutoPageTurn(menu.pageTurnOption);
|
|
if (!result.isCancelled) {
|
|
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Long press BACK (1s+) goes to file selection
|
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
|
activityManager.goToFileBrowser(epub ? epub->getPath() : "");
|
|
return;
|
|
}
|
|
|
|
// Short press BACK goes directly to home (or restores position if viewing footnote)
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
|
if (footnoteDepth > 0) {
|
|
restoreSavedPosition();
|
|
return;
|
|
}
|
|
onGoHome();
|
|
return;
|
|
}
|
|
|
|
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
|
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
|
|
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Left))
|
|
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
|
mappedInput.wasReleased(MappedInputManager::Button::Left));
|
|
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
|
mappedInput.wasReleased(MappedInputManager::Button::Power);
|
|
const bool nextTriggered = usePressForPageTurn
|
|
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Right))
|
|
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
|
mappedInput.wasReleased(MappedInputManager::Button::Right));
|
|
|
|
if (!prevTriggered && !nextTriggered) {
|
|
return;
|
|
}
|
|
|
|
// any botton press when at end of the book goes back to the last page
|
|
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
|
nextPageNumber = UINT16_MAX;
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
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);
|
|
nextPageNumber = 0;
|
|
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
|
section.reset();
|
|
}
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
// No current section, attempt to rerender the book
|
|
if (!section) {
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
if (prevTriggered) {
|
|
pageTurn(false);
|
|
} else {
|
|
pageTurn(true);
|
|
}
|
|
}
|
|
|
|
// Translate an absolute percent into a spine index plus a normalized position
|
|
// within that spine so we can jump after the section is loaded.
|
|
void EpubReaderActivity::jumpToPercent(int percent) {
|
|
if (!epub) {
|
|
return;
|
|
}
|
|
|
|
const size_t bookSize = epub->getBookSize();
|
|
if (bookSize == 0) {
|
|
return;
|
|
}
|
|
|
|
// Normalize input to 0-100 to avoid invalid jumps.
|
|
percent = clampPercent(percent);
|
|
|
|
// Convert percent into a byte-like absolute position across the spine sizes.
|
|
// Use an overflow-safe computation: (bookSize / 100) * percent + (bookSize % 100) * percent / 100
|
|
size_t targetSize =
|
|
(bookSize / 100) * static_cast<size_t>(percent) + (bookSize % 100) * static_cast<size_t>(percent) / 100;
|
|
if (percent >= 100) {
|
|
// Ensure the final percent lands inside the last spine item.
|
|
targetSize = bookSize - 1;
|
|
}
|
|
|
|
const int spineCount = epub->getSpineItemsCount();
|
|
if (spineCount == 0) {
|
|
return;
|
|
}
|
|
|
|
int targetSpineIndex = spineCount - 1;
|
|
size_t prevCumulative = 0;
|
|
|
|
for (int i = 0; i < spineCount; i++) {
|
|
const size_t cumulative = epub->getCumulativeSpineItemSize(i);
|
|
if (targetSize <= cumulative) {
|
|
// Found the spine item containing the absolute position.
|
|
targetSpineIndex = i;
|
|
prevCumulative = (i > 0) ? epub->getCumulativeSpineItemSize(i - 1) : 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const size_t cumulative = epub->getCumulativeSpineItemSize(targetSpineIndex);
|
|
const size_t spineSize = (cumulative > prevCumulative) ? (cumulative - prevCumulative) : 0;
|
|
// Store a normalized position within the spine so it can be applied once loaded.
|
|
pendingSpineProgress =
|
|
(spineSize == 0) ? 0.0f : static_cast<float>(targetSize - prevCumulative) / static_cast<float>(spineSize);
|
|
if (pendingSpineProgress < 0.0f) {
|
|
pendingSpineProgress = 0.0f;
|
|
} else if (pendingSpineProgress > 1.0f) {
|
|
pendingSpineProgress = 1.0f;
|
|
}
|
|
|
|
// Reset state so render() reloads and repositions on the target spine.
|
|
{
|
|
RenderLock lock(*this);
|
|
currentSpineIndex = targetSpineIndex;
|
|
nextPageNumber = 0;
|
|
pendingPercentJump = true;
|
|
section.reset();
|
|
}
|
|
}
|
|
|
|
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
|
switch (action) {
|
|
case EpubReaderMenuActivity::MenuAction::TABLE_OF_CONTENTS:
|
|
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
|
const int spineIdx = currentSpineIndex;
|
|
const std::string path = epub->getPath();
|
|
const bool consumeRelease = ignoreNextConfirmRelease;
|
|
startActivityForResult(
|
|
std::make_unique<EpubReaderChapterSelectionActivity>(renderer, mappedInput, epub, path, spineIdx,
|
|
consumeRelease),
|
|
[this](const ActivityResult& result) {
|
|
if (!result.isCancelled && currentSpineIndex != std::get<ChapterResult>(result.data).spineIndex) {
|
|
RenderLock lock(*this);
|
|
currentSpineIndex = std::get<ChapterResult>(result.data).spineIndex;
|
|
nextPageNumber = 0;
|
|
section.reset();
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::FOOTNOTES: {
|
|
startActivityForResult(std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
|
|
[this](const ActivityResult& result) {
|
|
if (!result.isCancelled) {
|
|
const auto& footnoteResult = std::get<FootnoteResult>(result.data);
|
|
navigateToHref(footnoteResult.href, true);
|
|
}
|
|
requestUpdate();
|
|
});
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: {
|
|
float bookProgress = 0.0f;
|
|
if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
|
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
|
}
|
|
const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
|
startActivityForResult(
|
|
std::make_unique<EpubReaderPercentSelectionActivity>(renderer, mappedInput, initialPercent),
|
|
[this](const ActivityResult& result) {
|
|
if (!result.isCancelled) {
|
|
jumpToPercent(std::get<PercentResult>(result.data).percent);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: {
|
|
if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) {
|
|
auto p = section->loadPageFromSectionFile();
|
|
if (p) {
|
|
std::string fullText;
|
|
for (const auto& el : p->elements) {
|
|
if (el->getTag() == TAG_PageLine) {
|
|
const auto& line = static_cast<const PageLine&>(*el);
|
|
if (line.getBlock()) {
|
|
const auto& words = line.getBlock()->getWords();
|
|
for (const auto& w : words) {
|
|
if (!fullText.empty()) fullText += " ";
|
|
fullText += w;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!fullText.empty()) {
|
|
startActivityForResult(std::make_unique<QrDisplayActivity>(renderer, mappedInput, fullText),
|
|
[this](const ActivityResult& result) {});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// If no text or page loading failed, just close menu
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
|
onGoHome();
|
|
return;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
|
|
{
|
|
RenderLock lock(*this);
|
|
if (epub && section) {
|
|
uint16_t backupSpine = currentSpineIndex;
|
|
uint16_t backupPage = section->currentPage;
|
|
uint16_t backupPageCount = section->pageCount;
|
|
section.reset();
|
|
epub->clearCache();
|
|
epub->setupCacheDir();
|
|
saveProgress(backupSpine, backupPage, backupPageCount);
|
|
}
|
|
}
|
|
onGoHome();
|
|
return;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::SCREENSHOT: {
|
|
{
|
|
RenderLock lock(*this);
|
|
pendingScreenshot = true;
|
|
}
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
|
if (KOREADER_STORE.hasCredentials()) {
|
|
const int currentPage = section ? section->currentPage : 0;
|
|
const int totalPages = section ? section->pageCount : 0;
|
|
startActivityForResult(
|
|
std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex,
|
|
currentPage, totalPages),
|
|
[this](const ActivityResult& result) {
|
|
if (!result.isCancelled) {
|
|
const auto& sync = std::get<SyncResult>(result.data);
|
|
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
|
|
RenderLock lock(*this);
|
|
currentSpineIndex = sync.spineIndex;
|
|
nextPageNumber = sync.page;
|
|
section.reset();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::PUSH_AND_SLEEP: {
|
|
const int cp = section ? section->currentPage : 0;
|
|
const int tp = section ? section->pageCount : 0;
|
|
if (KOREADER_STORE.hasCredentials()) {
|
|
startActivityForResult(
|
|
std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, cp,
|
|
tp, KOReaderSyncActivity::SyncMode::PUSH_ONLY),
|
|
[](const ActivityResult&) { activityManager.requestSleep(); });
|
|
} else {
|
|
activityManager.requestSleep();
|
|
}
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::CLOSE_BOOK:
|
|
onGoHome();
|
|
return;
|
|
case EpubReaderMenuActivity::MenuAction::ADD_BOOKMARK: {
|
|
if (section && BookmarkStore::addBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage, "")) {
|
|
requestUpdate();
|
|
}
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::REMOVE_BOOKMARK: {
|
|
if (section && BookmarkStore::removeBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage)) {
|
|
requestUpdate();
|
|
}
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::GO_TO_BOOKMARK: {
|
|
auto bookmarks = BookmarkStore::load(epub->getCachePath());
|
|
startActivityForResult(
|
|
std::make_unique<EpubReaderBookmarkSelectionActivity>(renderer, mappedInput, epub, std::move(bookmarks),
|
|
epub->getCachePath()),
|
|
[this](const ActivityResult& result) {
|
|
if (!result.isCancelled) {
|
|
const auto& sync = std::get<SyncResult>(result.data);
|
|
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
|
|
RenderLock lock(*this);
|
|
currentSpineIndex = sync.spineIndex;
|
|
nextPageNumber = sync.page;
|
|
section.reset();
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::LOOKUP_WORD: {
|
|
if (!section || !Dictionary::cacheExists()) {
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
auto p = section->loadPageFromSectionFile();
|
|
if (!p) {
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
|
&orientedMarginLeft);
|
|
orientedMarginTop += SETTINGS.screenMargin;
|
|
orientedMarginLeft += SETTINGS.screenMargin;
|
|
startActivityForResult(
|
|
std::make_unique<DictionaryWordSelectActivity>(
|
|
renderer, mappedInput, std::move(p), SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop,
|
|
epub->getCachePath(), SETTINGS.orientation, ""),
|
|
[this](const ActivityResult&) { requestUpdate(); });
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::LOOKUP_HISTORY: {
|
|
if (!Dictionary::cacheExists()) {
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
startActivityForResult(
|
|
std::make_unique<LookedUpWordsActivity>(renderer, mappedInput, epub->getCachePath(),
|
|
SETTINGS.getReaderFontId(), SETTINGS.orientation),
|
|
[this](const ActivityResult&) { requestUpdate(); });
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE: {
|
|
if (Dictionary::cacheExists()) {
|
|
Dictionary::deleteCache();
|
|
}
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::ARCHIVE_BOOK: {
|
|
if (epub) BookManager::archiveBook(epub->getPath());
|
|
activityManager.goHome();
|
|
return;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_BOOK: {
|
|
if (epub) BookManager::deleteBook(epub->getPath());
|
|
activityManager.goHome();
|
|
return;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK: {
|
|
if (epub) BookManager::reindexBook(epub->getPath(), false);
|
|
activityManager.goHome();
|
|
return;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::MANAGE_BOOK: {
|
|
if (!epub) break;
|
|
const bool isArchived = BookManager::isArchived(epub->getPath());
|
|
startActivityForResult(
|
|
std::make_unique<BookManageMenuActivity>(renderer, mappedInput, epub->getPath(), isArchived),
|
|
[this](ActivityResult result) {
|
|
if (result.isCancelled) {
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
const auto& menu = std::get<MenuResult>(result.data);
|
|
const auto bookAction = static_cast<BookManageMenuActivity::Action>(menu.action);
|
|
switch (bookAction) {
|
|
case BookManageMenuActivity::Action::ARCHIVE:
|
|
BookManager::archiveBook(epub->getPath());
|
|
activityManager.goHome();
|
|
return;
|
|
case BookManageMenuActivity::Action::UNARCHIVE:
|
|
BookManager::unarchiveBook(epub->getPath());
|
|
activityManager.goHome();
|
|
return;
|
|
case BookManageMenuActivity::Action::DELETE:
|
|
BookManager::deleteBook(epub->getPath());
|
|
activityManager.goHome();
|
|
return;
|
|
case BookManageMenuActivity::Action::DELETE_CACHE: {
|
|
RenderLock lock(*this);
|
|
if (section) {
|
|
uint16_t backupSpine = currentSpineIndex;
|
|
uint16_t backupPage = section->currentPage;
|
|
uint16_t backupPageCount = section->pageCount;
|
|
section.reset();
|
|
epub->clearCache();
|
|
epub->setupCacheDir();
|
|
saveProgress(backupSpine, backupPage, backupPageCount);
|
|
}
|
|
activityManager.goHome();
|
|
return;
|
|
}
|
|
case BookManageMenuActivity::Action::REINDEX:
|
|
BookManager::reindexBook(epub->getPath(), false);
|
|
activityManager.goHome();
|
|
return;
|
|
case BookManageMenuActivity::Action::REINDEX_FULL:
|
|
BookManager::reindexBook(epub->getPath(), true);
|
|
activityManager.goHome();
|
|
return;
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DICTIONARY: {
|
|
startActivityForResult(
|
|
std::make_unique<DictionaryMenuActivity>(renderer, mappedInput),
|
|
[this](ActivityResult result) {
|
|
if (result.isCancelled) {
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
const auto& menu = std::get<MenuResult>(result.data);
|
|
const auto dictAction = static_cast<DictionaryMenuActivity::Action>(menu.action);
|
|
switch (dictAction) {
|
|
case DictionaryMenuActivity::Action::LOOKUP_WORD:
|
|
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::LOOKUP_WORD);
|
|
return;
|
|
case DictionaryMenuActivity::Action::LOOKUP_HISTORY:
|
|
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::LOOKUP_HISTORY);
|
|
return;
|
|
case DictionaryMenuActivity::Action::DELETE_DICT_CACHE:
|
|
onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE);
|
|
return;
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
|
// No-op if the selected orientation matches current settings.
|
|
if (SETTINGS.orientation == orientation) {
|
|
return;
|
|
}
|
|
|
|
// Preserve current reading position so we can restore after reflow.
|
|
{
|
|
RenderLock lock(*this);
|
|
if (section) {
|
|
cachedSpineIndex = currentSpineIndex;
|
|
cachedChapterTotalPageCount = section->pageCount;
|
|
nextPageNumber = section->currentPage;
|
|
}
|
|
|
|
// Persist the selection so the reader keeps the new orientation on next launch.
|
|
SETTINGS.orientation = orientation;
|
|
SETTINGS.saveToFile();
|
|
|
|
// Update renderer orientation to match the new logical coordinate system.
|
|
applyReaderOrientation(renderer, SETTINGS.orientation);
|
|
|
|
// Reset section to force re-layout in the new orientation.
|
|
section.reset();
|
|
}
|
|
}
|
|
|
|
void EpubReaderActivity::applyFontSize(const uint8_t fontSize) {
|
|
if (fontSize >= CrossPointSettings::FONT_SIZE_COUNT || SETTINGS.fontSize == fontSize) {
|
|
return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
if (section) {
|
|
cachedSpineIndex = currentSpineIndex;
|
|
cachedChapterTotalPageCount = section->pageCount;
|
|
nextPageNumber = section->currentPage;
|
|
}
|
|
|
|
SETTINGS.fontSize = fontSize;
|
|
SETTINGS.saveToFile();
|
|
section.reset();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
bool EpubReaderActivity::silentIndexNextChapterIfNeeded() {
|
|
if (!epub || !section) return false;
|
|
if (preIndexedNextSpine == currentSpineIndex + 1) return false;
|
|
|
|
const bool nearEnd = (section->pageCount == 1 && section->currentPage == 0) ||
|
|
(section->pageCount >= 2 && section->currentPage == section->pageCount - 2);
|
|
if (!nearEnd) return false;
|
|
|
|
const int nextSpine = currentSpineIndex + 1;
|
|
if (nextSpine >= epub->getSpineItemsCount()) return false;
|
|
|
|
int marginTop, marginRight, marginBottom, marginLeft;
|
|
renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft);
|
|
marginTop += SETTINGS.screenMargin;
|
|
marginLeft += SETTINGS.screenMargin;
|
|
marginRight += SETTINGS.screenMargin;
|
|
marginBottom += std::max(static_cast<int>(SETTINGS.screenMargin), UITheme::getInstance().getStatusBarHeight());
|
|
const uint16_t vpWidth = renderer.getScreenWidth() - marginLeft - marginRight;
|
|
const uint16_t vpHeight = renderer.getScreenHeight() - marginTop - marginBottom;
|
|
|
|
Section nextSection(epub, nextSpine, renderer);
|
|
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, vpWidth, vpHeight,
|
|
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
|
|
preIndexedNextSpine = nextSpine;
|
|
return false;
|
|
}
|
|
|
|
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpine);
|
|
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, vpWidth, vpHeight,
|
|
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
|
|
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpine);
|
|
return false;
|
|
}
|
|
preIndexedNextSpine = nextSpine;
|
|
return true;
|
|
}
|
|
|
|
// TODO: Failure handling
|
|
void EpubReaderActivity::render(RenderLock&& lock) {
|
|
if (!epub) {
|
|
return;
|
|
}
|
|
|
|
// edge case handling for sub-zero spine index
|
|
if (currentSpineIndex < 0) {
|
|
currentSpineIndex = 0;
|
|
}
|
|
// based bounds of book, show end of book screen
|
|
if (currentSpineIndex > epub->getSpineItemsCount()) {
|
|
currentSpineIndex = epub->getSpineItemsCount();
|
|
}
|
|
|
|
// Show end of book screen (defer menu launch to loop() to avoid deadlock)
|
|
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
|
renderer.clearScreen();
|
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
|
renderer.displayBuffer();
|
|
automaticPageTurnActive = false;
|
|
if (!endOfBookMenuOpened) {
|
|
pendingEndOfBookMenu = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Apply screen viewable areas and additional padding
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
|
&orientedMarginLeft);
|
|
orientedMarginTop += SETTINGS.screenMargin;
|
|
orientedMarginLeft += SETTINGS.screenMargin;
|
|
orientedMarginRight += SETTINGS.screenMargin;
|
|
|
|
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;
|
|
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
|
|
|
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
|
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
|
|
|
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
|
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
|
SETTINGS.imageRendering)) {
|
|
LOG_DBG("ERS", "Cache not found, building...");
|
|
|
|
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
|
|
|
|
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
|
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
|
SETTINGS.imageRendering, popupFn)) {
|
|
LOG_ERR("ERS", "Failed to persist page data to SD");
|
|
section.reset();
|
|
return;
|
|
}
|
|
} else {
|
|
LOG_DBG("ERS", "Cache found, skipping build...");
|
|
}
|
|
|
|
if (nextPageNumber == UINT16_MAX) {
|
|
section->currentPage = section->pageCount - 1;
|
|
} else {
|
|
section->currentPage = nextPageNumber;
|
|
}
|
|
|
|
if (!pendingAnchor.empty()) {
|
|
if (const auto page = section->getPageForAnchor(pendingAnchor)) {
|
|
section->currentPage = *page;
|
|
LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page);
|
|
} else {
|
|
LOG_DBG("ERS", "Anchor '%s' not found in section %d", pendingAnchor.c_str(), currentSpineIndex);
|
|
}
|
|
pendingAnchor.clear();
|
|
}
|
|
|
|
// handles changes in reader settings and reset to approximate position based on cached progress
|
|
if (cachedChapterTotalPageCount > 0) {
|
|
// only goes to relative position if spine index matches cached value
|
|
if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) {
|
|
float progress = static_cast<float>(section->currentPage) / static_cast<float>(cachedChapterTotalPageCount);
|
|
int newPage = static_cast<int>(progress * section->pageCount);
|
|
section->currentPage = newPage;
|
|
}
|
|
cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again
|
|
}
|
|
|
|
if (pendingPercentJump && section->pageCount > 0) {
|
|
// Apply the pending percent jump now that we know the new section's page count.
|
|
int newPage = static_cast<int>(pendingSpineProgress * static_cast<float>(section->pageCount));
|
|
if (newPage >= section->pageCount) {
|
|
newPage = section->pageCount - 1;
|
|
}
|
|
section->currentPage = newPage;
|
|
pendingPercentJump = false;
|
|
}
|
|
}
|
|
|
|
renderer.clearScreen();
|
|
|
|
if (section->pageCount == 0) {
|
|
LOG_DBG("ERS", "No pages to render");
|
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD);
|
|
renderStatusBar();
|
|
renderer.displayBuffer();
|
|
automaticPageTurnActive = false;
|
|
return;
|
|
}
|
|
|
|
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
|
LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount);
|
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD);
|
|
renderStatusBar();
|
|
renderer.displayBuffer();
|
|
automaticPageTurnActive = false;
|
|
return;
|
|
}
|
|
|
|
{
|
|
auto p = section->loadPageFromSectionFile();
|
|
if (!p) {
|
|
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
|
|
section->clearCache();
|
|
section.reset();
|
|
silentIndexingActive = false;
|
|
requestUpdate();
|
|
automaticPageTurnActive = false;
|
|
return;
|
|
}
|
|
|
|
silentIndexingActive = false;
|
|
|
|
// Collect footnotes from the loaded page
|
|
currentPageFootnotes = std::move(p->footnotes);
|
|
|
|
const bool textOnlyPage = !p->hasImages();
|
|
if (textOnlyPage && SETTINGS.indexingDisplay != CrossPointSettings::INDEXING_DISPLAY::INDEXING_POPUP &&
|
|
section->pageCount >= 1 &&
|
|
((section->pageCount == 1 && section->currentPage == 0) ||
|
|
(section->pageCount >= 2 && section->currentPage == section->pageCount - 2)) &&
|
|
currentSpineIndex + 1 < epub->getSpineItemsCount() && preIndexedNextSpine != currentSpineIndex + 1) {
|
|
const uint16_t vpW = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
|
const uint16_t vpH = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
|
Section probe(epub, currentSpineIndex + 1, renderer);
|
|
if (probe.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, vpW, vpH,
|
|
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
|
|
preIndexedNextSpine = currentSpineIndex + 1;
|
|
} else {
|
|
silentIndexingActive = true;
|
|
}
|
|
}
|
|
|
|
const auto start = millis();
|
|
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
|
renderer.clearFontCache();
|
|
|
|
if (silentIndexingActive) {
|
|
silentIndexNextChapterIfNeeded();
|
|
requestUpdate();
|
|
}
|
|
}
|
|
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
|
|
|
if (pendingScreenshot) {
|
|
pendingScreenshot = false;
|
|
ScreenshotUtil::takeScreenshot(renderer);
|
|
}
|
|
}
|
|
|
|
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
|
FsFile f;
|
|
if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
|
uint8_t data[6];
|
|
data[0] = currentSpineIndex & 0xFF;
|
|
data[1] = (currentSpineIndex >> 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();
|
|
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage);
|
|
} else {
|
|
LOG_ERR("ERS", "Could not save progress!");
|
|
}
|
|
}
|
|
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
|
const int orientedMarginRight, const int orientedMarginBottom,
|
|
const int orientedMarginLeft) {
|
|
// Force special handling for pages with images when anti-aliasing is on
|
|
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
|
|
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderStatusBar();
|
|
if (imagePageWithAA) {
|
|
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
|
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
|
|
// Instead, blank only the image area and do two fast refreshes.
|
|
// Step 1: Display page with image area blanked (text appears, image area white)
|
|
// Step 2: Re-render with images and display again (images appear clean)
|
|
int16_t imgX, imgY, imgW, imgH;
|
|
if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) {
|
|
renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false);
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
|
|
// Re-render page content to restore images into the blanked area
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderStatusBar();
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
} else {
|
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
}
|
|
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
|
|
} else if (pagesUntilFullRefresh <= 1) {
|
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
|
} else {
|
|
renderer.displayBuffer();
|
|
pagesUntilFullRefresh--;
|
|
}
|
|
|
|
// Save bw buffer to reset buffer state after grayscale data sync
|
|
renderer.storeBwBuffer();
|
|
|
|
// grayscale rendering
|
|
// TODO: Only do this if font supports it
|
|
if (SETTINGS.textAntiAliasing) {
|
|
renderer.clearScreen(0x00);
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderer.copyGrayscaleLsbBuffers();
|
|
|
|
// Render and copy to MSB buffer
|
|
renderer.clearScreen(0x00);
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderer.copyGrayscaleMsbBuffers();
|
|
|
|
// display grayscale part
|
|
renderer.displayGrayBuffer();
|
|
renderer.setRenderMode(GfxRenderer::BW);
|
|
}
|
|
|
|
// restore the bw data
|
|
renderer.restoreBwBuffer();
|
|
}
|
|
|
|
void EpubReaderActivity::renderStatusBar() const {
|
|
// Calculate progress in book
|
|
const int currentPage = section->currentPage + 1;
|
|
const float pageCount = section->pageCount;
|
|
const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
|
|
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
|
|
|
|
std::string title;
|
|
|
|
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();
|
|
}
|
|
|
|
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset);
|
|
|
|
if (silentIndexingActive && SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
|
&orientedMarginLeft);
|
|
const int textY = renderer.getScreenHeight() - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - 4;
|
|
const bool showBatteryPercentage =
|
|
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
|
|
const int batteryWidth = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
|
const int indicatorX = orientedMarginLeft + batteryWidth + 8;
|
|
if (SETTINGS.indexingDisplay == CrossPointSettings::INDEXING_DISPLAY::INDEXING_STATUS_TEXT) {
|
|
renderer.drawText(SMALL_FONT_ID, indicatorX, textY, tr(STR_INDEXING));
|
|
} else if (SETTINGS.indexingDisplay == CrossPointSettings::INDEXING_DISPLAY::INDEXING_STATUS_ICON) {
|
|
renderer.drawIcon(kIndexingIcon, indicatorX, textY + 2, kIndexingIconSize, kIndexingIconSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
|
|
if (!epub) return;
|
|
|
|
// Push current position onto saved stack
|
|
if (savePosition && section && footnoteDepth < MAX_FOOTNOTE_DEPTH) {
|
|
savedPositions[footnoteDepth] = {currentSpineIndex, section->currentPage};
|
|
footnoteDepth++;
|
|
LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage);
|
|
}
|
|
|
|
// Extract fragment anchor (e.g. "#note1" or "chapter2.xhtml#note1")
|
|
std::string anchor;
|
|
const auto hashPos = hrefStr.find('#');
|
|
if (hashPos != std::string::npos && hashPos + 1 < hrefStr.size()) {
|
|
anchor = hrefStr.substr(hashPos + 1);
|
|
}
|
|
|
|
// Check for same-file anchor reference (#anchor only)
|
|
bool sameFile = !hrefStr.empty() && hrefStr[0] == '#';
|
|
|
|
int targetSpineIndex;
|
|
if (sameFile) {
|
|
targetSpineIndex = currentSpineIndex;
|
|
} else {
|
|
targetSpineIndex = epub->resolveHrefToSpineIndex(hrefStr);
|
|
}
|
|
|
|
if (targetSpineIndex < 0) {
|
|
LOG_DBG("ERS", "Could not resolve href: %s", hrefStr.c_str());
|
|
if (savePosition && footnoteDepth > 0) footnoteDepth--; // undo push
|
|
return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
pendingAnchor = std::move(anchor);
|
|
currentSpineIndex = targetSpineIndex;
|
|
nextPageNumber = 0;
|
|
section.reset();
|
|
}
|
|
requestUpdate();
|
|
LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, hrefStr.c_str());
|
|
}
|
|
|
|
void EpubReaderActivity::restoreSavedPosition() {
|
|
if (footnoteDepth <= 0) return;
|
|
footnoteDepth--;
|
|
const auto& pos = savedPositions[footnoteDepth];
|
|
LOG_DBG("ERS", "Restoring position [%d]: spine %d, page %d", footnoteDepth, pos.spineIndex, pos.pageNumber);
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
currentSpineIndex = pos.spineIndex;
|
|
nextPageNumber = pos.pageNumber;
|
|
section.reset();
|
|
}
|
|
requestUpdate();
|
|
}
|