From 99d8c4656ab418c99d38d7ca939e786179489bee Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 6 Apr 2026 19:43:57 +0200 Subject: [PATCH] Add progress badges to HomeScreen --- .../themes/lyra/Lyra3CoversTheme.cpp | 6 + src/components/themes/lyra/LyraTheme.cpp | 149 ++++++++++++++++++ src/components/themes/lyra/LyraTheme.h | 4 + 3 files changed, 159 insertions(+) diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 4d9f403e..b2ef5e44 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -83,6 +83,7 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con for (int i = 0; i < std::min(static_cast(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); i++) { bool bookSelected = (selectorIndex == i); + const int progressPercent = getRecentBookProgressPercent(recentBooks[i]); int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i; @@ -106,6 +107,11 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con cornerRadius, false, false, true, true, Color::LightGray); } + drawProgressBadge(renderer, + Rect{tileX + hPaddingInSelection, tileY + hPaddingInSelection, + tileWidth - 2 * hPaddingInSelection, coverHeight}, + progressPercent); + int currentY = tileY + coverHeight + hPaddingInSelection + 5; for (const auto& line : titleLines) { renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, currentY, line.c_str(), true); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index b31c2611..ef2a8a1f 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -1,12 +1,18 @@ #include "LyraTheme.h" +#include +#include #include #include #include #include #include #include +#include +#include +#include +#include #include #include #include @@ -44,8 +50,39 @@ constexpr int mainMenuIconSize = 32; constexpr int listIconSize = 24; constexpr int mainMenuColumns = 2; constexpr int minAdaptiveMenuRowHeight = 40; +constexpr int progressBadgeInset = 8; int coverWidth = 0; +int clampProgressPercent(const int progressPercent) { + if (progressPercent < 0) return 0; + if (progressPercent > 100) return 100; + 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(totalPages); +} + void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) { BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight); @@ -116,6 +153,111 @@ const uint8_t* iconForName(UIIcon icon, int size) { } } // namespace +int LyraTheme::getRecentBookProgressPercent(const RecentBook& book) { + if (book.path.empty()) { + return -1; + } + + 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(currentPage) / static_cast(pageCount); + return clampProgressPercent( + static_cast(std::lround(epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f))); + } + + 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(data[0]) | (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | (static_cast(data[3]) << 24); + return clampProgressPercent(static_cast(xtc.calculateProgress(currentPage))); + } + + if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) { + Txt txt(book.path, "/.crosspoint"); + if (!txt.load()) { + return -1; + } + + 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(std::lround((currentPage + 1) * 100.0f / totalPages))); + } + + return -1; +} + +void LyraTheme::drawProgressBadge(GfxRenderer& renderer, Rect anchorRect, int progressPercent) { + if (progressPercent < 0) { + return; + } + + const std::string badgeText = std::to_string(clampProgressPercent(progressPercent)) + "%"; + const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, badgeText.c_str()); + const int textHeight = renderer.getLineHeight(SMALL_FONT_ID); + const int badgeWidth = textWidth + 12; + const int badgeHeight = textHeight + 6; + const int badgeX = anchorRect.x + anchorRect.width - badgeWidth - progressBadgeInset; + const int badgeY = anchorRect.y + progressBadgeInset; + + renderer.fillRoundedRect(badgeX, badgeY, badgeWidth, badgeHeight, 5, Color::Black); + renderer.drawText(SMALL_FONT_ID, badgeX + (badgeWidth - textWidth) / 2, badgeY + 3, badgeText.c_str(), false); +} + void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Left aligned: icon on left, percentage on right (reader mode) const uint16_t percentage = powerManager.getBatteryPercentage(); @@ -442,6 +584,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // Only load from SD on first render, then use stored buffer if (hasContinueReading) { RecentBook book = recentBooks[0]; + const int progressPercent = getRecentBookProgressPercent(book); if (!coverRendered) { std::string coverPath = book.coverBmpPath; bool hasCover = true; @@ -496,6 +639,12 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: cornerRadius, false, false, true, true, Color::LightGray); } + drawProgressBadge( + renderer, + Rect{tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing, tileY, + tileWidth - 2 * hPaddingInSelection - coverWidth - LyraMetrics::values.verticalSpacing, tileHeight}, + progressPercent); + auto titleLines = renderer.wrappedText(UI_12_FONT_ID, book.title.c_str(), textWidth, 3, EpdFontFamily::BOLD); auto author = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), textWidth); diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index dff76869..57bd5efb 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -69,4 +69,8 @@ class LyraTheme : public BaseTheme { void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override; void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override; bool showsFileIcons() const override { return true; } + + protected: + static int getRecentBookProgressPercent(const RecentBook& book); + static void drawProgressBadge(GfxRenderer& renderer, Rect anchorRect, int progressPercent); };