granular position tracking

This commit is contained in:
cottongin
2026-01-25 00:24:54 -05:00
parent fedc14bcb4
commit 91c8cc67ce
9 changed files with 345 additions and 61 deletions

View File

@@ -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)) {
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<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);
}
}

View File

@@ -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();

View File

@@ -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)) {
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<int>(std::distance(pageOffsets.begin(), it) - 1);
}
bool TxtReaderActivity::loadPageIndexCache() {
// Cache file format (using serialization module):
// - uint32_t: magic "TXTI"

View File

@@ -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,