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 **_
This commit is contained in:
CaptainFrito 2026-02-06 14:58:32 +07:00 committed by GitHub
parent f89ce514c8
commit bd8132a260
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 95 additions and 69 deletions

View File

@ -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 // 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, bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth,
int targetMaxHeight) { int targetMaxHeight) {
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, false); return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
} }

View File

@ -340,7 +340,7 @@ bool Xtc::generateThumbBmp(int height) const {
// Calculate scale factor // Calculate scale factor
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width; float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height; float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
float scale = (scaleX < scaleY) ? scaleX : scaleY; float scale = (scaleX > scaleY) ? scaleX : scaleY; // for cropping
// Only scale down, never up // Only scale down, never up
if (scale >= 1.0f) { if (scale >= 1.0f) {

View File

@ -38,6 +38,19 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
saveToFile(); 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 { bool RecentBooksStore::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists
SdMan.mkdir("/.crosspoint"); SdMan.mkdir("/.crosspoint");

View File

@ -27,6 +27,9 @@ class RecentBooksStore {
void addBook(const std::string& path, const std::string& title, const std::string& author, void addBook(const std::string& path, const std::string& title, const std::string& author,
const std::string& coverBmpPath); 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) // Get the list of recent books (most recent first)
const std::vector<RecentBook>& getBooks() const { return recentBooks; } const std::vector<RecentBook>& getBooks() const { return recentBooks; }

View File

@ -35,16 +35,11 @@ int HomeActivity::getMenuItemCount() const {
return count; return count;
} }
void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { void HomeActivity::loadRecentBooks(int maxBooks) {
recentsLoading = true;
bool showingLoading = false;
Rect popupRect;
recentBooks.clear(); recentBooks.clear();
const auto& books = RECENT_BOOKS.getBooks(); const auto& books = RECENT_BOOKS.getBooks();
recentBooks.reserve(std::min(static_cast<int>(books.size()), maxBooks)); recentBooks.reserve(std::min(static_cast<int>(books.size()), maxBooks));
int progress = 0;
for (const RecentBook& book : books) { for (const RecentBook& book : books) {
// Limit to maximum number of recent books // Limit to maximum number of recent books
if (recentBooks.size() >= maxBooks) { if (recentBooks.size() >= maxBooks) {
@ -56,19 +51,22 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) {
continue; 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()) { if (!book.coverBmpPath.empty()) {
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
if (!SdMan.exists(coverPath.c_str())) { 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 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"); Epub epub(book.path, "/.crosspoint");
// Skip loading css since we only need metadata here // Skip loading css since we only need metadata here
epub.load(false, true); epub.load(false, true);
@ -78,10 +76,16 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) {
showingLoading = true; showingLoading = true;
popupRect = GUI.drawPopup(renderer, "Loading..."); popupRect = GUI.drawPopup(renderer, "Loading...");
} }
GUI.fillPopupProgress(renderer, popupRect, progress * 30); GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
epub.generateThumbBmp(coverHeight); bool success = epub.generateThumbBmp(coverHeight);
} else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") || if (!success) {
StringUtils::checkFileExtension(lastBookFileName, ".xtc")) { 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 // Handle XTC file
Xtc xtc(book.path, "/.crosspoint"); Xtc xtc(book.path, "/.crosspoint");
if (xtc.load()) { if (xtc.load()) {
@ -90,21 +94,23 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) {
showingLoading = true; showingLoading = true;
popupRect = GUI.drawPopup(renderer, "Loading..."); popupRect = GUI.drawPopup(renderer, "Loading...");
} }
GUI.fillPopupProgress(renderer, popupRect, progress * 30); GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
xtc.generateThumbBmp(coverHeight); 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++; progress++;
} }
Serial.printf("Recent books loaded: %d\n", recentBooks.size());
recentsLoaded = true; recentsLoaded = true;
recentsLoading = false; recentsLoading = false;
updateRequired = true;
} }
void HomeActivity::onEnter() { void HomeActivity::onEnter() {
@ -112,14 +118,14 @@ void HomeActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); 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 // Check if OPDS browser URL is configured
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
selectorIndex = 0; selectorIndex = 0;
auto metrics = UITheme::getInstance().getMetrics();
loadRecentBooks(metrics.homeRecentBooksCount);
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
@ -246,24 +252,14 @@ void HomeActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
bool bufferRestored = coverBufferStored && restoreCoverBuffer(); bool bufferRestored = coverBufferStored && restoreCoverBuffer();
if (!firstRenderDone || (recentsLoaded && !recentsDisplayed)) {
renderer.clearScreen();
}
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr);
if (hasContinueReading) { GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight},
if (recentsLoaded) { recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored,
recentsDisplayed = true; std::bind(&HomeActivity::storeCoverBuffer, this));
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);
}
}
// Build menu items dynamically // Build menu items dynamically
std::vector<const char*> menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"}; std::vector<const char*> menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"};
@ -288,5 +284,8 @@ void HomeActivity::render() {
if (!firstRenderDone) { if (!firstRenderDone) {
firstRenderDone = true; firstRenderDone = true;
updateRequired = true; updateRequired = true;
} else if (!recentsLoaded && !recentsLoading) {
recentsLoading = true;
loadRecentCovers(metrics.homeCoverHeight);
} }
} }

