Merge pull request #141 from jpirnay/refactor-progress-theme

refactor: store progress percentage in progress.bin to speedup the badge display
This commit is contained in:
jpirnay
2026-04-27 11:36:32 +02:00
committed by GitHub
7 changed files with 115 additions and 143 deletions
+3 -3
View File
@@ -51,9 +51,9 @@ class HomeActivity final : public Activity {
void dispatchMenuAction(MenuAction action);
void rebuildMenuEntries();
bool storeCoverBuffer(); // Store frame buffer for cover image
bool restoreCoverBuffer(); // Restore frame buffer from stored cover
void freeCoverBuffer(); // Free the stored cover buffer
bool storeCoverBuffer();
bool restoreCoverBuffer();
void freeCoverBuffer();
void loadRecentBooks(int maxBooks);
void loadRecentCovers(int coverHeight);
+28 -19
View File
@@ -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<float>(currentPage) / static_cast<float>(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,21 +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 data[6];
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);
f.close();
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage);
} 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> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom,
+22 -13
View File
@@ -747,29 +747,38 @@ void MdReaderActivity::renderStatusBar() const {
void MdReaderActivity::saveProgress() const {
FsFile f;
if (Storage.openFileForWrite("MDR", txt->getCachePath() + "/progress.bin", f)) {
uint32_t page = static_cast<uint32_t>(currentPage < 0 ? 0 : currentPage);
uint8_t data[4];
data[0] = page & 0xFF;
data[1] = (page >> 8) & 0xFF;
data[2] = (page >> 16) & 0xFF;
data[3] = (page >> 24) & 0xFF;
f.write(data, 4);
// 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<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
uint8_t data[7];
data[0] = currentPage & 0xFF;
data[1] = (currentPage >> 8) & 0xFF;
data[2] = offset & 0xFF;
data[3] = (offset >> 8) & 0xFF;
data[4] = (offset >> 16) & 0xFF;
data[5] = (offset >> 24) & 0xFF;
data[6] = ReaderUtils::pageProgressPercentByte(currentPage, totalPages);
f.write(data, 7);
f.close();
}
}
void MdReaderActivity::loadProgress() {
FsFile f;
if (Storage.openFileForRead("MDR", txt->getCachePath() + "/progress.bin", f)) {
uint8_t data[4];
if (f.read(data, 4) == 4) {
uint32_t loadedPage = static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
uint8_t data[7];
const int dataSize = f.read(data, 7);
f.close();
if (dataSize >= 4) {
// 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;
} else if (loadedPage >= static_cast<uint32_t>(totalPages)) {
} else if (loadedPage >= totalPages) {
currentPage = totalPages - 1;
} else {
currentPage = static_cast<int>(loadedPage);
currentPage = loadedPage;
}
LOG_DBG("MDR", "Loaded progress: page %d/%d", currentPage, totalPages);
}
+25
View File
@@ -4,12 +4,37 @@
#include <GfxRenderer.h>
#include <Logging.h>
#include <cstdint>
#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<long>(currentPage + 1) * 200L + totalPages;
const long percent = numerator / (2L * totalPages);
if (percent < 0) return 0;
if (percent > 100) return 100;
return static_cast<uint8_t>(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<int>(fraction * 100.0f + 0.5f);
if (percent < 0) return 0;
if (percent > 100) return 100;
return static_cast<uint8_t>(percent);
}
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
switch (orientation) {
case CrossPointSettings::ORIENTATION::PORTRAIT:
+4 -3
View File
@@ -408,17 +408,18 @@ void TxtReaderActivity::renderStatusBar() const {
void TxtReaderActivity::saveProgress() const {
FsFile f;
if (Storage.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
// 6-byte format: page(2 bytes LE) + file offset(4 bytes LE)
// 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<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
uint8_t data[6];
uint8_t data[7];
data[0] = currentPage & 0xFF;
data[1] = (currentPage >> 8) & 0xFF;
data[2] = offset & 0xFF;
data[3] = (offset >> 8) & 0xFF;
data[4] = (offset >> 16) & 0xFF;
data[5] = (offset >> 24) & 0xFF;
f.write(data, 6);
data[6] = ReaderUtils::pageProgressPercentByte(currentPage, totalPages);
f.write(data, 7);
f.close();
}
}
+8 -7
View File
@@ -333,12 +333,14 @@ void XtcReaderActivity::renderPage() {
void XtcReaderActivity::saveProgress() const {
FsFile f;
if (Storage.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) {
uint8_t data[4];
uint8_t data[5];
data[0] = currentPage & 0xFF;
data[1] = (currentPage >> 8) & 0xFF;
data[2] = (currentPage >> 16) & 0xFF;
data[3] = (currentPage >> 24) & 0xFF;
f.write(data, 4);
data[4] =
ReaderUtils::pageProgressPercentByte(static_cast<int>(currentPage), static_cast<int>(xtc->getPageCount()));
f.write(data, 5);
f.close();
}
}
@@ -463,11 +465,10 @@ void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION a
case BA::BTN_NEXT_SECTION:
if (xtc->hasChapters()) {
const auto& chapters = xtc->getChapters();
const auto nextChapter = std::find_if(chapters.begin(), chapters.end(),
[this](const auto& ch) { return ch.startPage > currentPage; });
if (nextChapter != chapters.end()) {
currentPage = nextChapter->startPage;
const auto it = std::find_if(chapters.begin(), chapters.end(),
[this](const auto& ch) { return ch.startPage > currentPage; });
if (it != chapters.end()) {
currentPage = it->startPage;
requestUpdate();
}
}
+25 -98
View File
@@ -8,7 +8,6 @@
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Serialization.h>
#include <Txt.h>
#include <Xtc.h>
@@ -58,30 +57,6 @@ int clampProgressPercent(const int progressPercent) {
return progressPercent;
}
int readTxtTotalPages(const std::string& cachePath) {
FsFile indexFile;
if (!Storage.openFileForRead("LYR", cachePath + "/index.bin", indexFile)) {
return 0;
}
uint32_t magic = 0;
uint8_t version = 0;
serialization::readPod(indexFile, magic);
serialization::readPod(indexFile, version);
static constexpr uint32_t INDEX_CACHE_MAGIC = 0x54585449; // "TXTI"
static constexpr uint8_t INDEX_CACHE_VERSION = 2;
if (magic != INDEX_CACHE_MAGIC || version != INDEX_CACHE_VERSION) {
indexFile.close();
return 0;
}
indexFile.seek(32);
uint32_t totalPages = 0;
serialization::readPod(indexFile, totalPages);
indexFile.close();
return static_cast<int>(totalPages);
}
void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight,
uint16_t percentage) {
BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
@@ -152,92 +127,44 @@ const uint8_t* iconForName(UIIcon icon, int size) {
}
} // namespace
// Reads the overall progress percent stored as the last byte of progress.bin.
// The cache path is derived from the book path alone (no epub/xtc/txt loading needed).
// Returns -1 if the file is absent or the percent byte is not yet written.
int LyraTheme::getRecentBookProgressPercent(const RecentBook& book) {
if (book.path.empty()) {
return -1;
}
std::string cachePath;
int percentByteOffset = 0; // byte index of the percent field in progress.bin
if (FsHelpers::hasEpubExtension(book.path)) {
Epub epub(book.path, "/.crosspoint");
if (!epub.load(true, true)) {
return -1;
}
FsFile progressFile;
if (!Storage.openFileForRead("LYR", epub.getCachePath() + "/progress.bin", progressFile)) {
return -1;
}
uint8_t data[6];
const int dataSize = progressFile.read(data, 6);
progressFile.close();
if (dataSize != 4 && dataSize != 6) {
return -1;
}
const int currentSpineIndex = data[0] + (data[1] << 8);
const int currentPage = data[2] + (data[3] << 8);
const int pageCount = (dataSize == 6) ? (data[4] + (data[5] << 8)) : 0;
if (pageCount <= 0) {
return -1;
}
const float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
return clampProgressPercent(
static_cast<int>(std::lround(epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f)));
cachePath = Epub(book.path, "/.crosspoint").getCachePath();
percentByteOffset = 6; // epub: [spineIdx(2), page(2), chapterPageCount(2), percent(1)]
} else if (FsHelpers::hasXtcExtension(book.path)) {
cachePath = Xtc(book.path, "/.crosspoint").getCachePath();
percentByteOffset = 4; // xtc: [page(4), percent(1)]
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
cachePath = Txt(book.path, "/.crosspoint").getCachePath();
percentByteOffset = 6; // [page(2), offset(4), percent(1)]
} else {
return -1;
}
if (FsHelpers::hasXtcExtension(book.path)) {
Xtc xtc(book.path, "/.crosspoint");
if (!xtc.load()) {
return -1;
}
FsFile progressFile;
if (!Storage.openFileForRead("LYR", xtc.getCachePath() + "/progress.bin", progressFile)) {
return -1;
}
uint8_t data[4];
if (progressFile.read(data, 4) != 4) {
progressFile.close();
return -1;
}
progressFile.close();
const uint32_t currentPage = static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
return clampProgressPercent(static_cast<int>(xtc.calculateProgress(currentPage)));
FsFile progressFile;
if (!Storage.openFileForRead("LYR", cachePath + "/progress.bin", progressFile)) {
return -1;
}
if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
Txt txt(book.path, "/.crosspoint");
if (!txt.load()) {
return -1;
}
uint8_t data[7];
const int dataSize = progressFile.read(data, 7);
progressFile.close();
FsFile progressFile;
if (!Storage.openFileForRead("LYR", txt.getCachePath() + "/progress.bin", progressFile)) {
return -1;
}
uint8_t data[4];
if (progressFile.read(data, 4) != 4) {
progressFile.close();
return -1;
}
progressFile.close();
const int currentPage = data[0] + (data[1] << 8);
const int totalPages = readTxtTotalPages(txt.getCachePath());
if (totalPages <= 0) {
return -1;
}
return clampProgressPercent(static_cast<int>(std::lround((currentPage + 1) * 100.0f / totalPages)));
if (dataSize < percentByteOffset + 1) {
return -1; // old format (or pre-render placeholder) without the percent byte
}
return -1;
return clampProgressPercent(static_cast<int>(data[percentByteOffset]));
}
void LyraTheme::drawProgressBadge(const GfxRenderer& renderer, Rect anchorRect, int progressPercent) {