Port PR #838 (epub cover fallback logic) and PR #907 (cover outlines): - Add fallback cover filename probing when EPUB metadata lacks cover info - Case-insensitive extension checking for cover images - Detect and re-generate corrupt/empty thumbnail BMPs - Always draw outline rect on cover tiles for legibility (PR #907) - Upgrade Storage.exists() checks to Epub::isValidThumbnailBmp() - Fallback chain: Real Cover → PlaceholderCoverGenerator → X-pattern marker - Add epub.load retry logic (cache-only first, then full build) - Adapt upstream Serial.printf calls to LOG_DBG/LOG_ERR macros Co-authored-by: Cursor <cursoragent@cursor.com>
1192 lines
49 KiB
C++
1192 lines
49 KiB
C++
#include "EpubReaderActivity.h"
|
|
|
|
#include <Epub/Page.h>
|
|
#include <FsHelpers.h>
|
|
#include <GfxRenderer.h>
|
|
#include <HalStorage.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 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::taskTrampoline(void* param) {
|
|
auto* self = static_cast<EpubReaderActivity*>(param);
|
|
self->displayTaskLoop();
|
|
}
|
|
|
|
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);
|
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
|
|
|
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
|
|
updateRequired = true;
|
|
|
|
xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
|
|
8192, // Stack size
|
|
this, // Parameters
|
|
1, // Priority
|
|
&displayTaskHandle // Task handle
|
|
);
|
|
}
|
|
|
|
void EpubReaderActivity::onExit() {
|
|
ActivityWithSubactivity::onExit();
|
|
|
|
// Reset orientation back to portrait for the rest of the UI
|
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
|
|
|
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
if (displayTaskHandle) {
|
|
vTaskDelete(displayTaskHandle);
|
|
displayTaskHandle = nullptr;
|
|
}
|
|
vSemaphoreDelete(renderingMutex);
|
|
renderingMutex = nullptr;
|
|
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();
|
|
updateRequired = true;
|
|
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;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Enter reader menu activity.
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
// Don't start activity transition while rendering
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
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, hasDictionary, isBookmarked, epub->getCachePath(),
|
|
[this](const uint8_t orientation) { onReaderMenuBack(orientation); },
|
|
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
|
|
// 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;
|
|
updateRequired = true;
|
|
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
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
nextPageNumber = 0;
|
|
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
|
section.reset();
|
|
xSemaphoreGive(renderingMutex);
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
// No current section, attempt to rerender the book
|
|
if (!section) {
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
if (prevTriggered) {
|
|
if (section->currentPage > 0) {
|
|
section->currentPage--;
|
|
} else {
|
|
// We don't want to delete the section mid-render, so grab the semaphore
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
nextPageNumber = UINT16_MAX;
|
|
currentSpineIndex--;
|
|
section.reset();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
updateRequired = true;
|
|
} else {
|
|
if (section->currentPage < section->pageCount - 1) {
|
|
section->currentPage++;
|
|
} else {
|
|
// We don't want to delete the section mid-render, so grab the semaphore
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
nextPageNumber = 0;
|
|
currentSpineIndex++;
|
|
section.reset();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
updateRequired = true;
|
|
}
|
|
}
|
|
|
|
void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) {
|
|
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);
|
|
// Force a half refresh on the next render to clear menu/popup artifacts
|
|
pagesUntilFullRefresh = 1;
|
|
updateRequired = 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 renderScreen() reloads and repositions on the target spine.
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
currentSpineIndex = targetSpineIndex;
|
|
nextPageNumber = 0;
|
|
pendingPercentJump = true;
|
|
section.reset();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
|
|
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);
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
GUI.drawPopup(renderer, "Bookmark added");
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
xSemaphoreGive(renderingMutex);
|
|
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;
|
|
updateRequired = true;
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::REMOVE_BOOKMARK: {
|
|
const int page = section ? section->currentPage : 0;
|
|
BookmarkStore::removeBookmark(epub->getCachePath(), currentSpineIndex, page);
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
GUI.drawPopup(renderer, "Bookmark removed");
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
xSemaphoreGive(renderingMutex);
|
|
vTaskDelay(750 / portTICK_PERIOD_MS);
|
|
exitActivity();
|
|
pagesUntilFullRefresh = 1;
|
|
updateRequired = true;
|
|
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) {
|
|
const int currentP = section ? section->currentPage : 0;
|
|
const int totalP = section ? section->pageCount : 0;
|
|
const int spineIdx = currentSpineIndex;
|
|
const std::string path = epub->getPath();
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
exitActivity();
|
|
enterNewActivity(new EpubReaderChapterSelectionActivity(
|
|
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
|
|
[this] {
|
|
exitActivity();
|
|
updateRequired = true;
|
|
},
|
|
[this](const int newSpineIndex) {
|
|
if (currentSpineIndex != newSpineIndex) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = 0;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
updateRequired = true;
|
|
},
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = newPage;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
updateRequired = true;
|
|
}));
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
// If no TOC either, just return to reader (menu already closed by callback)
|
|
break;
|
|
}
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
exitActivity();
|
|
enterNewActivity(new EpubReaderBookmarkSelectionActivity(
|
|
this->renderer, this->mappedInput, epub, std::move(bookmarks), epub->getCachePath(),
|
|
[this] {
|
|
exitActivity();
|
|
updateRequired = true;
|
|
},
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = newPage;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
updateRequired = true;
|
|
}));
|
|
xSemaphoreGive(renderingMutex);
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE: {
|
|
if (Dictionary::cacheExists()) {
|
|
Dictionary::deleteCache();
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
GUI.drawPopup(renderer, "Dictionary cache deleted");
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
xSemaphoreGive(renderingMutex);
|
|
} else {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
GUI.drawPopup(renderer, "No cache to delete");
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
vTaskDelay(1500 / portTICK_PERIOD_MS);
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
|
// Calculate values BEFORE we start destroying things
|
|
const int currentP = section ? section->currentPage : 0;
|
|
const int totalP = section ? section->pageCount : 0;
|
|
const int spineIdx = currentSpineIndex;
|
|
const std::string path = epub->getPath();
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
// 1. Close the menu
|
|
exitActivity();
|
|
|
|
// 2. Open the Chapter Selector
|
|
enterNewActivity(new EpubReaderChapterSelectionActivity(
|
|
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
|
|
[this] {
|
|
exitActivity();
|
|
updateRequired = true;
|
|
},
|
|
[this](const int newSpineIndex) {
|
|
if (currentSpineIndex != newSpineIndex) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = 0;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
updateRequired = true;
|
|
},
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
currentSpineIndex = newSpineIndex;
|
|
nextPageNumber = newPage;
|
|
section.reset();
|
|
}
|
|
exitActivity();
|
|
updateRequired = true;
|
|
}));
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
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));
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
exitActivity();
|
|
enterNewActivity(new EpubReaderPercentSelectionActivity(
|
|
renderer, mappedInput, initialPercent,
|
|
[this](const int percent) {
|
|
// Apply the new position and exit back to the reader.
|
|
jumpToPercent(percent);
|
|
exitActivity();
|
|
updateRequired = true;
|
|
},
|
|
[this]() {
|
|
// Cancel selection and return to the reader.
|
|
exitActivity();
|
|
updateRequired = true;
|
|
}));
|
|
xSemaphoreGive(renderingMutex);
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::LOOKUP: {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
// Compute margins (same logic as renderScreen)
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
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
|
|
auto pageForLookup = section ? section->loadPageFromSectionFile() : nullptr;
|
|
const int readerFontId = SETTINGS.getReaderFontId();
|
|
const std::string bookCachePath = epub->getCachePath();
|
|
const uint8_t currentOrientation = SETTINGS.orientation;
|
|
|
|
// Get first word of next page for cross-page hyphenation
|
|
std::string nextPageFirstWord;
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
exitActivity();
|
|
|
|
if (pageForLookup) {
|
|
enterNewActivity(new DictionaryWordSelectActivity(
|
|
renderer, mappedInput, std::move(pageForLookup), readerFontId, orientedMarginLeft, orientedMarginTop,
|
|
bookCachePath, currentOrientation, [this]() { pendingSubactivityExit = true; }, nextPageFirstWord));
|
|
}
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::LOOKED_UP_WORDS: {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
exitActivity();
|
|
enterNewActivity(new LookedUpWordsActivity(
|
|
renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation,
|
|
[this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; }));
|
|
xSemaphoreGive(renderingMutex);
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
|
// Defer go home to avoid race condition with display task
|
|
pendingGoHome = true;
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
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());
|
|
}
|
|
xSemaphoreGive(renderingMutex);
|
|
// Defer go home to avoid race condition with display task
|
|
pendingGoHome = true;
|
|
break;
|
|
}
|
|
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
|
if (KOREADER_STORE.hasCredentials()) {
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
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;
|
|
}));
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
break;
|
|
}
|
|
// Handled locally in the menu activity (cycle on Confirm, never dispatched here)
|
|
case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN:
|
|
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.
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
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();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
|
|
void EpubReaderActivity::displayTaskLoop() {
|
|
while (true) {
|
|
if (updateRequired) {
|
|
updateRequired = false;
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
renderScreen();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
// TODO: Failure handling
|
|
void EpubReaderActivity::renderScreen() {
|
|
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, "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, "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, "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, "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();
|
|
return renderScreen();
|
|
}
|
|
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.drawBattery(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 = "Unnamed";
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed");
|
|
} 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());
|
|
}
|
|
}
|