2025-12-03 22:00:29 +11:00
|
|
|
#include "EpubReaderScreen.h"
|
|
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
#include "EpubReaderFootnotesScreen.h"
|
2025-12-08 19:48:49 +11:00
|
|
|
#include <Epub/Page.h>
|
2025-12-08 22:06:09 +11:00
|
|
|
#include <GfxRenderer.h>
|
2025-12-03 22:00:29 +11:00
|
|
|
#include <SD.h>
|
|
|
|
|
|
|
|
|
|
#include "Battery.h"
|
2025-12-15 13:16:46 +01:00
|
|
|
#include "CrossPointSettings.h"
|
2025-12-13 21:17:34 +11:00
|
|
|
#include "EpubReaderChapterSelectionScreen.h"
|
2025-12-17 02:06:38 +01:00
|
|
|
#include "EpubReaderMenuScreen.h"
|
2025-12-08 22:06:09 +11:00
|
|
|
#include "config.h"
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 19:48:49 +11:00
|
|
|
constexpr int PAGES_PER_REFRESH = 15;
|
2025-12-03 22:00:29 +11:00
|
|
|
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
2025-12-08 22:06:09 +11:00
|
|
|
constexpr float lineCompression = 0.95f;
|
2025-12-13 00:16:10 +11:00
|
|
|
constexpr int marginTop = 8;
|
2025-12-08 22:06:09 +11:00
|
|
|
constexpr int marginRight = 10;
|
2025-12-13 00:16:10 +11:00
|
|
|
constexpr int marginBottom = 22;
|
2025-12-08 22:06:09 +11:00
|
|
|
constexpr int marginLeft = 10;
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
void EpubReaderScreen::taskTrampoline(void* param) {
|
|
|
|
|
auto* self = static_cast<EpubReaderScreen*>(param);
|
|
|
|
|
self->displayTaskLoop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderScreen::onEnter() {
|
2025-12-06 02:49:10 +11:00
|
|
|
if (!epub) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 03:02:52 +11:00
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
2025-12-03 22:00:29 +11:00
|
|
|
epub->setupCacheDir();
|
|
|
|
|
|
|
|
|
|
if (SD.exists((epub->getCachePath() + "/progress.bin").c_str())) {
|
|
|
|
|
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str());
|
|
|
|
|
uint8_t data[4];
|
|
|
|
|
f.read(data, 4);
|
|
|
|
|
currentSpineIndex = data[0] + (data[1] << 8);
|
|
|
|
|
nextPageNumber = data[2] + (data[3] << 8);
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber);
|
2025-12-03 22:00:29 +11:00
|
|
|
f.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trigger first update
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
|
|
|
|
|
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask",
|
2025-12-17 02:06:38 +01:00
|
|
|
24576, //32768
|
|
|
|
|
this,
|
|
|
|
|
1,
|
|
|
|
|
&displayTaskHandle
|
2025-12-03 22:00:29 +11:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderScreen::onExit() {
|
2025-12-06 03:02:52 +11:00
|
|
|
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2025-12-06 02:49:10 +11:00
|
|
|
if (displayTaskHandle) {
|
|
|
|
|
vTaskDelete(displayTaskHandle);
|
|
|
|
|
displayTaskHandle = nullptr;
|
|
|
|
|
}
|
2025-12-06 03:02:52 +11:00
|
|
|
vSemaphoreDelete(renderingMutex);
|
|
|
|
|
renderingMutex = nullptr;
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
|
|
|
|
epub.reset();
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 12:35:41 +11:00
|
|
|
void EpubReaderScreen::handleInput() {
|
2025-12-13 21:17:34 +11:00
|
|
|
// Pass input responsibility to sub screen if exists
|
|
|
|
|
if (subScreen) {
|
|
|
|
|
subScreen->handleInput();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
// Enter Menu selection screen
|
|
|
|
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
|
|
|
|
if (isViewingFootnote) {
|
|
|
|
|
restoreSavedPosition();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
onGoHome();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-13 21:17:34 +11:00
|
|
|
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
|
|
|
|
// Don't start screen transition while rendering
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2025-12-17 02:06:38 +01:00
|
|
|
|
|
|
|
|
subScreen.reset(new EpubReaderMenuScreen(
|
|
|
|
|
this->renderer, this->inputManager,
|
2025-12-13 21:17:34 +11:00
|
|
|
[this] {
|
2025-12-17 02:06:38 +01:00
|
|
|
// onGoBack - return to reading
|
2025-12-13 21:17:34 +11:00
|
|
|
subScreen->onExit();
|
|
|
|
|
subScreen.reset();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
2025-12-17 02:06:38 +01:00
|
|
|
[this](EpubReaderMenuScreen::MenuOption option) {
|
|
|
|
|
// onSelectOption - handle menu choice
|
|
|
|
|
if (option == EpubReaderMenuScreen::CHAPTERS) {
|
|
|
|
|
// Show chapter selection
|
|
|
|
|
subScreen->onExit();
|
|
|
|
|
subScreen.reset(new EpubReaderChapterSelectionScreen(
|
|
|
|
|
this->renderer, this->inputManager, epub, currentSpineIndex,
|
|
|
|
|
[this] {
|
|
|
|
|
// onGoBack from chapter selection
|
|
|
|
|
subScreen->onExit();
|
|
|
|
|
subScreen.reset();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this](const int newSpineIndex) {
|
|
|
|
|
// onSelectSpineIndex
|
|
|
|
|
if (currentSpineIndex != newSpineIndex) {
|
|
|
|
|
currentSpineIndex = newSpineIndex;
|
|
|
|
|
nextPageNumber = 0;
|
|
|
|
|
section.reset();
|
|
|
|
|
}
|
|
|
|
|
subScreen->onExit();
|
|
|
|
|
subScreen.reset();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}));
|
|
|
|
|
subScreen->onEnter();
|
|
|
|
|
} else if (option == EpubReaderMenuScreen::FOOTNOTES) {
|
|
|
|
|
// Show footnotes page with current page notes
|
|
|
|
|
subScreen->onExit();
|
|
|
|
|
|
|
|
|
|
subScreen.reset(new EpubReaderFootnotesScreen(
|
|
|
|
|
this->renderer,
|
|
|
|
|
this->inputManager,
|
|
|
|
|
currentPageFootnotes, // Pass collected footnotes (reference)
|
|
|
|
|
[this] {
|
|
|
|
|
// onGoBack from footnotes
|
|
|
|
|
subScreen->onExit();
|
|
|
|
|
subScreen.reset();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this](const char* href) {
|
|
|
|
|
// onSelectFootnote - navigate to the footnote location
|
|
|
|
|
navigateToHref(href, true); // true = save current position
|
|
|
|
|
subScreen->onExit();
|
|
|
|
|
subScreen.reset();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}));
|
|
|
|
|
subScreen->onEnter();
|
2025-12-13 21:17:34 +11:00
|
|
|
}
|
|
|
|
|
}));
|
2025-12-17 02:06:38 +01:00
|
|
|
|
2025-12-13 21:17:34 +11:00
|
|
|
subScreen->onEnter();
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 12:35:41 +11:00
|
|
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
|
|
|
|
onGoHome();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-06 12:35:41 +11:00
|
|
|
const bool prevReleased =
|
|
|
|
|
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
|
|
|
|
|
const bool nextReleased =
|
|
|
|
|
inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
|
|
|
|
|
|
|
|
|
|
if (!prevReleased && !nextReleased) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
// any button press when at end of the book goes back to the last page
|
2025-12-13 20:10:38 +11:00
|
|
|
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
|
|
|
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
|
|
|
|
nextPageNumber = UINT16_MAX;
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 12:35:41 +11:00
|
|
|
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
|
|
|
|
|
|
|
|
|
|
if (skipChapter) {
|
|
|
|
|
// We don't want to delete the section mid-render, so grab the semaphore
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
nextPageNumber = 0;
|
|
|
|
|
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-06 12:35:41 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No current section, attempt to rerender the book
|
|
|
|
|
if (!section) {
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (prevReleased) {
|
|
|
|
|
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;
|
2025-12-03 22:00:29 +11:00
|
|
|
currentSpineIndex--;
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-06 12:35:41 +11:00
|
|
|
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);
|
2025-12-03 22:00:29 +11:00
|
|
|
nextPageNumber = 0;
|
|
|
|
|
currentSpineIndex++;
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-06 12:35:41 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderScreen::displayTaskLoop() {
|
|
|
|
|
while (true) {
|
|
|
|
|
if (updateRequired) {
|
|
|
|
|
updateRequired = false;
|
2025-12-06 03:02:52 +11:00
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderScreen();
|
2025-12-06 03:02:52 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 19:48:49 +11:00
|
|
|
void EpubReaderScreen::renderScreen() {
|
2025-12-03 22:00:29 +11:00
|
|
|
if (!epub) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
// Edge case handling for sub-zero spine index
|
2025-12-13 20:10:38 +11:00
|
|
|
if (currentSpineIndex < 0) {
|
2025-12-03 22:00:29 +11:00
|
|
|
currentSpineIndex = 0;
|
|
|
|
|
}
|
2025-12-17 02:06:38 +01:00
|
|
|
// Based bounds of book, show end of book screen
|
2025-12-13 20:10:38 +11:00
|
|
|
if (currentSpineIndex > epub->getSpineItemsCount()) {
|
|
|
|
|
currentSpineIndex = epub->getSpineItemsCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show end of book screen
|
|
|
|
|
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
|
|
|
|
renderer.clearScreen();
|
|
|
|
|
renderer.drawCenteredText(READER_FONT_ID, 300, "End of book", true, BOLD);
|
|
|
|
|
renderer.displayBuffer();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
if (!section) {
|
|
|
|
|
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
2025-12-12 22:13:34 +11:00
|
|
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
2025-12-15 23:17:23 +11:00
|
|
|
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
|
|
|
|
SETTINGS.extraParagraphSpacing)) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
2025-12-05 21:12:15 +11:00
|
|
|
|
|
|
|
|
{
|
2025-12-17 00:17:49 +11:00
|
|
|
renderer.grayscaleRevert();
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing...");
|
2025-12-05 21:12:15 +11:00
|
|
|
constexpr int margin = 20;
|
2025-12-17 00:17:49 +11:00
|
|
|
// Round all coordinates to 8 pixel boundaries
|
|
|
|
|
const int x = ((GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2 + 7) / 8 * 8;
|
|
|
|
|
constexpr int y = 56;
|
|
|
|
|
const int w = (textWidth + margin * 2 + 7) / 8 * 8;
|
|
|
|
|
const int h = (renderer.getLineHeight(READER_FONT_ID) + margin * 2 + 7) / 8 * 8;
|
|
|
|
|
renderer.fillRect(x, y, w, h, false);
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
2025-12-17 00:17:49 +11:00
|
|
|
// EXPERIMENTAL: Still suffers from ghosting
|
|
|
|
|
renderer.displayWindow(x, y, w, h);
|
2025-12-08 19:48:49 +11:00
|
|
|
pagesUntilFullRefresh = 0;
|
2025-12-05 21:12:15 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
section->setupCacheDir();
|
2025-12-08 22:06:09 +11:00
|
|
|
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
2025-12-15 13:16:46 +01:00
|
|
|
marginLeft, SETTINGS.extraParagraphSpacing)) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-03 22:00:29 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextPageNumber == UINT16_MAX) {
|
|
|
|
|
section->currentPage = section->pageCount - 1;
|
|
|
|
|
} else {
|
|
|
|
|
section->currentPage = nextPageNumber;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.clearScreen();
|
2025-12-08 19:48:49 +11:00
|
|
|
|
|
|
|
|
if (section->pageCount == 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
|
2025-12-08 22:52:19 +11:00
|
|
|
renderer.drawCenteredText(READER_FONT_ID, 300, "Empty chapter", true, BOLD);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderStatusBar();
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.displayBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
return;
|
2025-12-05 22:19:44 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 19:48:49 +11:00
|
|
|
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
|
2025-12-08 22:52:19 +11:00
|
|
|
renderer.drawCenteredText(READER_FONT_ID, 300, "Out of bounds", true, BOLD);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderStatusBar();
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.displayBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
// Load page from SD - use pointer to avoid copying on stack
|
|
|
|
|
std::unique_ptr<Page> p = section->loadPageFromSD();
|
|
|
|
|
if (!p) {
|
|
|
|
|
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
|
|
|
|
|
section->clearCache();
|
|
|
|
|
section.reset();
|
|
|
|
|
return renderScreen();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy footnotes from page to currentPageFootnotes
|
|
|
|
|
currentPageFootnotes.clear();
|
|
|
|
|
for (int i = 0; i < p->footnoteCount && i < 16; i++) {
|
|
|
|
|
FootnoteEntry* footnote = p->getFootnote(i);
|
|
|
|
|
if (footnote) {
|
|
|
|
|
currentPageFootnotes.addFootnote(footnote->number, footnote->href);
|
2025-12-12 22:13:34 +11:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 02:06:38 +01:00
|
|
|
Serial.printf("[%lu] [ERS] Loaded %d footnotes for current page\n", millis(), p->footnoteCount);
|
2025-12-08 19:48:49 +11:00
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
const auto start = millis();
|
|
|
|
|
renderContents(std::move(p));
|
|
|
|
|
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
|
|
|
|
|
|
|
|
|
// Save progress
|
2025-12-03 22:00:29 +11:00
|
|
|
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
2025-12-17 02:06:38 +01:00
|
|
|
if (f) {
|
|
|
|
|
uint8_t data[4];
|
|
|
|
|
data[0] = currentSpineIndex & 0xFF;
|
|
|
|
|
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
|
|
|
|
data[2] = section->currentPage & 0xFF;
|
|
|
|
|
data[3] = (section->currentPage >> 8) & 0xFF;
|
|
|
|
|
f.write(data, 4);
|
|
|
|
|
f.close();
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-12 22:13:34 +11:00
|
|
|
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
|
|
|
|
|
page->render(renderer, READER_FONT_ID);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderStatusBar();
|
|
|
|
|
if (pagesUntilFullRefresh <= 1) {
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
2025-12-08 19:48:49 +11:00
|
|
|
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
|
|
|
|
} else {
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.displayBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
pagesUntilFullRefresh--;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 00:17:49 +11:00
|
|
|
// Save bw buffer to reset buffer state after grayscale data sync
|
|
|
|
|
renderer.storeBwBuffer();
|
|
|
|
|
|
2025-12-08 19:48:49 +11:00
|
|
|
// grayscale rendering
|
2025-12-08 22:06:09 +11:00
|
|
|
// TODO: Only do this if font supports it
|
2025-12-08 19:48:49 +11:00
|
|
|
{
|
|
|
|
|
renderer.clearScreen(0x00);
|
2025-12-16 02:16:35 +11:00
|
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
2025-12-12 22:13:34 +11:00
|
|
|
page->render(renderer, READER_FONT_ID);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderer.copyGrayscaleLsbBuffers();
|
|
|
|
|
|
|
|
|
|
// Render and copy to MSB buffer
|
|
|
|
|
renderer.clearScreen(0x00);
|
2025-12-16 02:16:35 +11:00
|
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
2025-12-12 22:13:34 +11:00
|
|
|
page->render(renderer, READER_FONT_ID);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderer.copyGrayscaleMsbBuffers();
|
|
|
|
|
|
|
|
|
|
// display grayscale part
|
|
|
|
|
renderer.displayGrayBuffer();
|
2025-12-16 02:16:35 +11:00
|
|
|
renderer.setRenderMode(GfxRenderer::BW);
|
2025-12-08 19:48:49 +11:00
|
|
|
}
|
2025-12-17 00:17:49 +11:00
|
|
|
|
|
|
|
|
// restore the bw data
|
|
|
|
|
renderer.restoreBwBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
void EpubReaderScreen::renderStatusBar() const {
|
2025-12-13 00:16:10 +11:00
|
|
|
constexpr auto textY = 776;
|
2025-12-08 22:06:09 +11:00
|
|
|
// Right aligned text for progress counter
|
2025-12-17 02:06:38 +01:00
|
|
|
char progressBuf[32]; // Use fixed buffer instead of std::string
|
|
|
|
|
snprintf(progressBuf, sizeof(progressBuf), "%d / %d", section->currentPage + 1, section->pageCount);
|
|
|
|
|
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressBuf);
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY, progressBuf);
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
// Left aligned battery icon and percentage
|
2025-12-03 22:00:29 +11:00
|
|
|
const uint16_t percentage = battery.readPercentage();
|
2025-12-17 02:06:38 +01:00
|
|
|
char percentageBuf[8]; // Use fixed buffer instead of std::string
|
|
|
|
|
snprintf(percentageBuf, sizeof(percentageBuf), "%d%%", percentage);
|
|
|
|
|
const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageBuf);
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, textY, percentageBuf);
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
// Battery icon drawing
|
2025-12-03 22:00:29 +11:00
|
|
|
constexpr int batteryWidth = 15;
|
|
|
|
|
constexpr int batteryHeight = 10;
|
2025-12-08 22:06:09 +11:00
|
|
|
constexpr int x = marginLeft;
|
|
|
|
|
constexpr int y = 783;
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
// Top line
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
2025-12-03 22:00:29 +11:00
|
|
|
// Bottom line
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1);
|
2025-12-03 22:00:29 +11:00
|
|
|
// Left line
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.drawLine(x, y, x, y + batteryHeight - 1);
|
2025-12-03 22:00:29 +11:00
|
|
|
// Battery end
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
|
2025-12-13 00:16:10 +11:00
|
|
|
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
|
|
|
|
|
renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
// Fill battery based on percentage
|
2025-12-03 22:00:29 +11:00
|
|
|
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
|
|
|
|
|
if (filledWidth > batteryWidth - 5) {
|
2025-12-17 02:06:38 +01:00
|
|
|
filledWidth = batteryWidth - 5;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
// Centered chapter title text
|
2025-12-08 22:06:09 +11:00
|
|
|
const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft;
|
|
|
|
|
const int titleMarginRight = progressTextWidth + 30 + marginRight;
|
|
|
|
|
const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight;
|
2025-12-13 21:17:22 +11:00
|
|
|
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
|
|
|
|
|
|
|
|
|
if (tocIndex == -1) {
|
2025-12-17 02:06:38 +01:00
|
|
|
const char* title = "Unnamed";
|
|
|
|
|
const int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title);
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title);
|
2025-12-13 21:17:22 +11:00
|
|
|
} else {
|
2025-12-17 02:06:38 +01:00
|
|
|
const auto& tocItem = epub->getTocItem(tocIndex);
|
|
|
|
|
std::string title = tocItem.title;
|
|
|
|
|
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
|
|
|
|
|
|
|
|
// Truncate title if too long
|
|
|
|
|
while (titleWidth > availableTextWidth && title.length() > 8) {
|
2025-12-13 21:17:22 +11:00
|
|
|
title = title.substr(0, title.length() - 8) + "...";
|
|
|
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
|
|
|
}
|
2025-12-17 02:06:38 +01:00
|
|
|
|
|
|
|
|
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderScreen::navigateToHref(const char* href, bool savePosition) {
|
|
|
|
|
if (!epub || !href) return;
|
|
|
|
|
|
|
|
|
|
// Save current position if requested
|
|
|
|
|
if (savePosition && section) {
|
|
|
|
|
savedSpineIndex = currentSpineIndex;
|
|
|
|
|
savedPageNumber = section->currentPage;
|
|
|
|
|
isViewingFootnote = true;
|
|
|
|
|
Serial.printf("[%lu] [ERS] Saved position: spine %d, page %d\n",
|
|
|
|
|
millis(), savedSpineIndex, savedPageNumber);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse href: "filename.html#anchor"
|
|
|
|
|
std::string hrefStr(href);
|
|
|
|
|
std::string filename;
|
|
|
|
|
std::string anchor;
|
|
|
|
|
|
|
|
|
|
size_t hashPos = hrefStr.find('#');
|
|
|
|
|
if (hashPos != std::string::npos) {
|
|
|
|
|
filename = hrefStr.substr(0, hashPos);
|
|
|
|
|
anchor = hrefStr.substr(hashPos + 1);
|
|
|
|
|
} else {
|
|
|
|
|
filename = hrefStr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract just filename without path
|
|
|
|
|
size_t lastSlash = filename.find_last_of('/');
|
|
|
|
|
if (lastSlash != std::string::npos) {
|
|
|
|
|
filename = filename.substr(lastSlash + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Serial.printf("[%lu] [ERS] Navigate to: %s (anchor: %s)\n",
|
|
|
|
|
millis(), filename.c_str(), anchor.c_str());
|
|
|
|
|
|
|
|
|
|
int targetSpineIndex = -1;
|
|
|
|
|
|
|
|
|
|
// FIRST: Check if we have an inline footnote for this anchor
|
|
|
|
|
if (!anchor.empty()) {
|
|
|
|
|
std::string inlineFilename = "inline_" + anchor + ".html";
|
|
|
|
|
Serial.printf("[%lu] [ERS] Looking for inline footnote: %s\n",
|
|
|
|
|
millis(), inlineFilename.c_str());
|
|
|
|
|
|
|
|
|
|
targetSpineIndex = epub->findVirtualSpineIndex(inlineFilename);
|
|
|
|
|
|
|
|
|
|
if (targetSpineIndex != -1) {
|
|
|
|
|
Serial.printf("[%lu] [ERS] Found inline footnote at index: %d\n",
|
|
|
|
|
millis(), targetSpineIndex);
|
|
|
|
|
|
|
|
|
|
// Navigate to inline footnote
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
currentSpineIndex = targetSpineIndex;
|
|
|
|
|
nextPageNumber = 0;
|
|
|
|
|
section.reset();
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
Serial.printf("[%lu] [ERS] No inline footnote found, trying normal navigation\n",
|
|
|
|
|
millis());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FALLBACK: Try to find the file in normal spine items
|
|
|
|
|
for (int i = 0; i < epub->getSpineItemsCount(); i++) {
|
|
|
|
|
if (epub->isVirtualSpineItem(i)) continue;
|
|
|
|
|
|
|
|
|
|
std::string spineItem = epub->getSpineItem(i);
|
|
|
|
|
size_t lastSlash = spineItem.find_last_of('/');
|
|
|
|
|
std::string spineFilename = (lastSlash != std::string::npos)
|
|
|
|
|
? spineItem.substr(lastSlash + 1)
|
|
|
|
|
: spineItem;
|
|
|
|
|
|
|
|
|
|
if (spineFilename == filename) {
|
|
|
|
|
targetSpineIndex = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 02:06:38 +01:00
|
|
|
if (targetSpineIndex == -1) {
|
|
|
|
|
Serial.printf("[%lu] [ERS] Could not find spine index for: %s\n",
|
|
|
|
|
millis(), filename.c_str());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Navigate to the target chapter
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
currentSpineIndex = targetSpineIndex;
|
|
|
|
|
nextPageNumber = 0;
|
|
|
|
|
section.reset();
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
|
|
|
|
|
Serial.printf("[%lu] [ERS] Navigated to spine index: %d\n",
|
|
|
|
|
millis(), targetSpineIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Method to restore saved position
|
|
|
|
|
void EpubReaderScreen::restoreSavedPosition() {
|
|
|
|
|
if (savedSpineIndex >= 0 && savedPageNumber >= 0) {
|
|
|
|
|
Serial.printf("[%lu] [ERS] Restoring position: spine %d, page %d\n",
|
|
|
|
|
millis(), savedSpineIndex, savedPageNumber);
|
|
|
|
|
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
currentSpineIndex = savedSpineIndex;
|
|
|
|
|
nextPageNumber = savedPageNumber;
|
|
|
|
|
section.reset();
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
|
|
|
|
|
savedSpineIndex = -1;
|
|
|
|
|
savedPageNumber = -1;
|
|
|
|
|
isViewingFootnote = false;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|