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
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);
}

View File

@ -340,7 +340,7 @@ bool Xtc::generateThumbBmp(int height) const {
// Calculate scale factor
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
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
if (scale >= 1.0f) {

View File

@ -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");

View File

@ -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<RecentBook>& getBooks() const { return recentBooks; }

View File

@ -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<int>(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<const char*> 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);
}
}

View File

@ -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,

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)
// 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);

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);
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<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);
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<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();
if (!hasCover) {
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection,
tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight);
}
}