Skip the reader menu when long-pressing Confirm (700ms) to jump straight to the chapter selection screen. Short press behavior (opening the menu) is unchanged. Extracts shared openChapterSelection() helper to eliminate duplicated construction across three call sites. Co-authored-by: Cursor <cursoragent@cursor.com>
1155 lines
46 KiB
C++
1155 lines
46 KiB
C++
#include "EpubReaderActivity.h"
|
|
|
|
#include <Epub/Page.h>
|
|
#include <FsHelpers.h>
|
|
#include <GfxRenderer.h>
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
#include <Logging.h>
|
|
|
|
#include <PlaceholderCoverGenerator.h>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "CrossPointState.h"
|
|
#include "EpubReaderBookmarkSelectionActivity.h"
|
|
#include "EpubReaderChapterSelectionActivity.h"
|
|
#include "EpubReaderPercentSelectionActivity.h"
|
|
#include "KOReaderCredentialStore.h"
|
|
#include "KOReaderSyncActivity.h"
|
|
#include "MappedInputManager.h"
|
|
#include "RecentBooksStore.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
#include "util/BookmarkStore.h"
|
|
#include "util/Dictionary.h"
|
|
|
|
// Image refresh optimization strategy:
|
|
// 0 = Use double FAST_REFRESH technique (default, feels snappier)
|
|
// 1 = Use displayWindow() for partial refresh (experimental)
|
|
#define USE_IMAGE_DOUBLE_FAST_REFRESH 0
|
|
|
|
namespace {
|
|
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
|
constexpr unsigned long skipChapterMs = 700;
|
|
constexpr unsigned long goHomeMs = 1000;
|
|
constexpr unsigned long longPressConfirmMs = 700;
|
|
constexpr int statusBarMargin = 19;
|
|
constexpr int progressBarMarginTop = 1;
|
|
|
|
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() {
|
|
ActivityWithSubactivity::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.
|
|
// Each generate* call is a no-op if the file already exists, so this only does work once.
|
|
{
|
|
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);
|
|
// Fallback: generate placeholder if real cover extraction failed
|
|
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) {
|
|
if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(false), epub->getTitle(), epub->getAuthor(),
|
|
480, 800)) {
|
|
// Last resort: X-pattern marker
|
|
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)) {
|
|
// Last resort: X-pattern marker
|
|
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]);
|
|
// Fallback: generate placeholder thumbnail
|
|
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)) {
|
|
// Last resort: X-pattern marker
|
|
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() {
|
|
ActivityWithSubactivity::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() {
|
|
// Pass input responsibility to sub activity if exists
|
|
if (subActivity) {
|
|
subActivity->loop();
|
|
// Deferred exit: process after subActivity->loop() returns to avoid use-after-free
|
|
if (pendingSubactivityExit) {
|
|
pendingSubactivityExit = false;
|
|
exitActivity();
|
|
requestUpdate();
|
|
skipNextButtonCheck = true; // Skip button processing to ignore stale events
|
|
}
|
|
// Deferred go home: process after subActivity->loop() returns to avoid race condition
|
|
if (pendingGoHome) {
|
|
pendingGoHome = false;
|
|
exitActivity();
|
|
if (onGoHome) {
|
|
onGoHome();
|
|
}
|
|
return; // Don't access 'this' after callback
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle pending go home when no subactivity (e.g., from long press back)
|
|
if (pendingGoHome) {
|
|
pendingGoHome = false;
|
|
if (onGoHome) {
|
|
onGoHome();
|
|
}
|
|
return; // Don't access 'this' after callback
|
|
}
|
|
|
|
// Skip button processing after returning from subactivity
|
|
// This prevents stale button release events from triggering actions
|
|
// We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared
|
|
if (skipNextButtonCheck) {
|
|
const bool confirmCleared = !mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
|
!mappedInput.wasReleased(MappedInputManager::Button::Confirm);
|
|
const bool backCleared = !mappedInput.isPressed(MappedInputManager::Button::Back) &&
|
|
!mappedInput.wasReleased(MappedInputManager::Button::Back);
|
|
if (confirmCleared && backCleared) {
|
|
skipNextButtonCheck = false;
|
|
ignoreNextConfirmRelease = false;
|
|
}
|
|
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) {
|
|
openChapterSelection(true); // skip the stale release from this long-press
|
|
}
|
|
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 && 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 hasDictionary = Dictionary::exists();
|
|
const bool isBookmarked = BookmarkStore::hasBookmark(
|
|
epub->getCachePath(), currentSpineIndex, section ? section->currentPage : 0);
|
|
exitActivity();
|
|
enterNewActivity(new EpubReaderMenuActivity(
|
|
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
|
SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(),
|
|
[this](const uint8_t orientation, const uint8_t fontSize) { onReaderMenuBack(orientation, fontSize); },
|
|
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
|
}
|
|
|
|
// Long press BACK (1s+) goes to file selection
|
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
|
onGoBack();
|
|
return;
|
|
}
|
|
|
|
// Short press BACK goes directly to home
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
|
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) {
|
|
// 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) {
|
|
if (section->currentPage > 0) {
|
|
section->currentPage--;
|
|
} else {
|
|
// We don't want to delete the section mid-render, so grab the semaphore
|
|
{
|
|
RenderLock lock(*this);
|
|
nextPageNumber = UINT16_MAX;
|
|
currentSpineIndex--;
|
|
section.reset();
|
|
}
|
|
}
|
|
requestUpdate();
|
|
} 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();
|
|
}
|
|
}
|
|
|
|
void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation, const uint8_t fontSize) {
|
|
exitActivity();
|
|
// Apply the user-selected orientation when the menu is dismissed.
|
|
// This ensures the menu can be navigated without immediately rotating the screen.
|
|
applyOrientation(orientation);
|
|
// Apply font size change (no-op if unchanged).
|
|
applyFontSize(fontSize);
|
|
// Force a half refresh on the next render to clear menu/popup artifacts
|
|
pagesUntilFullRefresh = 1;
|
|
requestUpdate();
|
|
}
|
|
|
|
// 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::openChapterSelection(bool initialSkipRelease) {
|
|
const int currentP = section ? section->currentPage : 0;
|
|
const int totalP = section ? section->pageCount : 0;
|
|
const int spineIdx = currentSpineIndex;
|
|
const std::string path = epub->getPath();
|
|
|
|
enterNewActivity(new EpubReaderChapterSelectionActivity(
|
|
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
|
|
[this] {
|
|
exitActivity();
|
|
requestUpdate();
|
|
},
|
|
[this](const int newSpineIndex) {
|
|
if (currentSpineIndex != newSpineIndex) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = 0;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
requestUpdate();
|
|
},
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = newPage;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
requestUpdate();
|
|
},
|
|
initialSkipRelease));
|
|
}
|
|
|
|
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
|
switch (action) {
|
|
case EpubReaderMenuActivity::MenuAction::ADD_BOOKMARK: {
|
|
const int page = section ? section->currentPage : 0;
|
|
|
|
// Extract first full sentence from the current page for the bookmark snippet.
|
|
// If the first word is lowercase, the page starts mid-sentence — skip to the
|
|
// next sentence boundary and start collecting from there.
|
|
std::string snippet;
|
|
if (section) {
|
|
auto p = section->loadPageFromSectionFile();
|
|
if (p) {
|
|
// Gather all words on the page into a flat list for easier traversal
|
|
std::vector<std::string> allWords;
|
|
for (const auto& element : p->elements) {
|
|
const auto* line = static_cast<const PageLine*>(element.get());
|
|
if (!line) continue;
|
|
const auto& block = line->getBlock();
|
|
if (!block) continue;
|
|
for (const auto& word : block->getWords()) {
|
|
allWords.push_back(word);
|
|
}
|
|
}
|
|
|
|
if (!allWords.empty()) {
|
|
size_t startIdx = 0;
|
|
|
|
// Check if the first word starts with a lowercase letter (mid-sentence)
|
|
const char firstChar = allWords[0].empty() ? '\0' : allWords[0][0];
|
|
if (firstChar >= 'a' && firstChar <= 'z') {
|
|
// Skip past the end of this partial sentence
|
|
for (size_t i = 0; i < allWords.size(); i++) {
|
|
if (!allWords[i].empty()) {
|
|
char last = allWords[i].back();
|
|
if (last == '.' || last == '!' || last == '?' || last == ':') {
|
|
startIdx = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// If no sentence boundary found, fall back to using everything from the start
|
|
if (startIdx >= allWords.size()) {
|
|
startIdx = 0;
|
|
}
|
|
}
|
|
|
|
// Collect words from startIdx until the next sentence boundary
|
|
for (size_t i = startIdx; i < allWords.size(); i++) {
|
|
if (!snippet.empty()) snippet += " ";
|
|
snippet += allWords[i];
|
|
if (!allWords[i].empty()) {
|
|
char last = allWords[i].back();
|
|
if (last == '.' || last == '!' || last == '?' || last == ':') {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
BookmarkStore::addBookmark(epub->getCachePath(), currentSpineIndex, page, snippet);
|
|
{
|
|
RenderLock lock(*this);
|
|
GUI.drawPopup(renderer, tr(STR_BOOKMARK_ADDED));
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
}
|
|
vTaskDelay(750 / portTICK_PERIOD_MS);
|
|
// Exit the menu and return to reading — the bookmark indicator will show on re-render,
|
|
// and next menu open will reflect the updated state.
|
|
exitActivity();
|
|
pagesUntilFullRefresh = 1;
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::REMOVE_BOOKMARK: {
|
|
const int page = section ? section->currentPage : 0;
|
|
BookmarkStore::removeBookmark(epub->getCachePath(), currentSpineIndex, page);
|
|
{
|
|
RenderLock lock(*this);
|
|
GUI.drawPopup(renderer, tr(STR_BOOKMARK_REMOVED));
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
}
|
|
vTaskDelay(750 / portTICK_PERIOD_MS);
|
|
exitActivity();
|
|
pagesUntilFullRefresh = 1;
|
|
requestUpdate();
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::GO_TO_BOOKMARK: {
|
|
auto bookmarks = BookmarkStore::load(epub->getCachePath());
|
|
|
|
if (bookmarks.empty()) {
|
|
// No bookmarks: fall back to Table of Contents if available, otherwise go back
|
|
if (epub->getTocItemsCount() > 0) {
|
|
exitActivity();
|
|
openChapterSelection();
|
|
}
|
|
// If no TOC either, just return to reader (menu already closed by callback)
|
|
break;
|
|
}
|
|
|
|
exitActivity();
|
|
enterNewActivity(new EpubReaderBookmarkSelectionActivity(
|
|
this->renderer, this->mappedInput, epub, std::move(bookmarks), epub->getCachePath(),
|
|
[this] {
|
|
exitActivity();
|
|
requestUpdate();
|
|
},
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = newPage;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
requestUpdate();
|
|
}));
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
|
exitActivity();
|
|
openChapterSelection();
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: {
|
|
// Launch the slider-based percent selector and return here on confirm/cancel.
|
|
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));
|
|
exitActivity();
|
|
enterNewActivity(new EpubReaderPercentSelectionActivity(
|
|
renderer, mappedInput, initialPercent,
|
|
[this](const int percent) {
|
|
// Apply the new position and exit back to the reader.
|
|
jumpToPercent(percent);
|
|
exitActivity();
|
|
requestUpdate();
|
|
},
|
|
[this]() {
|
|
// Cancel selection and return to the reader.
|
|
exitActivity();
|
|
requestUpdate();
|
|
}));
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::LOOKUP: {
|
|
// Gather data we need while holding the render lock
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
std::unique_ptr<Page> pageForLookup;
|
|
int readerFontId;
|
|
std::string bookCachePath;
|
|
uint8_t currentOrientation;
|
|
std::string nextPageFirstWord;
|
|
{
|
|
RenderLock lock(*this);
|
|
|
|
// Compute margins (same logic as render)
|
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
|
&orientedMarginLeft);
|
|
orientedMarginTop += SETTINGS.screenMargin;
|
|
orientedMarginLeft += SETTINGS.screenMargin;
|
|
orientedMarginRight += SETTINGS.screenMargin;
|
|
orientedMarginBottom += SETTINGS.screenMargin;
|
|
|
|
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
const bool showProgressBar =
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
|
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
|
|
(showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0);
|
|
}
|
|
|
|
// Load the current page
|
|
pageForLookup = section ? section->loadPageFromSectionFile() : nullptr;
|
|
readerFontId = SETTINGS.getReaderFontId();
|
|
bookCachePath = epub->getCachePath();
|
|
currentOrientation = SETTINGS.orientation;
|
|
|
|
// Get first word of next page for cross-page hyphenation
|
|
if (section && section->currentPage < section->pageCount - 1) {
|
|
int savedPage = section->currentPage;
|
|
section->currentPage = savedPage + 1;
|
|
auto nextPage = section->loadPageFromSectionFile();
|
|
section->currentPage = savedPage;
|
|
if (nextPage && !nextPage->elements.empty()) {
|
|
const auto* firstLine = static_cast<const PageLine*>(nextPage->elements[0].get());
|
|
if (firstLine->getBlock() && !firstLine->getBlock()->getWords().empty()) {
|
|
nextPageFirstWord = firstLine->getBlock()->getWords().front();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Lock released — safe to call enterNewActivity which takes its own lock
|
|
exitActivity();
|
|
|
|
if (pageForLookup) {
|
|
enterNewActivity(new DictionaryWordSelectActivity(
|
|
renderer, mappedInput, std::move(pageForLookup), readerFontId, orientedMarginLeft, orientedMarginTop,
|
|
bookCachePath, currentOrientation, [this]() { pendingSubactivityExit = true; }, nextPageFirstWord));
|
|
}
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::LOOKED_UP_WORDS: {
|
|
exitActivity();
|
|
enterNewActivity(new LookedUpWordsActivity(
|
|
renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation,
|
|
[this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; },
|
|
true)); // initialSkipRelease: consumed the long-press that triggered this
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
|
// Defer go home to avoid race condition with display task
|
|
pendingGoHome = true;
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
|
|
{
|
|
RenderLock lock(*this);
|
|
if (epub) {
|
|
// 2. BACKUP: Read current progress
|
|
// We use the current variables that track our position
|
|
uint16_t backupSpine = currentSpineIndex;
|
|
uint16_t backupPage = section->currentPage;
|
|
uint16_t backupPageCount = section->pageCount;
|
|
|
|
section.reset();
|
|
// 3. WIPE: Clear the cache directory
|
|
epub->clearCache();
|
|
|
|
// 4. RESTORE: Re-setup the directory and rewrite the progress file
|
|
epub->setupCacheDir();
|
|
|
|
saveProgress(backupSpine, backupPage, backupPageCount);
|
|
|
|
// 5. Remove from recent books so the home screen doesn't show a stale/placeholder cover
|
|
RECENT_BOOKS.removeBook(epub->getPath());
|
|
}
|
|
}
|
|
// Defer go home to avoid race condition with display task
|
|
pendingGoHome = true;
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
|
if (KOREADER_STORE.hasCredentials()) {
|
|
const int currentPage = section ? section->currentPage : 0;
|
|
const int totalPages = section ? section->pageCount : 0;
|
|
exitActivity();
|
|
enterNewActivity(new KOReaderSyncActivity(
|
|
renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages,
|
|
[this]() {
|
|
// On cancel - defer exit to avoid use-after-free
|
|
pendingSubactivityExit = true;
|
|
},
|
|
[this](int newSpineIndex, int newPage) {
|
|
// On sync complete - update position and defer exit
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = newPage;
|
|
section.reset();
|
|
}
|
|
pendingSubactivityExit = true;
|
|
}));
|
|
}
|
|
break;
|
|
}
|
|
// Handled locally in the menu activity (cycle on Confirm, never dispatched here)
|
|
case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN:
|
|
case EpubReaderMenuActivity::MenuAction::TOGGLE_FONT_SIZE:
|
|
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 (SETTINGS.fontSize == fontSize) {
|
|
return;
|
|
}
|
|
|
|
// Preserve current reading position so we can restore after reflow.
|
|
{
|
|
RenderLock lock(*this);
|
|
if (section) {
|
|
cachedSpineIndex = currentSpineIndex;
|
|
cachedChapterTotalPageCount = section->pageCount;
|
|
nextPageNumber = section->currentPage;
|
|
}
|
|
|
|
SETTINGS.fontSize = fontSize;
|
|
SETTINGS.saveToFile();
|
|
|
|
// Reset section to force re-layout with the new font size.
|
|
section.reset();
|
|
}
|
|
}
|
|
|
|
// TODO: Failure handling
|
|
void EpubReaderActivity::render(Activity::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
|
|
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
|
renderer.clearScreen();
|
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
|
renderer.displayBuffer();
|
|
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;
|
|
orientedMarginBottom += SETTINGS.screenMargin;
|
|
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
|
|
// Add status bar margin
|
|
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
|
|
// Add additional margin for status bar if progress bar is shown
|
|
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
|
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
|
|
(showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0);
|
|
}
|
|
|
|
if (!section) {
|
|
loadingSection = true;
|
|
|
|
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)) {
|
|
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, popupFn)) {
|
|
LOG_ERR("ERS", "Failed to persist page data to SD");
|
|
section.reset();
|
|
loadingSection = false;
|
|
return;
|
|
}
|
|
} else {
|
|
LOG_DBG("ERS", "Cache found, skipping build...");
|
|
}
|
|
|
|
if (nextPageNumber == UINT16_MAX) {
|
|
section->currentPage = section->pageCount - 1;
|
|
} else {
|
|
section->currentPage = nextPageNumber;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
loadingSection = 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(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
renderer.displayBuffer();
|
|
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(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
renderer.displayBuffer();
|
|
return;
|
|
}
|
|
|
|
{
|
|
auto p = section->loadPageFromSectionFile();
|
|
if (!p) {
|
|
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
|
|
section->clearCache();
|
|
section.reset();
|
|
requestUpdate(); // Try again after clearing cache
|
|
// TODO: prevent infinite loop if the page keeps failing to load for some reason
|
|
return;
|
|
}
|
|
const auto start = millis();
|
|
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
|
}
|
|
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
|
}
|
|
|
|
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) {
|
|
// Determine if this page needs special image handling
|
|
bool pageHasImages = page->hasImages();
|
|
bool useAntiAliasing = SETTINGS.textAntiAliasing;
|
|
|
|
// Force half refresh for pages with images when anti-aliasing is on,
|
|
// as grayscale tones require half refresh to display correctly
|
|
bool forceFullRefresh = pageHasImages && useAntiAliasing;
|
|
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
|
|
// Draw bookmark ribbon indicator in top-right corner if current page is bookmarked
|
|
if (section && BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage)) {
|
|
const int screenWidth = renderer.getScreenWidth();
|
|
const int bkWidth = 12;
|
|
const int bkHeight = 22;
|
|
const int bkX = screenWidth - orientedMarginRight - bkWidth + 2;
|
|
const int bkY = 0;
|
|
const int notchDepth = bkHeight / 3;
|
|
const int centerX = bkX + bkWidth / 2;
|
|
|
|
const int xPoints[5] = {bkX, bkX + bkWidth, bkX + bkWidth, centerX, bkX};
|
|
const int yPoints[5] = {bkY, bkY, bkY + bkHeight, bkY + bkHeight - notchDepth, bkY + bkHeight};
|
|
renderer.fillPolygon(xPoints, yPoints, 5, true);
|
|
}
|
|
|
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
|
|
// Check if half-refresh is needed (either entering Reader or pages counter reached)
|
|
if (pagesUntilFullRefresh <= 1) {
|
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
|
} else if (forceFullRefresh) {
|
|
// OPTIMIZATION: For image pages with anti-aliasing, use fast double-refresh technique
|
|
// to reduce perceived lag. Only when pagesUntilFullRefresh > 1 (screen already clean).
|
|
int imgX, imgY, imgW, imgH;
|
|
if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) {
|
|
int screenX = imgX + orientedMarginLeft;
|
|
int screenY = imgY + orientedMarginTop;
|
|
LOG_DBG("ERS", "Image page: fast double-refresh (page bbox: %d,%d %dx%d, screen: %d,%d %dx%d)",
|
|
imgX, imgY, imgW, imgH, screenX, screenY, imgW, imgH);
|
|
|
|
#if USE_IMAGE_DOUBLE_FAST_REFRESH == 0
|
|
// Method A: Fill blank area + two FAST_REFRESH operations
|
|
renderer.fillRect(screenX, screenY, imgW, imgH, false);
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
#else
|
|
// Method B (experimental): Use displayWindow() for partial refresh
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
renderer.displayWindow(screenX, screenY, imgW, imgH, HalDisplay::FAST_REFRESH);
|
|
#endif
|
|
} else {
|
|
LOG_DBG("ERS", "Image page but no bbox, using standard half refresh");
|
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
}
|
|
pagesUntilFullRefresh--;
|
|
} else {
|
|
// Normal page without images, or images without anti-aliasing
|
|
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 int orientedMarginRight, const int orientedMarginBottom,
|
|
const int orientedMarginLeft) const {
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
|
|
// determine visible status bar elements
|
|
const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
|
|
const bool showBookProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR;
|
|
const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
|
const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR;
|
|
const bool showBookPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
|
const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
|
const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
|
const bool showBatteryPercentage =
|
|
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
|
|
|
|
// Position status bar near the bottom of the logical screen, regardless of orientation
|
|
const auto screenHeight = renderer.getScreenHeight();
|
|
const auto textY = screenHeight - orientedMarginBottom - 4;
|
|
int progressTextWidth = 0;
|
|
|
|
// Calculate progress in book
|
|
const float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
|
|
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
|
|
|
|
if (showProgressText || showProgressPercentage || showBookPercentage) {
|
|
// Right aligned text for progress counter
|
|
char progressStr[32];
|
|
|
|
// Hide percentage when progress bar is shown to reduce clutter
|
|
if (showProgressPercentage) {
|
|
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount,
|
|
bookProgress);
|
|
} else if (showBookPercentage) {
|
|
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
|
|
} else {
|
|
snprintf(progressStr, sizeof(progressStr), "%d/%d", section->currentPage + 1, section->pageCount);
|
|
}
|
|
|
|
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
|
|
renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY,
|
|
progressStr);
|
|
}
|
|
|
|
if (showBookProgressBar) {
|
|
// Draw progress bar at the very bottom of the screen, from edge to edge of viewable area
|
|
GUI.drawReadingProgressBar(renderer, static_cast<size_t>(bookProgress));
|
|
}
|
|
|
|
if (showChapterProgressBar) {
|
|
// Draw chapter progress bar at the very bottom of the screen, from edge to edge of viewable area
|
|
const float chapterProgress =
|
|
(section->pageCount > 0) ? (static_cast<float>(section->currentPage + 1) / section->pageCount) * 100 : 0;
|
|
GUI.drawReadingProgressBar(renderer, static_cast<size_t>(chapterProgress));
|
|
}
|
|
|
|
if (showBattery) {
|
|
GUI.drawBatteryLeft(renderer, Rect{orientedMarginLeft + 1, textY, metrics.batteryWidth, metrics.batteryHeight},
|
|
showBatteryPercentage);
|
|
}
|
|
|
|
if (showChapterTitle) {
|
|
// Centered chatper title text
|
|
// Page width minus existing content with 30px padding on each side
|
|
const int rendererableScreenWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
|
|
|
const int batterySize = showBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
|
const int titleMarginLeft = batterySize + 30;
|
|
const int titleMarginRight = progressTextWidth + 30;
|
|
|
|
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
|
|
// available space.
|
|
int titleMarginLeftAdjusted = std::max(titleMarginLeft, titleMarginRight);
|
|
int availableTitleSpace = rendererableScreenWidth - 2 * titleMarginLeftAdjusted;
|
|
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
|
|
|
std::string title;
|
|
int titleWidth;
|
|
if (tocIndex == -1) {
|
|
title = tr(STR_UNNAMED);
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
} else {
|
|
const auto tocItem = epub->getTocItem(tocIndex);
|
|
title = tocItem.title;
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
if (titleWidth > availableTitleSpace) {
|
|
// Not enough space to center on the screen, center it within the remaining space instead
|
|
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
|
titleMarginLeftAdjusted = titleMarginLeft;
|
|
}
|
|
if (titleWidth > availableTitleSpace) {
|
|
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
}
|
|
}
|
|
|
|
renderer.drawText(SMALL_FONT_ID,
|
|
titleMarginLeftAdjusted + orientedMarginLeft + (availableTitleSpace - titleWidth) / 2, textY,
|
|
title.c_str());
|
|
}
|
|
}
|