mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
## Summary * Truncating chapter titles using utf8 safe functions (Cyrillic titles were split mid codepoint) * refactoring of lib/Utf8 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_
584 lines
23 KiB
C++
584 lines
23 KiB
C++
#include "EpubReaderActivity.h"
|
|
|
|
#include <Epub/Page.h>
|
|
#include <FsHelpers.h>
|
|
#include <GfxRenderer.h>
|
|
#include <SDCardManager.h>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "CrossPointState.h"
|
|
#include "EpubReaderChapterSelectionActivity.h"
|
|
#include "MappedInputManager.h"
|
|
#include "RecentBooksStore.h"
|
|
#include "ScreenComponents.h"
|
|
#include "fontIds.h"
|
|
|
|
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;
|
|
|
|
} // 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
|
|
switch (SETTINGS.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;
|
|
}
|
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
|
|
|
epub->setupCacheDir();
|
|
|
|
FsFile f;
|
|
if (SdMan.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;
|
|
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), 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;
|
|
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
|
|
textSpineIndex);
|
|
}
|
|
}
|
|
|
|
// 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());
|
|
|
|
// 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;
|
|
section.reset();
|
|
epub.reset();
|
|
}
|
|
|
|
void EpubReaderActivity::loop() {
|
|
// Pass input responsibility to sub activity if exists
|
|
if (subActivity) {
|
|
subActivity->loop();
|
|
return;
|
|
}
|
|
|
|
// Enter chapter selection activity
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
// Don't start activity transition while rendering
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
const int currentPage = section ? section->currentPage : 0;
|
|
const int totalPages = section ? section->pageCount : 0;
|
|
exitActivity();
|
|
enterNewActivity(new EpubReaderMenuActivity(
|
|
this->renderer, this->mappedInput, epub->getTitle(), [this]() { onReaderMenuBack(); },
|
|
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
|
|
// Long press BACK (1s+) goes directly to home
|
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
|
onGoHome();
|
|
return;
|
|
}
|
|
|
|
// Short press BACK goes to file selection
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
|
onGoBack();
|
|
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() {
|
|
exitActivity();
|
|
updateRequired = true;
|
|
}
|
|
|
|
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
|
switch (action) {
|
|
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_HOME: {
|
|
// 2. Trigger the reader's "Go Home" callback
|
|
if (onGoHome) {
|
|
onGoHome();
|
|
}
|
|
|
|
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);
|
|
}
|
|
exitActivity();
|
|
updateRequired = true;
|
|
xSemaphoreGive(renderingMutex);
|
|
if (onGoHome) onGoHome();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// 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::FULL_WITH_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
|
|
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
|
|
(showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0);
|
|
}
|
|
|
|
if (!section) {
|
|
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
|
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), 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)) {
|
|
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
|
|
|
const auto popupFn = [this]() { ScreenComponents::drawPopup(renderer, "Indexing..."); };
|
|
|
|
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
|
viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) {
|
|
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
|
section.reset();
|
|
return;
|
|
}
|
|
} else {
|
|
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
renderer.clearScreen();
|
|
|
|
if (section->pageCount == 0) {
|
|
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
|
|
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) {
|
|
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), 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) {
|
|
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
|
|
section->clearCache();
|
|
section.reset();
|
|
return renderScreen();
|
|
}
|
|
const auto start = millis();
|
|
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
|
}
|
|
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
|
}
|
|
|
|
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
|
FsFile f;
|
|
if (SdMan.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();
|
|
Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, currentPage);
|
|
} else {
|
|
Serial.printf("[ERS] Could not save progress!\n");
|
|
}
|
|
}
|
|
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
|
const int orientedMarginRight, const int orientedMarginBottom,
|
|
const int orientedMarginLeft) {
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
|
if (pagesUntilFullRefresh <= 1) {
|
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
|
} else {
|
|
renderer.displayBuffer();
|
|
pagesUntilFullRefresh--;
|
|
}
|
|
|
|
// Save bw buffer to reset buffer state after grayscale data sync
|
|
renderer.storeBwBuffer();
|
|
|
|
// grayscale rendering
|
|
// TODO: Only do this if font supports it
|
|
if (SETTINGS.textAntiAliasing) {
|
|
renderer.clearScreen(0x00);
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderer.copyGrayscaleLsbBuffers();
|
|
|
|
// Render and copy to MSB buffer
|
|
renderer.clearScreen(0x00);
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
|
renderer.copyGrayscaleMsbBuffers();
|
|
|
|
// display grayscale part
|
|
renderer.displayGrayBuffer();
|
|
renderer.setRenderMode(GfxRenderer::BW);
|
|
}
|
|
|
|
// restore the bw data
|
|
renderer.restoreBwBuffer();
|
|
}
|
|
|
|
void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom,
|
|
const int orientedMarginLeft) const {
|
|
// determine visible status bar elements
|
|
const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
|
|
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
|
|
const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_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::FULL_WITH_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::FULL_WITH_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) {
|
|
// 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 {
|
|
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 (showProgressBar) {
|
|
// Draw progress bar at the very bottom of the screen, from edge to edge of viewable area
|
|
ScreenComponents::drawBookProgressBar(renderer, static_cast<size_t>(bookProgress));
|
|
}
|
|
|
|
if (showBattery) {
|
|
ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, 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());
|
|
}
|
|
}
|