diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 954fd1c1..26621ec8 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -51,22 +51,37 @@ void logReaderMemSnapshot(const char* stage) { inline void logReaderMemSnapshot(const char*) {} #endif +// Computes the [0..100] EPUB progress percent. Returns 0 when pageCount is unknown (sync/bookmark +// pre-render writes), in which case the next saveProgress() will overwrite progress.bin with the +// real value before the user can leave the reader. +uint8_t epubProgressPercentByte(const Epub& epub, const int spineIndex, const int currentPage, const int pageCount) { + if (pageCount <= 0) { + return 0; + } + const float chapterProgress = static_cast(currentPage) / static_cast(pageCount); + return ReaderUtils::fractionProgressPercentByte(epub.calculateProgress(spineIndex, chapterProgress)); +} + +// Writes the canonical EPUB progress.bin layout: spine(2) + page(2) + pageCount(2) + percent(1). +// Used by the per-page saveProgress() and by transient writers (sync restore, bookmark jump) so +// the on-disk format stays consistent regardless of caller. bool writeReaderProgressCache(const std::string& cachePath, const int spineIndex, const int currentPage, - const int pageCount) { + const int pageCount, const uint8_t percent) { FsFile f; if (!Storage.openFileForWrite("ERS", cachePath + "/progress.bin", f)) { - LOG_ERR("ERS", "Failed to open progress cache for sync restore: %s", cachePath.c_str()); + LOG_ERR("ERS", "Failed to open progress cache: %s", cachePath.c_str()); return false; } - uint8_t data[6]; + uint8_t data[7]; data[0] = spineIndex & 0xFF; data[1] = (spineIndex >> 8) & 0xFF; data[2] = currentPage & 0xFF; data[3] = (currentPage >> 8) & 0xFF; data[4] = pageCount & 0xFF; data[5] = (pageCount >> 8) & 0xFF; - f.write(data, 6); + data[6] = percent; + f.write(data, 7); f.close(); return true; } @@ -900,7 +915,9 @@ void EpubReaderActivity::applyPendingSyncSession() { // Store 0 to disable rescaling; the paragraph lookup handles precise positioning. const int restorePageCount = (restoreSpineIndex == sync.spineIndex) ? sync.totalPagesInSpine : 0; - if (writeReaderProgressCache(epub->getCachePath(), restoreSpineIndex, restorePage, restorePageCount)) { + // Transient write — the next render's saveProgress() supplies the real percent before the user + // can return to the home screen, so a placeholder 0 here is harmless. + if (writeReaderProgressCache(epub->getCachePath(), restoreSpineIndex, restorePage, restorePageCount, 0)) { cachedSpineIndex = restoreSpineIndex; cachedChapterTotalPageCount = restorePageCount; LOG_DBG("ERS", "Prepared progress.bin for sync restore: spine=%d page=%d/%d", restoreSpineIndex, restorePage, @@ -924,7 +941,8 @@ void EpubReaderActivity::applyPendingBookmarkJump() { return; } LOG_DBG("ERS", "Applying pending bookmark jump: spine=%u page=%u", jump.spineIndex, jump.pageNumber); - if (writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0)) { + // Transient write before initializeReader; saveProgress() overwrites with the real percent. + if (writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0, 0)) { cachedSpineIndex = jump.spineIndex; cachedChapterTotalPageCount = 0; } else { @@ -1383,28 +1401,12 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW } void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) { - FsFile f; - if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { - uint8_t overallPercent = 0; - if (epub->getBookSize() > 0 && pageCount > 0) { - const float chapterProgress = static_cast(currentPage) / static_cast(pageCount); - overallPercent = static_cast( - clampPercent(static_cast(epub->calculateProgress(spineIndex, chapterProgress) * 100.0f + 0.5f))); - } - uint8_t data[7]; - data[0] = spineIndex & 0xFF; - data[1] = (spineIndex >> 8) & 0xFF; - data[2] = currentPage & 0xFF; - data[3] = (currentPage >> 8) & 0xFF; - data[4] = pageCount & 0xFF; - data[5] = (pageCount >> 8) & 0xFF; - data[6] = overallPercent; - f.write(data, 7); - f.close(); - LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d (%d%%)", spineIndex, currentPage, overallPercent); - } else { + const uint8_t percent = epubProgressPercentByte(*epub, spineIndex, currentPage, pageCount); + if (!writeReaderProgressCache(epub->getCachePath(), spineIndex, currentPage, pageCount, percent)) { LOG_ERR("ERS", "Could not save progress!"); + return; } + LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d (%d%%)", spineIndex, currentPage, percent); } void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, diff --git a/src/activities/reader/MdReaderActivity.cpp b/src/activities/reader/MdReaderActivity.cpp index e38ed44e..7d68b586 100644 --- a/src/activities/reader/MdReaderActivity.cpp +++ b/src/activities/reader/MdReaderActivity.cpp @@ -750,7 +750,6 @@ void MdReaderActivity::saveProgress() const { // 7-byte format matching TxtReaderActivity: page(2 bytes LE) + file offset(4 bytes LE) + overallPercent(1 byte) const size_t offset = (currentPage >= 0 && currentPage < static_cast(pageOffsets.size())) ? pageOffsets[currentPage] : 0; - const uint8_t overallPercent = (totalPages > 0) ? static_cast((currentPage + 1) * 100 / totalPages) : 0; uint8_t data[7]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; @@ -758,7 +757,7 @@ void MdReaderActivity::saveProgress() const { data[3] = (offset >> 8) & 0xFF; data[4] = (offset >> 16) & 0xFF; data[5] = (offset >> 24) & 0xFF; - data[6] = overallPercent; + data[6] = ReaderUtils::pageProgressPercentByte(currentPage, totalPages); f.write(data, 7); } } @@ -770,10 +769,8 @@ void MdReaderActivity::loadProgress() { const int dataSize = f.read(data, 7); f.close(); if (dataSize >= 4) { - // Old 4-byte format: uint32 page — detect by checking if bytes 2-3 are non-zero - // (new format stores page in bytes 0-1 only, bytes 2-3 are offset low bytes). - // Since page counts are small (< 65536), bytes 2-3 of the old uint32 page are always 0. - // We can safely read bytes 0-1 as the page number in both formats. + // Page sits in bytes 0-1 in both the old 4-byte uint32 format and the new 7-byte format + // (page counts stay well under 65536, so the upper bytes were always zero). int loadedPage = data[0] + (data[1] << 8); if (totalPages == 0) { currentPage = 0; diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 1e326803..9650bd25 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -4,12 +4,37 @@ #include #include +#include + #include "MappedInputManager.h" namespace ReaderUtils { constexpr unsigned long GO_HOME_MS = 1000; +// Round-half-up integer division clamped to [0, 100], used as the percent byte appended to +// progress.bin so the home screen can render a per-book badge without re-loading the document. +// All reader types must funnel through this so the displayed value matches across formats. +inline uint8_t pageProgressPercentByte(int currentPage, int totalPages) { + if (totalPages <= 0 || currentPage < 0) { + return 0; + } + const long numerator = static_cast(currentPage + 1) * 200L + totalPages; + const long percent = numerator / (2L * totalPages); + if (percent < 0) return 0; + if (percent > 100) return 100; + return static_cast(percent); +} + +// Round-half-up clamp for a pre-computed [0,1] progress fraction (used by EPUB, where progress +// is byte-weighted across spine items rather than a simple page ratio). +inline uint8_t fractionProgressPercentByte(float fraction) { + const int percent = static_cast(fraction * 100.0f + 0.5f); + if (percent < 0) return 0; + if (percent > 100) return 100; + return static_cast(percent); +} + inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) { switch (orientation) { case CrossPointSettings::ORIENTATION::PORTRAIT: diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 2c6afdd1..bed59e7b 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -411,7 +411,6 @@ void TxtReaderActivity::saveProgress() const { // 7-byte format: page(2 bytes LE) + file offset(4 bytes LE) + overallPercent(1 byte) // The offset lets drawCurrentPageToBuffer render without requiring index.bin. const size_t offset = (currentPage < static_cast(pageOffsets.size())) ? pageOffsets[currentPage] : 0; - const uint8_t overallPercent = (totalPages > 0) ? static_cast((currentPage + 1) * 100 / totalPages) : 0; uint8_t data[7]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; @@ -419,7 +418,7 @@ void TxtReaderActivity::saveProgress() const { data[3] = (offset >> 8) & 0xFF; data[4] = (offset >> 16) & 0xFF; data[5] = (offset >> 24) & 0xFF; - data[6] = overallPercent; + data[6] = ReaderUtils::pageProgressPercentByte(currentPage, totalPages); f.write(data, 7); f.close(); } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 89f4d306..63bfc244 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -334,14 +334,13 @@ void XtcReaderActivity::renderPage() { void XtcReaderActivity::saveProgress() const { FsFile f; if (Storage.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) { - const uint32_t pageCount = xtc->getPageCount(); - const uint8_t overallPercent = (pageCount > 0) ? static_cast((currentPage + 1) * 100 / pageCount) : 0; uint8_t data[5]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; data[2] = (currentPage >> 16) & 0xFF; data[3] = (currentPage >> 24) & 0xFF; - data[4] = overallPercent; + data[4] = + ReaderUtils::pageProgressPercentByte(static_cast(currentPage), static_cast(xtc->getPageCount())); f.write(data, 5); f.close(); } diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index e352b8d1..aac3a156 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -136,7 +136,7 @@ int LyraTheme::getRecentBookProgressPercent(const RecentBook& book) { } std::string cachePath; - int percentByteOffset; // byte index of the percent field in progress.bin + int percentByteOffset = 0; // byte index of the percent field in progress.bin if (FsHelpers::hasEpubExtension(book.path)) { cachePath = Epub(book.path, "/.crosspoint").getCachePath(); @@ -160,8 +160,8 @@ int LyraTheme::getRecentBookProgressPercent(const RecentBook& book) { const int dataSize = progressFile.read(data, 7); progressFile.close(); - if (dataSize <= percentByteOffset) { - return -1; // old format without percent byte — not yet read by new firmware + if (dataSize < percentByteOffset + 1) { + return -1; // old format (or pre-render placeholder) without the percent byte } return clampProgressPercent(static_cast(data[percentByteOffset]));