Xteink-X4-crosspoint-reader/src/screens/EpubReaderScreen.cpp
2025-12-03 22:06:45 +11:00

210 lines
6.7 KiB
C++

#include "EpubReaderScreen.h"
#include <EpdRenderer.h>
#include <SD.h>
#include "Battery.h"
constexpr unsigned long SKIP_CHAPTER_MS = 700;
void EpubReaderScreen::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderScreen*>(param);
self->displayTaskLoop();
}
void EpubReaderScreen::onEnter() {
sectionMutex = xSemaphoreCreateMutex();
epub->setupCacheDir();
// TODO: Move this to a state object
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);
Serial.printf("Loaded cache: %d, %d\n", currentSpineIndex, nextPageNumber);
f.close();
}
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask",
8192, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void EpubReaderScreen::onExit() {
vTaskDelete(displayTaskHandle);
xSemaphoreTake(sectionMutex, portMAX_DELAY);
vSemaphoreDelete(sectionMutex);
sectionMutex = nullptr;
}
void EpubReaderScreen::handleInput(const Input input) {
if (input.button == VOLUME_UP || input.button == VOLUME_DOWN) {
const bool skipChapter = input.pressTime > SKIP_CHAPTER_MS;
// No current section, attempt to rerender the book
if (!section) {
updateRequired = true;
return;
}
if (input.button == VOLUME_UP && skipChapter) {
nextPageNumber = 0;
currentSpineIndex--;
delete section;
section = nullptr;
} else if (input.button == VOLUME_DOWN && skipChapter) {
nextPageNumber = 0;
currentSpineIndex++;
delete section;
section = nullptr;
} else if (input.button == VOLUME_UP) {
if (section->currentPage > 0) {
section->currentPage--;
} else {
xSemaphoreTake(sectionMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX;
currentSpineIndex--;
delete section;
section = nullptr;
xSemaphoreGive(sectionMutex);
}
} else if (input.button == VOLUME_DOWN) {
if (section->currentPage < section->pageCount - 1) {
section->currentPage++;
} else {
xSemaphoreTake(sectionMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex++;
delete section;
section = nullptr;
xSemaphoreGive(sectionMutex);
}
}
updateRequired = true;
}
}
void EpubReaderScreen::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
renderPage();
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderScreen::renderPage() {
if (!epub) {
return;
}
if (currentSpineIndex >= epub->getSpineItemsCount() || currentSpineIndex < 0) {
currentSpineIndex = 0;
}
xSemaphoreTake(sectionMutex, portMAX_DELAY);
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex);
Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex);
section = new Section(epub, currentSpineIndex, renderer);
if (!section->hasCache()) {
Serial.println("Cache not found, building...");
section->setupCacheDir();
if (!section->persistPageDataToSD()) {
Serial.println("Failed to persist page data to SD");
free(section);
section = nullptr;
xSemaphoreGive(sectionMutex);
return;
}
} else {
Serial.println("Cache found, skipping build...");
}
if (nextPageNumber == UINT16_MAX) {
section->currentPage = section->pageCount - 1;
} else {
section->currentPage = nextPageNumber;
}
}
renderer->clearScreen();
section->renderPage();
renderStatusBar();
renderer->flushDisplay();
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
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();
xSemaphoreGive(sectionMutex);
}
void EpubReaderScreen::renderStatusBar() const {
const auto pageWidth = renderer->getPageWidth();
std::string progress = std::to_string(currentPage + 1) + " / " + std::to_string(section->pageCount);
const auto progressTextWidth = renderer->getSmallTextWidth(progress.c_str());
renderer->drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str());
const uint16_t percentage = battery.readPercentage();
auto percentageText = std::to_string(percentage) + "%";
const auto percentageTextWidth = renderer->getSmallTextWidth(percentageText.c_str());
renderer->drawSmallText(20, 765, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int batteryWidth = 15;
constexpr int batteryHeight = 10;
const int x = 0;
const int y = 772;
// Top line
renderer->drawLine(x, y, x + batteryWidth - 4, y, 1);
// Bottom line
renderer->drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1, 1);
// Left line
renderer->drawLine(x, y, x, y + batteryHeight - 1, 1);
// Battery end
renderer->drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1, 1);
renderer->drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3, 1);
renderer->drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3, 1);
renderer->drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3, 1);
// The +1 is to round up, so that we always fill at least one pixel
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
if (filledWidth > batteryWidth - 5) {
filledWidth = batteryWidth - 5; // Ensure we don't overflow
}
renderer->fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2, 1);
// Page width minus existing content with 30px padding on each side
const int leftMargin = 20 + percentageTextWidth + 30;
const int rightMargin = progressTextWidth + 30;
const int availableTextWidth = pageWidth - leftMargin - rightMargin;
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
auto title = tocItem.title;
int titleWidth = renderer->getSmallTextWidth(title.c_str());
while (titleWidth > availableTextWidth) {
title = title.substr(0, title.length() - 8) + "...";
titleWidth = renderer->getSmallTextWidth(title.c_str());
}
renderer->drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str());
}