diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 5a3edc4c..a37aaeea 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -377,6 +377,7 @@ void RecentBooksActivity::renderGridCell(int index, bool selected, int cellX, in if (!book.coverBmpPath.empty()) { const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); FsFile file; + bool thumbDrawn = false; if (Storage.openFileForRead("RBA", thumbPath, file)) { Bitmap bmp(file); if (bmp.parseHeaders() == BmpReaderError::Ok) { @@ -399,10 +400,19 @@ void RecentBooksActivity::renderGridCell(int index, bool selected, int cellX, in // shows through in the surrounding space. renderer.fillRect(cellX + offsetX, cellY + offsetY, rendW, rendH, false); renderer.drawBitmap1Bit(bmp, cellX + offsetX, cellY + offsetY, rendW, rendH); + thumbDrawn = true; } } file.close(); } + if (!thumbDrawn) { + // Thumbnail not yet generated — clear interior and show loading label + renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); + const char* loadingText = tr(STR_LOADING); + const int textW = renderer.getTextWidth(SMALL_FONT_ID, loadingText); + const int textH = renderer.getLineHeight(SMALL_FONT_ID); + renderer.drawText(SMALL_FONT_ID, cellX + (tw - textW) / 2, cellY + (th - textH) / 2, loadingText, true); + } } else { // No cover — clear the whole interior so the placeholder looks clean. renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 06f5f230..7fa25e8f 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -32,10 +33,12 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con // Only load from SD on first render, then use stored buffer if (hasContinueReading) { if (!coverRendered) { + bool anyPending = false; for (int i = 0; i < std::min(static_cast(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); i++) { std::string coverPath = recentBooks[i].coverBmpPath; bool hasCover = true; + bool tilePending = false; int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i; if (coverPath.empty()) { hasCover = false; @@ -62,6 +65,8 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con file.close(); } else { hasCover = false; + tilePending = true; // path exists but BMP not ready yet + anyPending = true; } } // Draw either way @@ -69,15 +74,29 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con coverHeight, true); if (!hasCover) { - // Render empty cover - renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection + (coverHeight / 3), - tileWidth - 2 * hPaddingInSelection, 2 * coverHeight / 3, true); - renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32); + if (tilePending) { + // Cover is being generated — show a loading label centred in the tile + const char* loadingText = tr(STR_LOADING); + const int textW = renderer.getTextWidth(SMALL_FONT_ID, loadingText); + const int textH = renderer.getLineHeight(SMALL_FONT_ID); + renderer.drawText(SMALL_FONT_ID, + tileX + hPaddingInSelection + (tileWidth - 2 * hPaddingInSelection - textW) / 2, + tileY + hPaddingInSelection + (coverHeight - textH) / 2, loadingText, true); + } else { + // No cover at all — render empty cover placeholder + renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection + (coverHeight / 3), + tileWidth - 2 * hPaddingInSelection, 2 * coverHeight / 3, true); + renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32); + } } } - coverBufferStored = storeCoverBuffer(); - coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer + // Only cache the frame buffer once all tiles are definitively resolved. + // If any cover is still being generated we keep coverRendered=false so the next render will retry. + if (!anyPending) { + coverBufferStored = storeCoverBuffer(); + coverRendered = coverBufferStored; + } } for (int i = 0; i < std::min(static_cast(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); diff --git a/src/components/themes/lyra/LyraCarouselTheme.cpp b/src/components/themes/lyra/LyraCarouselTheme.cpp index 771142b0..ba7bfe93 100644 --- a/src/components/themes/lyra/LyraCarouselTheme.cpp +++ b/src/components/themes/lyra/LyraCarouselTheme.cpp @@ -317,6 +317,8 @@ void LyraCarouselTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, // Returns true if a book exists at bookIdx (cover image or placeholder drawn). // Returns false only when the slot has no book — caller skips the border too. + // anyPending is set to true when a book has a coverBmpPath but the BMP isn't readable yet. + bool anyPending = false; auto drawCover = [&](int bookIdx, int x, int y, int maxW, int maxH) -> bool { if (bookIdx < 0 || bookIdx >= bookCount) return false; const RecentBook& book = recentBooks[bookIdx]; @@ -324,6 +326,7 @@ void LyraCarouselTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const bool roundLeft = (x >= 0); const bool roundRight = (x + maxW <= screenW); bool hasCover = false; + bool tilePending = false; if (!book.coverBmpPath.empty()) { const std::string thumbPath = UITheme::getCoverThumbPath(book.coverBmpPath, maxW, maxH); FsFile file; @@ -361,14 +364,25 @@ void LyraCarouselTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, hasCover = true; } file.close(); + } else { + tilePending = true; // path exists but BMP not ready yet + anyPending = true; } } if (!hasCover) { renderer.drawRoundedRect(x, y, maxW, maxH, 1, kCornerRadius, roundLeft, roundRight, roundLeft, roundRight, true); - renderer.fillRoundedRect(x, y + maxH / 3, maxW, 2 * maxH / 3, kCornerRadius, /*roundTopLeft=*/false, - /*roundTopRight=*/false, /*roundBottomLeft=*/roundLeft, /*roundBottomRight=*/roundRight, - Color::Black); - renderer.drawIcon(CoverIcon, x + maxW / 2 - 16, y + 8, 32, 32); + if (tilePending) { + // Cover is being generated — show a loading label centred in the tile + const char* loadingText = tr(STR_LOADING); + const int textW = renderer.getTextWidth(SMALL_FONT_ID, loadingText); + const int textH = renderer.getLineHeight(SMALL_FONT_ID); + renderer.drawText(SMALL_FONT_ID, x + (maxW - textW) / 2, y + (maxH - textH) / 2, loadingText, true); + } else { + renderer.fillRoundedRect(x, y + maxH / 3, maxW, 2 * maxH / 3, kCornerRadius, /*roundTopLeft=*/false, + /*roundTopRight=*/false, /*roundBottomLeft=*/roundLeft, + /*roundBottomRight=*/roundRight, Color::Black); + renderer.drawIcon(CoverIcon, x + maxW / 2 - 16, y + 8, 32, 32); + } } return true; }; @@ -437,8 +451,12 @@ void LyraCarouselTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const int titleW = renderer.getTextWidth(kTitleFontId, titleTrunc.c_str()); renderer.drawText(kTitleFontId, centerX + (kCenterCoverMaxW - titleW) / 2, titleY, titleTrunc.c_str(), true); - coverBufferStored = storeCoverBuffer(); - coverRendered = coverBufferStored; + // Only cache the frame buffer once all tiles are definitively resolved. + // If any cover is still being generated we keep coverRendered=false so the next render will retry. + if (!anyPending) { + coverBufferStored = storeCoverBuffer(); + coverRendered = coverBufferStored; + } } // Always outline the centre cover at its own edge (white ring sits outside the black line); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 9ee2f037..c233137d 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -554,6 +554,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (!coverRendered) { std::string coverPath = book.coverBmpPath; bool hasCover = true; + bool coverPending = false; int tileX = LyraMetrics::values.contentSidePadding; if (coverPath.empty()) { hasCover = false; @@ -572,6 +573,9 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: hasCover = false; } file.close(); + } else { + hasCover = false; + coverPending = true; // path exists but BMP not ready yet } } @@ -579,14 +583,27 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, coverHeight, true); if (!hasCover) { - // Render empty cover - renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection + (coverHeight / 3), coverWidth, - 2 * coverHeight / 3, true); - renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32); + if (coverPending) { + // Cover is being generated — show a loading label centred in the cover area + const char* loadingText = tr(STR_LOADING); + const int textW = renderer.getTextWidth(SMALL_FONT_ID, loadingText); + const int textH = renderer.getLineHeight(SMALL_FONT_ID); + renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection + (coverWidth - textW) / 2, + tileY + hPaddingInSelection + (coverHeight - textH) / 2, loadingText, true); + } else { + // No cover at all — render empty cover placeholder + renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection + (coverHeight / 3), coverWidth, + 2 * coverHeight / 3, true); + renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32); + } } - coverBufferStored = storeCoverBuffer(); - coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer + // Only cache the frame buffer once the cover is definitively resolved (loaded or confirmed absent). + // If a cover is still being generated we keep coverRendered=false so the next render will retry. + if (!coverPending) { + coverBufferStored = storeCoverBuffer(); + coverRendered = coverBufferStored; + } } bool bookSelected = (selectorIndex == 0);