diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 37e76a2..9245cc9 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -40,6 +40,12 @@ class Page { public: // the list of block index and line numbers on this page std::vector> 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 deserialize(FsFile& file); diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 9f43321..91dda07 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -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) { @@ -181,12 +186,23 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled); - std::vector lut = {}; + + // LUT entries: { filePosition, contentOffset } pairs + struct LutEntry { + uint32_t filePos; + uint32_t contentOffset; + }; + std::vector lut = {}; ChapterHtmlSlimParser visitor( tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled, - [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, progressFn, + [this, &lut](std::unique_ptr 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(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 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; +} diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 5b72614..4294ef1 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -36,4 +36,9 @@ class Section { const std::function& progressSetupFn = nullptr, const std::function& progressFn = nullptr); std::unique_ptr 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; }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 275cc01..d3fb90b 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -332,6 +332,11 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char if (self->skipUntilDepth < self->depth) { 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; @@ -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; } @@ -495,19 +501,24 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { const size_t totalSize = file.size(); size_t bytesRead = 0; int lastProgress = -1; + + // Initialize offset tracking - first page starts at offset 0 + currentPageStartOffset = 0; + lastCharDataOffset = 0; - XML_SetUserData(parser, this); - XML_SetElementHandler(parser, startElement, endElement); - XML_SetCharacterDataHandler(parser, characterData); + 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(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(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(currentPageStartOffset); + } completePageFn(std::move(currentPage)); currentPage.reset(); currentTextBlock.reset(); @@ -570,8 +588,15 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr 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(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; } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 3da5c58..9042581 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -54,6 +54,11 @@ class ChapterHtmlSlimParser { bool effectiveBold = false; 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); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 47648a6..c96358c 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -1,9 +1,11 @@ #include "EpubReaderActivity.h" #include +#include #include #include #include +#include #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)) { - 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); + 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); + 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(currentSpineIndex)); + serialization::writePod(f, static_cast(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); } } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 7018b9f..b87a51d 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -22,6 +22,10 @@ class EpubReaderActivity final : public ActivityWithSubactivity { // End-of-book prompt state 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(); diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 94442ad..631f960 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -5,6 +5,8 @@ #include #include +#include + #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(pageOffsets.size())) + ? pageOffsets[currentPage] : 0; + serialization::writePod(f, static_cast(byteOffset)); + + // Also store page number for debugging/logging purposes + serialization::writePod(f, static_cast(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)) { - uint8_t data[4]; - if (f.read(data, 4) == 4) { - currentPage = data[0] + (data[1] << 8); - if (currentPage >= totalPages) { - currentPage = totalPages - 1; - } - if (currentPage < 0) { + // 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; } - Serial.printf("[%lu] [TRS] Loaded progress: page %d/%d\n", millis(), currentPage, totalPages); + } 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; + } + 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(std::distance(pageOffsets.begin(), it) - 1); +} + bool TxtReaderActivity::loadPageIndexCache() { // Cache file format (using serialization module): // - uint32_t: magic "TXTI" diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index dd4e152..04369be 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -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,