mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 16:17:38 +03:00
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:
parent
f89ce514c8
commit
bd8132a260
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
bool bufferRestored = coverBufferStored && restoreCoverBuffer();
|
|
||||||
if (!firstRenderDone || (recentsLoaded && !recentsDisplayed)) {
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
}
|
bool bufferRestored = coverBufferStored && restoreCoverBuffer();
|
||||||
|
|
||||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr);
|
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},
|
GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight},
|
||||||
recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored,
|
recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored,
|
||||||
std::bind(&HomeActivity::storeCoverBuffer, this));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -274,13 +274,12 @@ 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;
|
||||||
if (coverPath.empty()) {
|
bool hasCover = true;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight);
|
|
||||||
|
|
||||||
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
||||||
|
if (coverPath.empty()) {
|
||||||
|
hasCover = false;
|
||||||
|
} else {
|
||||||
|
const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight);
|
||||||
|
|
||||||
// First time: load cover from SD and render
|
// First time: load cover from SD and render
|
||||||
FsFile file;
|
FsFile file;
|
||||||
@ -296,11 +295,19 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
|
|
||||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX);
|
tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX);
|
||||||
|
} else {
|
||||||
|
hasCover = false;
|
||||||
}
|
}
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasCover) {
|
||||||
|
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
|
tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
coverBufferStored = storeCoverBuffer();
|
coverBufferStored = storeCoverBuffer();
|
||||||
coverRendered = true;
|
coverRendered = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user