diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 837bfd27..0b1541b4 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -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); } diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 05f6651d..717bb670 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -340,7 +340,7 @@ bool Xtc::generateThumbBmp(int height) const { // Calculate scale factor float scaleX = static_cast(THUMB_TARGET_WIDTH) / pageInfo.width; float scaleY = static_cast(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) { diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 276eb522..99586b79 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -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"); diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 6f6a164a..8dbf0813 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -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& getBooks() const { return recentBooks; } diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index ef5cc98f..15a59bfc 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -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(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 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); } } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index f27a8f93..1f714217 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -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, diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 3b23e3fe..2e8ddbc8 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -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); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 71d6a15d..c07e7a5f 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -274,30 +274,37 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: for (int i = 0; i < std::min(static_cast(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(bitmap.getHeight()); + float coverWidth = static_cast(bitmap.getWidth()); + float ratio = coverWidth / coverHeight; + const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / + static_cast(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(bitmap.getHeight()); - float coverWidth = static_cast(bitmap.getWidth()); - float ratio = coverWidth / coverHeight; - const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / - static_cast(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); } }