A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 **_

authored by

CaptainFrito and committed by
GitHub
bd8132a2 f89ce514

+94 -68
+1 -1
lib/JpegToBmpConverter/JpegToBmpConverter.cpp
··· 567 567 // Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering 568 568 bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, 569 569 int targetMaxHeight) { 570 - return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, false); 570 + return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true); 571 571 }
+1 -1
lib/Xtc/Xtc.cpp
··· 340 340 // Calculate scale factor 341 341 float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width; 342 342 float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height; 343 - float scale = (scaleX < scaleY) ? scaleX : scaleY; 343 + float scale = (scaleX > scaleY) ? scaleX : scaleY; // for cropping 344 344 345 345 // Only scale down, never up 346 346 if (scale >= 1.0f) {
+13
src/RecentBooksStore.cpp
··· 38 38 saveToFile(); 39 39 } 40 40 41 + void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, 42 + const std::string& coverBmpPath) { 43 + auto it = 44 + std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); 45 + if (it != recentBooks.end()) { 46 + RecentBook& book = *it; 47 + book.title = title; 48 + book.author = author; 49 + book.coverBmpPath = coverBmpPath; 50 + saveToFile(); 51 + } 52 + } 53 + 41 54 bool RecentBooksStore::saveToFile() const { 42 55 // Make sure the directory exists 43 56 SdMan.mkdir("/.crosspoint");
+3
src/RecentBooksStore.h
··· 27 27 void addBook(const std::string& path, const std::string& title, const std::string& author, 28 28 const std::string& coverBmpPath); 29 29 30 + void updateBook(const std::string& path, const std::string& title, const std::string& author, 31 + const std::string& coverBmpPath); 32 + 30 33 // Get the list of recent books (most recent first) 31 34 const std::vector<RecentBook>& getBooks() const { return recentBooks; } 32 35
+41 -42
src/activities/home/HomeActivity.cpp
··· 35 35 return count; 36 36 } 37 37 38 - void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { 39 - recentsLoading = true; 40 - bool showingLoading = false; 41 - Rect popupRect; 42 - 38 + void HomeActivity::loadRecentBooks(int maxBooks) { 43 39 recentBooks.clear(); 44 40 const auto& books = RECENT_BOOKS.getBooks(); 45 41 recentBooks.reserve(std::min(static_cast<int>(books.size()), maxBooks)); 46 42 47 - int progress = 0; 48 43 for (const RecentBook& book : books) { 49 44 // Limit to maximum number of recent books 50 45 if (recentBooks.size() >= maxBooks) { ··· 56 51 continue; 57 52 } 58 53 54 + recentBooks.push_back(book); 55 + } 56 + } 57 + 58 + void HomeActivity::loadRecentCovers(int coverHeight) { 59 + recentsLoading = true; 60 + bool showingLoading = false; 61 + Rect popupRect; 62 + 63 + int progress = 0; 64 + for (RecentBook& book : recentBooks) { 59 65 if (!book.coverBmpPath.empty()) { 60 66 std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); 61 67 if (!SdMan.exists(coverPath.c_str())) { 62 - std::string lastBookFileName = ""; 63 - const size_t lastSlash = book.path.find_last_of('/'); 64 - if (lastSlash != std::string::npos) { 65 - lastBookFileName = book.path.substr(lastSlash + 1); 66 - } 67 - 68 - Serial.printf("Loading recent book: %s\n", book.path.c_str()); 69 - 70 68 // If epub, try to load the metadata for title/author and cover 71 - if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) { 69 + if (StringUtils::checkFileExtension(book.path, ".epub")) { 72 70 Epub epub(book.path, "/.crosspoint"); 73 71 // Skip loading css since we only need metadata here 74 72 epub.load(false, true); ··· 78 76 showingLoading = true; 79 77 popupRect = GUI.drawPopup(renderer, "Loading..."); 80 78 } 81 - GUI.fillPopupProgress(renderer, popupRect, progress * 30); 82 - epub.generateThumbBmp(coverHeight); 83 - } else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") || 84 - StringUtils::checkFileExtension(lastBookFileName, ".xtc")) { 79 + GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); 80 + bool success = epub.generateThumbBmp(coverHeight); 81 + if (!success) { 82 + RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); 83 + book.coverBmpPath = ""; 84 + } 85 + coverRendered = false; 86 + updateRequired = true; 87 + } else if (StringUtils::checkFileExtension(book.path, ".xtch") || 88 + StringUtils::checkFileExtension(book.path, ".xtc")) { 85 89 // Handle XTC file 86 90 Xtc xtc(book.path, "/.crosspoint"); 87 91 if (xtc.load()) { ··· 90 94 showingLoading = true; 91 95 popupRect = GUI.drawPopup(renderer, "Loading..."); 92 96 } 93 - GUI.fillPopupProgress(renderer, popupRect, progress * 30); 94 - xtc.generateThumbBmp(coverHeight); 97 + GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); 98 + bool success = xtc.generateThumbBmp(coverHeight); 99 + if (!success) { 100 + RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); 101 + book.coverBmpPath = ""; 102 + } 103 + coverRendered = false; 104 + updateRequired = true; 95 105 } 96 106 } 97 107 } 98 108 } 99 - 100 - recentBooks.push_back(book); 101 109 progress++; 102 110 } 103 111 104 - Serial.printf("Recent books loaded: %d\n", recentBooks.size()); 105 112 recentsLoaded = true; 106 113 recentsLoading = false; 107 - updateRequired = true; 108 114 } 109 115 110 116 void HomeActivity::onEnter() { 111 117 Activity::onEnter(); 112 118 113 119 renderingMutex = xSemaphoreCreateMutex(); 114 - 115 - // Check if we have a book to continue reading 116 - hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); 117 120 118 121 // Check if OPDS browser URL is configured 119 122 hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; 120 123 121 124 selectorIndex = 0; 125 + 126 + auto metrics = UITheme::getInstance().getMetrics(); 127 + loadRecentBooks(metrics.homeRecentBooksCount); 122 128 123 129 // Trigger first update 124 130 updateRequired = true; ··· 246 252 const auto pageWidth = renderer.getScreenWidth(); 247 253 const auto pageHeight = renderer.getScreenHeight(); 248 254 255 + renderer.clearScreen(); 249 256 bool bufferRestored = coverBufferStored && restoreCoverBuffer(); 250 - if (!firstRenderDone || (recentsLoaded && !recentsDisplayed)) { 251 - renderer.clearScreen(); 252 - } 253 257 254 258 GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr); 255 259 256 - if (hasContinueReading) { 257 - if (recentsLoaded) { 258 - recentsDisplayed = true; 259 - GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, 260 - recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, 261 - std::bind(&HomeActivity::storeCoverBuffer, this)); 262 - } else if (!recentsLoading && firstRenderDone) { 263 - recentsLoading = true; 264 - loadRecentBooks(metrics.homeRecentBooksCount, metrics.homeCoverHeight); 265 - } 266 - } 260 + GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, 261 + recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, 262 + std::bind(&HomeActivity::storeCoverBuffer, this)); 267 263 268 264 // Build menu items dynamically 269 265 std::vector<const char*> menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"}; ··· 288 284 if (!firstRenderDone) { 289 285 firstRenderDone = true; 290 286 updateRequired = true; 287 + } else if (!recentsLoaded && !recentsLoading) { 288 + recentsLoading = true; 289 + loadRecentCovers(metrics.homeCoverHeight); 291 290 } 292 291 }
+2 -3
src/activities/home/HomeActivity.h
··· 17 17 SemaphoreHandle_t renderingMutex = nullptr; 18 18 int selectorIndex = 0; 19 19 bool updateRequired = false; 20 - bool hasContinueReading = false; 21 20 bool recentsLoading = false; 22 21 bool recentsLoaded = false; 23 - bool recentsDisplayed = false; 24 22 bool firstRenderDone = false; 25 23 bool hasOpdsUrl = false; 26 24 bool coverRendered = false; // Track if cover has been rendered once ··· 41 39 bool storeCoverBuffer(); // Store frame buffer for cover image 42 40 bool restoreCoverBuffer(); // Restore frame buffer from stored cover 43 41 void freeCoverBuffer(); // Free the stored cover buffer 44 - void loadRecentBooks(int maxBooks, int coverHeight); 42 + void loadRecentBooks(int maxBooks); 43 + void loadRecentCovers(int coverHeight); 45 44 46 45 public: 47 46 explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
+6 -1
src/components/themes/BaseTheme.cpp
··· 301 301 { 302 302 // Draw cover image as background if available (inside the box) 303 303 // Only load from SD on first render, then use stored buffer 304 + 304 305 if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) { 305 306 const std::string coverBmpPath = 306 307 UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight); ··· 310 311 if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { 311 312 Bitmap bitmap(file); 312 313 if (bitmap.parseHeaders() == BmpReaderError::Ok) { 314 + Serial.printf("Rendering bmp\n"); 313 315 // Calculate position to center image within the book card 314 316 int coverX, coverY; 315 317 ··· 343 345 344 346 // First render: if selected, draw selection indicators now 345 347 if (bookSelected) { 348 + Serial.printf("Drawing selection\n"); 346 349 renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); 347 350 renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); 348 351 } 349 352 } 350 353 file.close(); 351 354 } 352 - } else if (!bufferRestored && !coverRendered) { 355 + } 356 + 357 + if (!bufferRestored && !coverRendered) { 353 358 // No cover image: draw border or fill, plus bookmark as visual flair 354 359 if (bookSelected) { 355 360 renderer.fillRect(bookX, bookY, bookWidth, bookHeight);
+27 -20
src/components/themes/lyra/LyraTheme.cpp
··· 274 274 for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); 275 275 i++) { 276 276 std::string coverPath = recentBooks[i].coverBmpPath; 277 + bool hasCover = true; 278 + int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; 277 279 if (coverPath.empty()) { 278 - continue; 279 - } 280 + hasCover = false; 281 + } else { 282 + const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); 280 283 281 - const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); 284 + // First time: load cover from SD and render 285 + FsFile file; 286 + if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { 287 + Bitmap bitmap(file); 288 + if (bitmap.parseHeaders() == BmpReaderError::Ok) { 289 + float coverHeight = static_cast<float>(bitmap.getHeight()); 290 + float coverWidth = static_cast<float>(bitmap.getWidth()); 291 + float ratio = coverWidth / coverHeight; 292 + const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) / 293 + static_cast<float>(LyraMetrics::values.homeCoverHeight); 294 + float cropX = 1.0f - (tileRatio / ratio); 282 295 283 - int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; 296 + renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, 297 + tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); 298 + } else { 299 + hasCover = false; 300 + } 301 + file.close(); 302 + } 303 + } 284 304 285 - // First time: load cover from SD and render 286 - FsFile file; 287 - if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { 288 - Bitmap bitmap(file); 289 - if (bitmap.parseHeaders() == BmpReaderError::Ok) { 290 - float coverHeight = static_cast<float>(bitmap.getHeight()); 291 - float coverWidth = static_cast<float>(bitmap.getWidth()); 292 - float ratio = coverWidth / coverHeight; 293 - const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) / 294 - static_cast<float>(LyraMetrics::values.homeCoverHeight); 295 - float cropX = 1.0f - (tileRatio / ratio); 296 - 297 - renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, 298 - tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); 299 - } 300 - file.close(); 305 + if (!hasCover) { 306 + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, 307 + tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight); 301 308 } 302 309 } 303 310