From bd8132a26028d7b3148bfa556425324c5099456e Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Fri, 6 Feb 2026 14:58:32 +0700 Subject: [PATCH] fix: Lag before displaying covers on home screen (#721) ## Summary Reduce/fix the lag on the home screen before recent book covers are rendered ## Additional Context We were previously rendering the screen in two steps, delaying the recent book covers render to avoid a lag before the screen loads. In this PR, we are now doing that only if at least one book doesn't have the cover thumbnail generated yet. If all thumbs are already generated, we load and display them right away, with no lag. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO **_ --- lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 2 +- lib/Xtc/Xtc.cpp | 2 +- src/RecentBooksStore.cpp | 13 +++ src/RecentBooksStore.h | 3 + src/activities/home/HomeActivity.cpp | 83 +++++++++---------- src/activities/home/HomeActivity.h | 5 +- src/components/themes/BaseTheme.cpp | 7 +- src/components/themes/lyra/LyraTheme.cpp | 49 ++++++----- 8 files changed, 95 insertions(+), 69 deletions(-) diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 837bfd27..0b1541b4 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -567,5 +567,5 @@ bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bm // Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight) { - return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, false); + return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true); } diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 05f6651d..717bb670 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -340,7 +340,7 @@ bool Xtc::generateThumbBmp(int height) const { // Calculate scale factor float scaleX = static_cast(THUMB_TARGET_WIDTH) / pageInfo.width; float scaleY = static_cast(THUMB_TARGET_HEIGHT) / pageInfo.height; - float scale = (scaleX < scaleY) ? scaleX : scaleY; + float scale = (scaleX > scaleY) ? scaleX : scaleY; // for cropping // Only scale down, never up if (scale >= 1.0f) { diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 276eb522..99586b79 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -38,6 +38,19 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title saveToFile(); } +void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, + const std::string& coverBmpPath) { + auto it = + std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); + if (it != recentBooks.end()) { + RecentBook& book = *it; + book.title = title; + book.author = author; + book.coverBmpPath = coverBmpPath; + saveToFile(); + } +} + bool RecentBooksStore::saveToFile() const { // Make sure the directory exists SdMan.mkdir("/.crosspoint"); diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 6f6a164a..8dbf0813 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -27,6 +27,9 @@ class RecentBooksStore { void addBook(const std::string& path, const std::string& title, const std::string& author, const std::string& coverBmpPath); + void updateBook(const std::string& path, const std::string& title, const std::string& author, + const std::string& coverBmpPath); + // Get the list of recent books (most recent first) const std::vector& getBooks() const { return recentBooks; } diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index ef5cc98f..15a59bfc 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -35,16 +35,11 @@ int HomeActivity::getMenuItemCount() const { return count; } -void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { - recentsLoading = true; - bool showingLoading = false; - Rect popupRect; - +void HomeActivity::loadRecentBooks(int maxBooks) { recentBooks.clear(); const auto& books = RECENT_BOOKS.getBooks(); recentBooks.reserve(std::min(static_cast(books.size()), maxBooks)); - int progress = 0; for (const RecentBook& book : books) { // Limit to maximum number of recent books if (recentBooks.size() >= maxBooks) { @@ -56,19 +51,22 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { continue; } + recentBooks.push_back(book); + } +} + +void HomeActivity::loadRecentCovers(int coverHeight) { + recentsLoading = true; + bool showingLoading = false; + Rect popupRect; + + int progress = 0; + for (RecentBook& book : recentBooks) { if (!book.coverBmpPath.empty()) { std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); if (!SdMan.exists(coverPath.c_str())) { - std::string lastBookFileName = ""; - const size_t lastSlash = book.path.find_last_of('/'); - if (lastSlash != std::string::npos) { - lastBookFileName = book.path.substr(lastSlash + 1); - } - - Serial.printf("Loading recent book: %s\n", book.path.c_str()); - // If epub, try to load the metadata for title/author and cover - if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) { + if (StringUtils::checkFileExtension(book.path, ".epub")) { Epub epub(book.path, "/.crosspoint"); // Skip loading css since we only need metadata here epub.load(false, true); @@ -78,10 +76,16 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { showingLoading = true; popupRect = GUI.drawPopup(renderer, "Loading..."); } - GUI.fillPopupProgress(renderer, popupRect, progress * 30); - epub.generateThumbBmp(coverHeight); - } else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") || - StringUtils::checkFileExtension(lastBookFileName, ".xtc")) { + GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); + bool success = epub.generateThumbBmp(coverHeight); + if (!success) { + RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); + book.coverBmpPath = ""; + } + coverRendered = false; + updateRequired = true; + } else if (StringUtils::checkFileExtension(book.path, ".xtch") || + StringUtils::checkFileExtension(book.path, ".xtc")) { // Handle XTC file Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { @@ -90,21 +94,23 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { showingLoading = true; popupRect = GUI.drawPopup(renderer, "Loading..."); } - GUI.fillPopupProgress(renderer, popupRect, progress * 30); - xtc.generateThumbBmp(coverHeight); + GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); + bool success = xtc.generateThumbBmp(coverHeight); + if (!success) { + RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); + book.coverBmpPath = ""; + } + coverRendered = false; + updateRequired = true; } } } } - - recentBooks.push_back(book); progress++; } - Serial.printf("Recent books loaded: %d\n", recentBooks.size()); recentsLoaded = true; recentsLoading = false; - updateRequired = true; } void HomeActivity::onEnter() { @@ -112,14 +118,14 @@ void HomeActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); - // Check if we have a book to continue reading - hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); - // Check if OPDS browser URL is configured hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; selectorIndex = 0; + auto metrics = UITheme::getInstance().getMetrics(); + loadRecentBooks(metrics.homeRecentBooksCount); + // Trigger first update updateRequired = true; @@ -246,24 +252,14 @@ void HomeActivity::render() { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + renderer.clearScreen(); bool bufferRestored = coverBufferStored && restoreCoverBuffer(); - if (!firstRenderDone || (recentsLoaded && !recentsDisplayed)) { - renderer.clearScreen(); - } GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr); - if (hasContinueReading) { - if (recentsLoaded) { - recentsDisplayed = true; - GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, - recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, - std::bind(&HomeActivity::storeCoverBuffer, this)); - } else if (!recentsLoading && firstRenderDone) { - recentsLoading = true; - loadRecentBooks(metrics.homeRecentBooksCount, metrics.homeCoverHeight); - } - } + GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, + recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, + std::bind(&HomeActivity::storeCoverBuffer, this)); // Build menu items dynamically std::vector menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"}; @@ -288,5 +284,8 @@ void HomeActivity::render() { if (!firstRenderDone) { firstRenderDone = true; updateRequired = true; + } else if (!recentsLoaded && !recentsLoading) { + recentsLoading = true; + loadRecentCovers(metrics.homeCoverHeight); } } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index f27a8f93..1f714217 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -17,10 +17,8 @@ class HomeActivity final : public Activity { SemaphoreHandle_t renderingMutex = nullptr; int selectorIndex = 0; bool updateRequired = false; - bool hasContinueReading = false; bool recentsLoading = false; bool recentsLoaded = false; - bool recentsDisplayed = false; bool firstRenderDone = false; bool hasOpdsUrl = false; bool coverRendered = false; // Track if cover has been rendered once @@ -41,7 +39,8 @@ class HomeActivity final : public Activity { bool storeCoverBuffer(); // Store frame buffer for cover image bool restoreCoverBuffer(); // Restore frame buffer from stored cover void freeCoverBuffer(); // Free the stored cover buffer - void loadRecentBooks(int maxBooks, int coverHeight); + void loadRecentBooks(int maxBooks); + void loadRecentCovers(int coverHeight); public: explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 3b23e3fe..2e8ddbc8 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -301,6 +301,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: { // Draw cover image as background if available (inside the box) // Only load from SD on first render, then use stored buffer + if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) { const std::string coverBmpPath = UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight); @@ -310,6 +311,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { + Serial.printf("Rendering bmp\n"); // Calculate position to center image within the book card int coverX, coverY; @@ -343,13 +345,16 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // First render: if selected, draw selection indicators now if (bookSelected) { + Serial.printf("Drawing selection\n"); renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); } } file.close(); } - } else if (!bufferRestored && !coverRendered) { + } + + if (!bufferRestored && !coverRendered) { // No cover image: draw border or fill, plus bookmark as visual flair if (bookSelected) { renderer.fillRect(bookX, bookY, bookWidth, bookHeight); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 71d6a15d..c07e7a5f 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -274,30 +274,37 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: for (int i = 0; i < std::min(static_cast(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); i++) { std::string coverPath = recentBooks[i].coverBmpPath; + bool hasCover = true; + int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; if (coverPath.empty()) { - continue; + hasCover = false; + } else { + const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); + + // First time: load cover from SD and render + FsFile file; + if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { + Bitmap bitmap(file); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + float coverHeight = static_cast(bitmap.getHeight()); + float coverWidth = static_cast(bitmap.getWidth()); + float ratio = coverWidth / coverHeight; + const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / + static_cast(LyraMetrics::values.homeCoverHeight); + float cropX = 1.0f - (tileRatio / ratio); + + renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, + tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); + } else { + hasCover = false; + } + file.close(); + } } - const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); - - int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; - - // First time: load cover from SD and render - FsFile file; - if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { - Bitmap bitmap(file); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - float coverHeight = static_cast(bitmap.getHeight()); - float coverWidth = static_cast(bitmap.getWidth()); - float ratio = coverWidth / coverHeight; - const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / - static_cast(LyraMetrics::values.homeCoverHeight); - float cropX = 1.0f - (tileRatio / ratio); - - renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, - tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); - } - file.close(); + if (!hasCover) { + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, + tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight); } }