granular position tracking
This commit is contained in:
parent
fedc14bcb4
commit
91c8cc67ce
@ -40,6 +40,12 @@ class Page {
|
||||
public:
|
||||
// the list of block index and line numbers on this page
|
||||
std::vector<std::shared_ptr<PageElement>> elements;
|
||||
|
||||
// Byte offset in source HTML where this page's content begins
|
||||
// Used for restoring reading position after re-indexing due to font/setting changes
|
||||
// This is stored in the Section file's LUT, not in Page serialization
|
||||
uint32_t firstContentOffset = 0;
|
||||
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
bool serialize(FsFile& file) const;
|
||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||
|
||||
@ -8,10 +8,15 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 11;
|
||||
// Version 12: Added content offsets to LUT for position restoration after re-indexing
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 12;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
||||
sizeof(uint32_t);
|
||||
|
||||
// LUT entry structure: { filePosition, contentOffset }
|
||||
// Each entry is 8 bytes (2 x uint32_t)
|
||||
constexpr size_t LUT_ENTRY_SIZE = sizeof(uint32_t) * 2;
|
||||
} // namespace
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
@ -181,12 +186,23 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled);
|
||||
std::vector<uint32_t> lut = {};
|
||||
|
||||
// LUT entries: { filePosition, contentOffset } pairs
|
||||
struct LutEntry {
|
||||
uint32_t filePos;
|
||||
uint32_t contentOffset;
|
||||
};
|
||||
std::vector<LutEntry> lut = {};
|
||||
|
||||
ChapterHtmlSlimParser visitor(
|
||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, progressFn,
|
||||
[this, &lut](std::unique_ptr<Page> page) {
|
||||
// Capture content offset before processing
|
||||
const uint32_t contentOffset = page->firstContentOffset;
|
||||
const uint32_t filePos = this->onPageComplete(std::move(page));
|
||||
lut.push_back({filePos, contentOffset});
|
||||
}, progressFn,
|
||||
epub->getCssParser());
|
||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||
success = visitor.parseAndBuildPages();
|
||||
@ -197,8 +213,10 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
// Create a placeholder page for malformed chapters instead of failing entirely
|
||||
// This allows the book to continue loading with chapters that do parse successfully
|
||||
auto placeholderPage = std::unique_ptr<Page>(new Page());
|
||||
placeholderPage->firstContentOffset = 0;
|
||||
// Add placeholder to LUT
|
||||
lut.emplace_back(this->onPageComplete(std::move(placeholderPage)));
|
||||
const uint32_t filePos = this->onPageComplete(std::move(placeholderPage));
|
||||
lut.push_back({filePos, 0});
|
||||
|
||||
// If we still have no pages, the placeholder creation failed
|
||||
if (pageCount == 0) {
|
||||
@ -211,13 +229,14 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
|
||||
const uint32_t lutOffset = file.position();
|
||||
bool hasFailedLutRecords = false;
|
||||
// Write LUT
|
||||
for (const uint32_t& pos : lut) {
|
||||
if (pos == 0) {
|
||||
// Write LUT with both file position and content offset
|
||||
for (const auto& entry : lut) {
|
||||
if (entry.filePos == 0) {
|
||||
hasFailedLutRecords = true;
|
||||
break;
|
||||
}
|
||||
serialization::writePod(file, pos);
|
||||
serialization::writePod(file, entry.filePos);
|
||||
serialization::writePod(file, entry.contentOffset);
|
||||
}
|
||||
|
||||
if (hasFailedLutRecords) {
|
||||
@ -243,12 +262,106 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(file, lutOffset);
|
||||
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
||||
|
||||
// LUT entries are now 8 bytes each: { filePos (4), contentOffset (4) }
|
||||
file.seek(lutOffset + LUT_ENTRY_SIZE * currentPage);
|
||||
uint32_t pagePos;
|
||||
serialization::readPod(file, pagePos);
|
||||
// Skip contentOffset for now - we don't need it when just loading the page
|
||||
|
||||
file.seek(pagePos);
|
||||
|
||||
auto page = Page::deserialize(file);
|
||||
file.close();
|
||||
return page;
|
||||
}
|
||||
|
||||
int Section::findPageForContentOffset(uint32_t targetOffset) const {
|
||||
if (pageCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FsFile f;
|
||||
if (!SdMan.openFileForRead("SCT", filePath, f)) {
|
||||
Serial.printf("[%lu] [SCT] findPageForContentOffset: Failed to open file\n", millis());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read LUT offset from header
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(f, lutOffset);
|
||||
|
||||
// Binary search through the LUT to find the page containing targetOffset
|
||||
// We want the largest contentOffset that is <= targetOffset
|
||||
int left = 0;
|
||||
int right = pageCount - 1;
|
||||
int result = 0;
|
||||
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
|
||||
// Read content offset for page 'mid'
|
||||
// LUT entry format: { filePos (4), contentOffset (4) }
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * mid + sizeof(uint32_t)); // Skip filePos
|
||||
uint32_t midOffset;
|
||||
serialization::readPod(f, midOffset);
|
||||
|
||||
if (midOffset <= targetOffset) {
|
||||
result = mid; // This page could be the answer
|
||||
left = mid + 1; // Look for a later page that might also qualify
|
||||
} else {
|
||||
right = mid - 1; // Look for an earlier page
|
||||
}
|
||||
}
|
||||
|
||||
// When multiple pages share the same content offset (e.g., a large text
|
||||
// block spanning multiple pages), scan backward to find the FIRST page
|
||||
// with that offset, not the last
|
||||
if (result > 0) {
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * result + sizeof(uint32_t));
|
||||
uint32_t resultOffset;
|
||||
serialization::readPod(f, resultOffset);
|
||||
|
||||
while (result > 0) {
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * (result - 1) + sizeof(uint32_t));
|
||||
uint32_t prevOffset;
|
||||
serialization::readPod(f, prevOffset);
|
||||
if (prevOffset == resultOffset) {
|
||||
result--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.close();
|
||||
Serial.printf("[%lu] [SCT] findPageForContentOffset: offset %u -> page %d\n", millis(), targetOffset, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t Section::getContentOffsetForPage(int pageIndex) const {
|
||||
if (pageCount == 0 || pageIndex < 0 || pageIndex >= pageCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FsFile f;
|
||||
if (!SdMan.openFileForRead("SCT", filePath, f)) {
|
||||
Serial.printf("[%lu] [SCT] getContentOffsetForPage: Failed to open file\n", millis());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read LUT offset from header
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(f, lutOffset);
|
||||
|
||||
// Read content offset for the specified page
|
||||
// LUT entry format: { filePos (4), contentOffset (4) }
|
||||
f.seek(lutOffset + LUT_ENTRY_SIZE * pageIndex + sizeof(uint32_t)); // Skip filePos
|
||||
uint32_t contentOffset;
|
||||
serialization::readPod(f, contentOffset);
|
||||
|
||||
f.close();
|
||||
return contentOffset;
|
||||
}
|
||||
|
||||
@ -36,4 +36,9 @@ class Section {
|
||||
const std::function<void()>& progressSetupFn = nullptr,
|
||||
const std::function<void(int)>& progressFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
|
||||
// Methods for content offset-based position tracking
|
||||
// Used to restore reading position after re-indexing due to font/setting changes
|
||||
int findPageForContentOffset(uint32_t targetOffset) const;
|
||||
uint32_t getContentOffsetForPage(int pageIndex) const;
|
||||
};
|
||||
|
||||
@ -333,6 +333,11 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture byte offset of this character data for page position tracking
|
||||
if (self->xmlParser) {
|
||||
self->lastCharDataOffset = XML_GetCurrentByteIndex(self->xmlParser);
|
||||
}
|
||||
|
||||
// Determine font style from depth-based tracking and CSS effective style
|
||||
const bool isBold = self->boldUntilDepth < self->depth || self->effectiveBold;
|
||||
const bool isItalic = self->italicUntilDepth < self->depth || self->effectiveItalic;
|
||||
@ -477,17 +482,18 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
||||
|
||||
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
xmlParser = XML_ParserCreate(nullptr);
|
||||
int done;
|
||||
|
||||
if (!parser) {
|
||||
if (!xmlParser) {
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile file;
|
||||
if (!SdMan.openFileForRead("EHP", filepath, file)) {
|
||||
XML_ParserFree(parser);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -496,18 +502,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
size_t bytesRead = 0;
|
||||
int lastProgress = -1;
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
// Initialize offset tracking - first page starts at offset 0
|
||||
currentPageStartOffset = 0;
|
||||
lastCharDataOffset = 0;
|
||||
|
||||
XML_SetUserData(xmlParser, this);
|
||||
XML_SetElementHandler(xmlParser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(xmlParser, characterData);
|
||||
|
||||
do {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
void* const buf = XML_GetBuffer(xmlParser, 1024);
|
||||
if (!buf) {
|
||||
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@ -516,10 +527,11 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
|
||||
if (len == 0 && file.available() > 0) {
|
||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@ -537,27 +549,33 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
|
||||
done = file.available() == 0;
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
if (XML_ParseBuffer(xmlParser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(xmlParser),
|
||||
XML_ErrorString(XML_GetErrorCode(xmlParser)));
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
} while (!done);
|
||||
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(parser, nullptr);
|
||||
XML_ParserFree(parser);
|
||||
XML_StopParser(xmlParser, XML_FALSE); // Stop any pending processing
|
||||
XML_SetElementHandler(xmlParser, nullptr, nullptr); // Clear callbacks
|
||||
XML_SetCharacterDataHandler(xmlParser, nullptr);
|
||||
XML_ParserFree(xmlParser);
|
||||
xmlParser = nullptr;
|
||||
file.close();
|
||||
|
||||
// Process last page if there is still text
|
||||
if (currentTextBlock) {
|
||||
makePages();
|
||||
// Set the content offset for the final page
|
||||
if (currentPage) {
|
||||
currentPage->firstContentOffset = static_cast<uint32_t>(currentPageStartOffset);
|
||||
}
|
||||
completePageFn(std::move(currentPage));
|
||||
currentPage.reset();
|
||||
currentTextBlock.reset();
|
||||
@ -570,8 +588,15 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||
|
||||
if (currentPageNextY + lineHeight > viewportHeight) {
|
||||
// Set the content offset for the page being completed
|
||||
if (currentPage) {
|
||||
currentPage->firstContentOffset = static_cast<uint32_t>(currentPageStartOffset);
|
||||
}
|
||||
completePageFn(std::move(currentPage));
|
||||
|
||||
// Start new page - offset will be set when first content is added
|
||||
currentPage.reset(new Page());
|
||||
currentPageStartOffset = lastCharDataOffset; // Use offset from when content was parsed
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
@ -587,6 +612,8 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
|
||||
if (!currentPage) {
|
||||
currentPage.reset(new Page());
|
||||
// Use offset captured during character data parsing
|
||||
currentPageStartOffset = lastCharDataOffset;
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
|
||||
@ -55,6 +55,11 @@ class ChapterHtmlSlimParser {
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
|
||||
// Byte offset tracking for position restoration after re-indexing
|
||||
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
||||
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
||||
size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing)
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
void startNewTextBlock(TextBlock::Style style);
|
||||
void startNewTextBlock(TextBlock::Style style, const BlockStyle& blockStyle);
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
#include "EpubReaderActivity.h"
|
||||
|
||||
#include <Epub/Page.h>
|
||||
#include <Epub/Section.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "BookManager.h"
|
||||
#include "CrossPointSettings.h"
|
||||
@ -22,6 +24,10 @@ namespace {
|
||||
constexpr unsigned long skipChapterMs = 700;
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
constexpr int statusBarMargin = 19;
|
||||
|
||||
// Progress file version for content offset tracking
|
||||
// Version 1: Added content offset for position restoration after re-indexing
|
||||
constexpr uint8_t EPUB_PROGRESS_VERSION = 1;
|
||||
} // namespace
|
||||
|
||||
void EpubReaderActivity::taskTrampoline(void* param) {
|
||||
@ -101,11 +107,47 @@ void EpubReaderActivity::onEnter() {
|
||||
|
||||
FsFile f;
|
||||
if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||
const size_t fileSize = f.size();
|
||||
|
||||
if (fileSize >= 9) {
|
||||
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
|
||||
uint8_t version;
|
||||
serialization::readPod(f, version);
|
||||
|
||||
if (version == EPUB_PROGRESS_VERSION) {
|
||||
uint16_t spineIndex, pageNumber;
|
||||
serialization::readPod(f, spineIndex);
|
||||
serialization::readPod(f, pageNumber);
|
||||
serialization::readPod(f, savedContentOffset);
|
||||
|
||||
currentSpineIndex = spineIndex;
|
||||
nextPageNumber = pageNumber;
|
||||
hasContentOffset = true;
|
||||
|
||||
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n",
|
||||
millis(), currentSpineIndex, nextPageNumber, savedContentOffset);
|
||||
} else {
|
||||
// Unknown version, try legacy format
|
||||
f.seek(0);
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
currentSpineIndex = data[0] + (data[1] << 8);
|
||||
nextPageNumber = data[2] + (data[3] << 8);
|
||||
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber);
|
||||
hasContentOffset = false;
|
||||
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n",
|
||||
millis(), version, currentSpineIndex, nextPageNumber);
|
||||
}
|
||||
}
|
||||
} else if (fileSize >= 4) {
|
||||
// Legacy format: just spineIndex (2) + pageNumber (2) = 4 bytes
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
currentSpineIndex = data[0] + (data[1] << 8);
|
||||
nextPageNumber = data[2] + (data[3] << 8);
|
||||
hasContentOffset = false;
|
||||
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n",
|
||||
millis(), currentSpineIndex, nextPageNumber);
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
@ -435,10 +477,13 @@ void EpubReaderActivity::renderScreen() {
|
||||
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
||||
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
||||
|
||||
bool sectionWasReIndexed = false;
|
||||
|
||||
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());
|
||||
sectionWasReIndexed = true;
|
||||
|
||||
// Progress bar dimensions
|
||||
constexpr int barWidth = 200;
|
||||
@ -491,9 +536,21 @@ void EpubReaderActivity::renderScreen() {
|
||||
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
|
||||
}
|
||||
|
||||
// Determine the correct page to display
|
||||
if (nextPageNumber == UINT16_MAX) {
|
||||
// Special case: go to last page
|
||||
section->currentPage = section->pageCount - 1;
|
||||
} else if (sectionWasReIndexed && hasContentOffset) {
|
||||
// Section was re-indexed (settings changed) and we have a content offset
|
||||
// Use the offset to find the correct page
|
||||
const int restoredPage = section->findPageForContentOffset(savedContentOffset);
|
||||
section->currentPage = restoredPage;
|
||||
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n",
|
||||
millis(), savedContentOffset, restoredPage, nextPageNumber);
|
||||
// Clear the offset flag since we've used it
|
||||
hasContentOffset = false;
|
||||
} else {
|
||||
// Normal case: use the saved page number
|
||||
section->currentPage = nextPageNumber;
|
||||
}
|
||||
}
|
||||
@ -540,16 +597,21 @@ void EpubReaderActivity::renderScreen() {
|
||||
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
||||
}
|
||||
|
||||
// Save progress
|
||||
// Save progress with content offset for position restoration after re-indexing
|
||||
FsFile f;
|
||||
if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", 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);
|
||||
// Get content offset for current page
|
||||
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
||||
|
||||
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
|
||||
serialization::writePod(f, EPUB_PROGRESS_VERSION);
|
||||
serialization::writePod(f, static_cast<uint16_t>(currentSpineIndex));
|
||||
serialization::writePod(f, static_cast<uint16_t>(section->currentPage));
|
||||
serialization::writePod(f, contentOffset);
|
||||
|
||||
f.close();
|
||||
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n",
|
||||
millis(), currentSpineIndex, section->currentPage, contentOffset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,10 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
||||
bool showingEndOfBookPrompt = false;
|
||||
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep (default to safe option)
|
||||
|
||||
// Content offset for position restoration after re-indexing
|
||||
uint32_t savedContentOffset = 0;
|
||||
bool hasContentOffset = false; // True if we have a valid content offset to use
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void renderScreen();
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
#include <Serialization.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "BookManager.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
@ -20,6 +22,9 @@ constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
||||
// Cache file magic and version
|
||||
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
||||
|
||||
// Progress file version (for byte offset tracking)
|
||||
constexpr uint8_t PROGRESS_VERSION = 1;
|
||||
} // namespace
|
||||
|
||||
void TxtReaderActivity::taskTrampoline(void* param) {
|
||||
@ -617,34 +622,90 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
|
||||
void TxtReaderActivity::saveProgress() const {
|
||||
FsFile f;
|
||||
if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[4];
|
||||
data[0] = currentPage & 0xFF;
|
||||
data[1] = (currentPage >> 8) & 0xFF;
|
||||
data[2] = 0;
|
||||
data[3] = 0;
|
||||
f.write(data, 4);
|
||||
// New format: version + byte offset + page number (for backwards compatibility debugging)
|
||||
serialization::writePod(f, PROGRESS_VERSION);
|
||||
|
||||
// Store byte offset - this is stable across font/setting changes
|
||||
const size_t byteOffset = (currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size()))
|
||||
? pageOffsets[currentPage] : 0;
|
||||
serialization::writePod(f, static_cast<uint32_t>(byteOffset));
|
||||
|
||||
// Also store page number for debugging/logging purposes
|
||||
serialization::writePod(f, static_cast<uint16_t>(currentPage));
|
||||
|
||||
f.close();
|
||||
Serial.printf("[%lu] [TRS] Saved progress: page %d, offset %zu\n", millis(), currentPage, byteOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void TxtReaderActivity::loadProgress() {
|
||||
FsFile f;
|
||||
if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
||||
// Check file size to determine format
|
||||
const size_t fileSize = f.size();
|
||||
|
||||
if (fileSize >= 7) {
|
||||
// New format: version (1) + byte offset (4) + page number (2) = 7 bytes
|
||||
uint8_t version;
|
||||
serialization::readPod(f, version);
|
||||
|
||||
if (version == PROGRESS_VERSION) {
|
||||
uint32_t savedOffset;
|
||||
serialization::readPod(f, savedOffset);
|
||||
|
||||
uint16_t savedPage;
|
||||
serialization::readPod(f, savedPage);
|
||||
|
||||
// Use byte offset to find the correct page (works even if re-indexed)
|
||||
currentPage = findPageForOffset(savedOffset);
|
||||
|
||||
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n",
|
||||
millis(), savedOffset, currentPage, totalPages, savedPage);
|
||||
} else {
|
||||
// Unknown version, fall back to legacy behavior
|
||||
Serial.printf("[%lu] [TRS] Unknown progress version %d, ignoring\n", millis(), version);
|
||||
currentPage = 0;
|
||||
}
|
||||
} else if (fileSize >= 4) {
|
||||
// Legacy format: just page number (4 bytes)
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
currentPage = data[0] + (data[1] << 8);
|
||||
Serial.printf("[%lu] [TRS] Loaded legacy progress: page %d/%d\n", millis(), currentPage, totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
if (currentPage >= totalPages) {
|
||||
currentPage = totalPages - 1;
|
||||
}
|
||||
if (currentPage < 0) {
|
||||
currentPage = 0;
|
||||
}
|
||||
Serial.printf("[%lu] [TRS] Loaded progress: page %d/%d\n", millis(), currentPage, totalPages);
|
||||
}
|
||||
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
int TxtReaderActivity::findPageForOffset(size_t targetOffset) const {
|
||||
if (pageOffsets.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Binary search: find the largest offset that is <= targetOffset
|
||||
// This finds the page that contains or starts at the target offset
|
||||
auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), targetOffset);
|
||||
|
||||
if (it == pageOffsets.begin()) {
|
||||
// Target is before the first page, return page 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
// upper_bound returns iterator to first element > targetOffset
|
||||
// So we need the element before it (which is <= targetOffset)
|
||||
return static_cast<int>(std::distance(pageOffsets.begin(), it) - 1);
|
||||
}
|
||||
|
||||
bool TxtReaderActivity::loadPageIndexCache() {
|
||||
// Cache file format (using serialization module):
|
||||
// - uint32_t: magic "TXTI"
|
||||
|
||||
@ -52,6 +52,7 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
||||
void savePageIndexCache() const;
|
||||
void saveProgress() const;
|
||||
void loadProgress();
|
||||
int findPageForOffset(size_t targetOffset) const;
|
||||
|
||||
public:
|
||||
explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user