mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 06:37:38 +03:00
• What is the goal of this PR? Implement a horizontal EPUB reading mode so books can be read in landscape orientation (both 90° and 270°), while keeping the rest of the UI in portrait. • What changes are included? ◦ Rendering / Display ▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal, LandscapeFlipped) and made: ▪ drawPixel, drawImage, displayWindow map logical coordinates differently depending on orientation. ▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical dimensions (480×800 in portrait, 800×480 in landscape). ◦ Settings / Configuration ▪ Extended CrossPointSettings with: ▪ landscapeReading (toggle for portrait vs. landscape EPUB reading). ▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal holding directions are supported). ▪ Updated settings serialization/deserialization to persist these fields while remaining backward‑compatible with existing settings files. ▪ Updated SettingsActivity to expose two new toggles: ▪ “Landscape Reading” ▪ “Flip Landscape (swap top/bottom)” ◦ EPUB Reader ▪ In EpubReaderActivity: ▪ On onEnter, set GfxRenderer orientation based on the new settings (Portrait, LandscapeNormal, or LandscapeFlipped). ▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings, etc. continue to render as before. ▪ Adjusted renderStatusBar to position the status bar and battery indicator relative to GfxRenderer::getScreenHeight() instead of hard‑coded Y coordinates, so it stays correctly at the bottom in both portrait and landscape. ◦ EPUB Caching / Layout ▪ Extended Section cache metadata (section.bin) to include the logical screenWidth and screenHeight used when pages were generated; bumped SECTION_FILE_VERSION. ▪ Updated loadCacheMetadata to compare: ▪ font/margins/line compression/extraParagraphSpacing and screen dimensions; mismatches now invalidate and clear the cache. ▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so portrait and landscape caches are kept separate and correctly sized. Additional Context • Cache behavior / migration ◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected as incompatible and their caches cleared and rebuilt once per chapter when first opened after this change. ◦ Within a given orientation, caches will be reused as before. Switching orientation (portrait ↔ landscape) will cause a one‑time re‑index of each chapter in the new orientation. • Scope and risks ◦ Orientation changes are scoped to the EPUB reader; the Home screen, Settings, WiFi selection, sleep screens, and web server UI continue to assume portrait orientation. ◦ The renderer’s orientation is a static/global setting; if future code uses GfxRenderer outside the reader while a reader instance is active, it should be aware that orientation is no longer implicitly fixed. ◦ All drawing primitives now go through orientation‑aware coordinate transforms; any code that previously relied on edge‑case behavior or out‑of‑bounds writes might surface as logged “Outside range” warnings instead. • Testing suggestions / areas to focus on ◦ Verify in hardware: ▪ Portrait mode still renders correctly (boot, home, settings, WiFi, reader). ▪ Landscape reading in both directions: ▪ Landscape Reading = ON, Flip Landscape = OFF. ▪ Landscape Reading = ON, Flip Landscape = ON. ▪ Status bar (page X/Y, % progress, battery icon) is fully visible and aligned at the bottom in all three combinations. ◦ Open the same book: ▪ In portrait first, then switch to landscape and reopen it. ▪ Confirm that: ▪ Old portrait caches are rebuilt once for landscape (you should see the “Indexing…” page). ▪ Progress save/restore still works (resume opens to the correct page in the current orientation). ◦ Ensure grayscale rendering (the secondary pass in EpubReaderActivity::renderContents) still looks correct in both orientations. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
186 lines
6.4 KiB
C++
186 lines
6.4 KiB
C++
#include "Section.h"
|
|
|
|
#include <FsHelpers.h>
|
|
#include <SD.h>
|
|
#include <Serialization.h>
|
|
|
|
#include "Page.h"
|
|
#include "parsers/ChapterHtmlSlimParser.h"
|
|
|
|
namespace {
|
|
constexpr uint8_t SECTION_FILE_VERSION = 6;
|
|
} // namespace
|
|
|
|
void Section::onPageComplete(std::unique_ptr<Page> page) {
|
|
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
|
|
|
File outputFile;
|
|
if (!FsHelpers::openFileForWrite("SCT", filePath, outputFile)) {
|
|
return;
|
|
}
|
|
page->serialize(outputFile);
|
|
outputFile.close();
|
|
|
|
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
|
|
|
pageCount++;
|
|
}
|
|
|
|
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
|
const int viewportWidth, const int viewportHeight) const {
|
|
File outputFile;
|
|
if (!FsHelpers::openFileForWrite("SCT", cachePath + "/section.bin", outputFile)) {
|
|
return;
|
|
}
|
|
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
|
serialization::writePod(outputFile, fontId);
|
|
serialization::writePod(outputFile, lineCompression);
|
|
serialization::writePod(outputFile, extraParagraphSpacing);
|
|
serialization::writePod(outputFile, viewportWidth);
|
|
serialization::writePod(outputFile, viewportHeight);
|
|
serialization::writePod(outputFile, pageCount);
|
|
outputFile.close();
|
|
}
|
|
|
|
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
|
const int viewportWidth, const int viewportHeight) {
|
|
const auto sectionFilePath = cachePath + "/section.bin";
|
|
File inputFile;
|
|
if (!FsHelpers::openFileForRead("SCT", sectionFilePath, inputFile)) {
|
|
return false;
|
|
}
|
|
|
|
// Match parameters
|
|
{
|
|
uint8_t version;
|
|
serialization::readPod(inputFile, version);
|
|
if (version != SECTION_FILE_VERSION) {
|
|
inputFile.close();
|
|
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
|
clearCache();
|
|
return false;
|
|
}
|
|
|
|
int fileFontId, fileViewportWidth, fileViewportHeight;
|
|
float fileLineCompression;
|
|
bool fileExtraParagraphSpacing;
|
|
serialization::readPod(inputFile, fileFontId);
|
|
serialization::readPod(inputFile, fileLineCompression);
|
|
serialization::readPod(inputFile, fileExtraParagraphSpacing);
|
|
serialization::readPod(inputFile, fileViewportWidth);
|
|
serialization::readPod(inputFile, fileViewportHeight);
|
|
|
|
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
|
extraParagraphSpacing != fileExtraParagraphSpacing || viewportWidth != fileViewportWidth ||
|
|
viewportHeight != fileViewportHeight) {
|
|
inputFile.close();
|
|
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
|
clearCache();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
serialization::readPod(inputFile, pageCount);
|
|
inputFile.close();
|
|
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
|
return true;
|
|
}
|
|
|
|
void Section::setupCacheDir() const {
|
|
epub->setupCacheDir();
|
|
SD.mkdir(cachePath.c_str());
|
|
}
|
|
|
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
|
bool Section::clearCache() const {
|
|
if (!SD.exists(cachePath.c_str())) {
|
|
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
|
return true;
|
|
}
|
|
|
|
if (!FsHelpers::removeDir(cachePath.c_str())) {
|
|
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
|
|
return false;
|
|
}
|
|
|
|
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
|
|
return true;
|
|
}
|
|
|
|
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
|
const int viewportWidth, const int viewportHeight,
|
|
const std::function<void()>& progressSetupFn,
|
|
const std::function<void(int)>& progressFn) {
|
|
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
|
|
|
// Retry logic for SD card timing issues
|
|
bool success = false;
|
|
size_t fileSize = 0;
|
|
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
|
if (attempt > 0) {
|
|
Serial.printf("[%lu] [SCT] Retrying stream (attempt %d)...\n", millis(), attempt + 1);
|
|
delay(50); // Brief delay before retry
|
|
}
|
|
|
|
// Remove any incomplete file from previous attempt before retrying
|
|
if (SD.exists(tmpHtmlPath.c_str())) {
|
|
SD.remove(tmpHtmlPath.c_str());
|
|
}
|
|
|
|
File tmpHtml;
|
|
if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
|
continue;
|
|
}
|
|
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
|
fileSize = tmpHtml.size();
|
|
tmpHtml.close();
|
|
|
|
// If streaming failed, remove the incomplete file immediately
|
|
if (!success && SD.exists(tmpHtmlPath.c_str())) {
|
|
SD.remove(tmpHtmlPath.c_str());
|
|
Serial.printf("[%lu] [SCT] Removed incomplete temp file after failed attempt\n", millis());
|
|
}
|
|
}
|
|
|
|
if (!success) {
|
|
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file after retries\n", millis());
|
|
return false;
|
|
}
|
|
|
|
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
|
|
|
// Only show progress bar for larger chapters where rendering overhead is worth it
|
|
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
|
|
progressSetupFn();
|
|
}
|
|
|
|
ChapterHtmlSlimParser visitor(
|
|
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight,
|
|
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); }, progressFn);
|
|
success = visitor.parseAndBuildPages();
|
|
|
|
SD.remove(tmpHtmlPath.c_str());
|
|
if (!success) {
|
|
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
|
return false;
|
|
}
|
|
|
|
writeCacheMetadata(fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::unique_ptr<Page> Section::loadPageFromSD() const {
|
|
const auto filePath = cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
|
|
|
File inputFile;
|
|
if (!FsHelpers::openFileForRead("SCT", filePath, inputFile)) {
|
|
return nullptr;
|
|
}
|
|
auto page = Page::deserialize(inputFile);
|
|
inputFile.close();
|
|
return page;
|
|
}
|