View File

@ -17,10 +17,8 @@ class HomeActivity final : public Activity {
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false; bool updateRequired = false;
bool hasContinueReading = false;
bool recentsLoading = false; bool recentsLoading = false;
bool recentsLoaded = false; bool recentsLoaded = false;
bool recentsDisplayed = false;
bool firstRenderDone = false; bool firstRenderDone = false;
bool hasOpdsUrl = false; bool hasOpdsUrl = false;
bool coverRendered = false; // Track if cover has been rendered once 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 storeCoverBuffer(); // Store frame buffer for cover image
bool restoreCoverBuffer(); // Restore frame buffer from stored cover bool restoreCoverBuffer(); // Restore frame buffer from stored cover
void freeCoverBuffer(); // Free the stored cover buffer void freeCoverBuffer(); // Free the stored cover buffer
void loadRecentBooks(int maxBooks, int coverHeight); void loadRecentBooks(int maxBooks);
void loadRecentCovers(int coverHeight);
public: public:
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,

View File

@ -301,6 +301,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
{ {
// Draw cover image as background if available (inside the box) // Draw cover image as background if available (inside the box)
// Only load from SD on first render, then use stored buffer // Only load from SD on first render, then use stored buffer
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) { if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) {
const std::string coverBmpPath = const std::string coverBmpPath =
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight); 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)) { if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
Bitmap bitmap(file); Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
Serial.printf("Rendering bmp\n");
// Calculate position to center image within the book card // Calculate position to center image within the book card
int coverX, coverY; int coverX, coverY;
@ -343,13 +345,16 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
// First render: if selected, draw selection indicators now // First render: if selected, draw selection indicators now
if (bookSelected) { if (bookSelected) {
Serial.printf("Drawing selection\n");
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
} }
} }
file.close(); file.close();
} }
} else if (!bufferRestored && !coverRendered) { }
if (!bufferRestored && !coverRendered) {
// No cover image: draw border or fill, plus bookmark as visual flair // No cover image: draw border or fill, plus bookmark as visual flair
if (bookSelected) { if (bookSelected) {
renderer.fillRect(bookX, bookY, bookWidth, bookHeight); renderer.fillRect(bookX, bookY, bookWidth, bookHeight);

View File

@ -274,30 +274,37 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount);
i++) { i++) {
std::string coverPath = recentBooks[i].coverBmpPath; std::string coverPath = recentBooks[i].coverBmpPath;
bool hasCover = true;
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
if (coverPath.empty()) { 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<float>(bitmap.getHeight());
float coverWidth = static_cast<float>(bitmap.getWidth());
float ratio = coverWidth / coverHeight;
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
static_cast<float>(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); if (!hasCover) {
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection,
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; tileWidth - 2 * hPaddingInSelection, 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<float>(bitmap.getHeight());
float coverWidth = static_cast<float>(bitmap.getWidth());
float ratio = coverWidth / coverHeight;
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
static_cast<float>(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();
} }
} }