From da9b3fe60d3aa9f8be26a39435756ab021c40777 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 13 May 2026 22:42:11 +0200 Subject: [PATCH 1/4] Add 3x3 grid view --- lib/I18n/translations/belarusian.yaml | 3 + lib/I18n/translations/catalan.yaml | 3 + lib/I18n/translations/czech.yaml | 3 + lib/I18n/translations/danish.yaml | 3 + lib/I18n/translations/dutch.yaml | 3 + lib/I18n/translations/english.yaml | 4 +- lib/I18n/translations/finnish.yaml | 3 + lib/I18n/translations/french.yaml | 3 + lib/I18n/translations/german.yaml | 3 + lib/I18n/translations/hungarian.yaml | 3 + lib/I18n/translations/italian.yaml | 3 + lib/I18n/translations/kazakh.yaml | 3 + lib/I18n/translations/lithuanian.yaml | 3 + lib/I18n/translations/polish.yaml | 3 + lib/I18n/translations/portuguese_br.yaml | 3 + lib/I18n/translations/portuguese_pt.yaml | 3 + lib/I18n/translations/romanian.yaml | 3 + lib/I18n/translations/russian.yaml | 3 + lib/I18n/translations/slovenian.yaml | 3 + lib/I18n/translations/spanish.yaml | 3 + lib/I18n/translations/swedish.yaml | 3 + lib/I18n/translations/turkish.yaml | 3 + lib/I18n/translations/ukrainian.yaml | 3 + lib/I18n/translations/vietnamese.yaml | 3 + src/CrossPointState.h | 1 + src/JsonSettingsIO.cpp | 2 + src/activities/home/RecentBooksActivity.cpp | 398 +++++++++++++++++--- src/activities/home/RecentBooksActivity.h | 20 +- 28 files changed, 439 insertions(+), 55 deletions(-) diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 56cb0281..d21e6c24 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -437,3 +437,6 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак" STR_LINK: "[спасылка]" STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: " STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 01c0d705..ef17dc7d 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -283,3 +283,6 @@ STR_LINK: "[enllaç]" STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla" STR_AUTO_TURN_ENABLED: "Passar automàtic activat: " STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 5da37a60..a7d6833c 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -254,3 +254,6 @@ STR_BOOK_S_STYLE: "Styl knihy" STR_EMBEDDED_STYLE: "Vložený styl" STR_OPDS_SERVER_URL: "URL serveru OPDS" STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 9e540dca..c87dfb3f 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -283,3 +283,6 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Tag skærmbillede" STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: " STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index db303031..2674c235 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -646,3 +646,6 @@ STR_KB_HINT_CLEAR_TEXT: "Houd DEL ingedrukt om alle tekst te wissen" STR_KB_HINT_SECONDARY_CHAR: "Houd SELECT ingedrukt voor secundair teken" STR_KB_HINT_UPPER_SECONDARY: "Houd SELECT ingedrukt voor HOOFDLETTERS of secundair teken" STR_KB_HINT_LOWER_SECONDARY: "Houd SELECT ingedrukt voor kleine letters of secundair teken" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index f7eae047..51eca17f 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -661,4 +661,6 @@ STR_FIRMWARE_FILE_OPEN_FAILED: "Cannot open file" STR_FIRMWARE_WRITE_FAILED: "Firmware write failed" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Do not power off!" STR_RECOVERY_MODE: "Recovery Mode" -STR_RECOVERY_MODE_HINT: "Place firmware.bin on SD card root and select it" \ No newline at end of file +STR_RECOVERY_MODE_HINT: "Place firmware.bin on SD card root and select it" +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" \ No newline at end of file diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 50923526..3acbc0ec 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -253,3 +253,6 @@ STR_BOOK_S_STYLE: "Kirjan tyyli" STR_EMBEDDED_STYLE: "Upotettu tyyli" STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite" STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 3c7bf107..8c8766c6 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -648,3 +648,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Basculer la lecture bionique" STR_BTN_ACT_CYCLE_FONT_SIZE: "Changer la taille de police" STR_BTN_ACT_CYCLE_ORIENTATION: "Changer l'orientation" STR_RESTART_DEVICE: "Redémarrer l'appareil" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index e77b7caa..cd3aa3a5 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -648,3 +648,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Bionisches Lesen umschalten" STR_BTN_ACT_CYCLE_FONT_SIZE: "Schriftgröße ändern" STR_BTN_ACT_CYCLE_ORIENTATION: "Ändere Ausrichtung" STR_RESTART_DEVICE: "Neustart des Geräts" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index a3090ed3..a65c6270 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -283,3 +283,6 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Képernyőkép készítése" STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: " STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index a09428a1..bfe3a412 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -648,3 +648,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Attiva/disattiva lettura bionica" STR_BTN_ACT_CYCLE_FONT_SIZE: "Modificare dimensione carattere" STR_BTN_ACT_CYCLE_ORIENTATION: "Cambia orientamento" STR_RESTART_DEVICE: "Riavvia dispositivo" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index f6d87b8a..4257680b 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -282,3 +282,6 @@ STR_LINK: "[сілтеме]" STR_SCREENSHOT_BUTTON: "Скриншот түсіру" STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: " STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 29c457db..02199537 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -283,3 +283,6 @@ STR_LINK: "[nuoroda]" STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka" STR_AUTO_TURN_ENABLED: "Automatinis puslapių vartymas: " STR_AUTO_TURN_PAGES_PER_MIN: "Automatinis puslapių vartymas (psl./min.)" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index ee56de48..69be6d02 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -663,3 +663,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Przełącz bioniczne czytanie" STR_BTN_ACT_CYCLE_FONT_SIZE: "Zmień rozmiar czcionki" STR_BTN_ACT_CYCLE_ORIENTATION: "Zmień orientację" STR_RESTART_DEVICE: "Uruchom ponownie urządzenie" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/portuguese_br.yaml b/lib/I18n/translations/portuguese_br.yaml index aa1ceea8..dd442710 100644 --- a/lib/I18n/translations/portuguese_br.yaml +++ b/lib/I18n/translations/portuguese_br.yaml @@ -647,3 +647,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Alternar leitura biónica" STR_BTN_ACT_CYCLE_FONT_SIZE: "Alterar tamanho da fonte" STR_BTN_ACT_CYCLE_ORIENTATION: "Alterar orientação" STR_RESTART_DEVICE: "Reiniciar dispositivo" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/portuguese_pt.yaml b/lib/I18n/translations/portuguese_pt.yaml index b9862f93..243f119e 100644 --- a/lib/I18n/translations/portuguese_pt.yaml +++ b/lib/I18n/translations/portuguese_pt.yaml @@ -646,3 +646,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Alternar leitura biónica" STR_BTN_ACT_CYCLE_FONT_SIZE: "Alterar tamanho da fonte" STR_BTN_ACT_CYCLE_ORIENTATION: "Alterar orientação" STR_RESTART_DEVICE: "Reiniciar dispositivo" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 4d935023..e411b0a4 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -283,3 +283,6 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Captură ecran" STR_AUTO_TURN_ENABLED: "Răsfoire automată: " STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 57ab0c49..f36a8814 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -647,3 +647,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Переключить бионическо STR_BTN_ACT_CYCLE_FONT_SIZE: "Изменить размер шрифта" STR_BTN_ACT_CYCLE_ORIENTATION: "Изменить ориентацию" STR_RESTART_DEVICE: "Перезагрузить устройство" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index 81862896..1454a0e8 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -646,3 +646,6 @@ STR_KB_HINT_CLEAR_TEXT: "Drži DEL za izbris celotnega besedila" STR_KB_HINT_SECONDARY_CHAR: "Drži SELECT za sekundarni znak" STR_KB_HINT_UPPER_SECONDARY: "Drži SELECT za VELIKE ČRKE ali sek. znak" STR_KB_HINT_LOWER_SECONDARY: "Drži SELECT za male črke ali sek. znak" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index ee38f4a6..436f6e30 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -647,3 +647,6 @@ STR_BTN_ACT_TOGGLE_BIONIC_READING: "Alternar lectura biónica" STR_BTN_ACT_CYCLE_FONT_SIZE: "Cambiar tamaño de fuente" STR_BTN_ACT_CYCLE_ORIENTATION: "Cambiar orientación" STR_RESTART_DEVICE: "Reiniciar dispositivo" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 88819e98..4c7be7ad 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -646,3 +646,6 @@ STR_KB_HINT_CLEAR_TEXT: "Håll DEL för att rensa all text" STR_KB_HINT_SECONDARY_CHAR: "Håll VÄLJ för sekundärt tecken" STR_KB_HINT_UPPER_SECONDARY: "Håll VÄLJ för VERSALER eller sekundärt tecken" STR_KB_HINT_LOWER_SECONDARY: "Håll VÄLJ för gemener eller sekundärt tecken" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 53036eed..d81b35eb 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -646,3 +646,6 @@ STR_KB_HINT_CLEAR_TEXT: "Tüm metni temizlemek için DEL basılı tutun" STR_KB_HINT_SECONDARY_CHAR: "İkincil karakter için SELECT tutun" STR_KB_HINT_UPPER_SECONDARY: "BÜYÜK HARF veya ikincil karakter için SELECT tutun" STR_KB_HINT_LOWER_SECONDARY: "Küçük harf veya ikincil karakter için SELECT tutun" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 286883a6..b36ade30 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -646,3 +646,6 @@ STR_KB_HINT_CLEAR_TEXT: "Утримуйте DEL, щоб очистити вес STR_KB_HINT_SECONDARY_CHAR: "Утримуйте SELECT для додаткового символу" STR_KB_HINT_UPPER_SECONDARY: "Утримуйте SELECT для ВЕЛИКИХ літер або додаткового символу" STR_KB_HINT_LOWER_SECONDARY: "Утримуйте SELECT для малих літер або додаткового символу" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index 34178f8d..d109175d 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -646,3 +646,6 @@ STR_KB_HINT_CLEAR_TEXT: "Giữ DEL để xóa tất cả văn bản" STR_KB_HINT_SECONDARY_CHAR: "Giữ SELECT để nhập ký tự phụ" STR_KB_HINT_UPPER_SECONDARY: "Giữ SELECT để nhập CHỮ HOA hoặc ký tự phụ" STR_KB_HINT_LOWER_SECONDARY: "Giữ SELECT để nhập chữ thường hoặc ký tự phụ" + +STR_VIEW_LIST: "List" +STR_VIEW_GRID: "Grid" diff --git a/src/CrossPointState.h b/src/CrossPointState.h index cfbb50a8..868c2e6a 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -95,6 +95,7 @@ class CrossPointState { size_t lastSleepImage = SIZE_MAX; // SIZE_MAX = unset sentinel uint8_t readerActivityLoadCount = 0; bool lastSleepFromReader = false; + bool recentBooksGridView = false; // true = grid/thumbnail view, false = list view KOReaderSyncSessionState koReaderSyncSession; PendingBookmarkJumpState pendingBookmarkJump; ~CrossPointState() = default; diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index 0860ea51..e7ceb177 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -85,6 +85,7 @@ bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) { doc["lastSleepImage"] = s.lastSleepImage; doc["readerActivityLoadCount"] = s.readerActivityLoadCount; doc["lastSleepFromReader"] = s.lastSleepFromReader; + doc["recentBooksGridView"] = s.recentBooksGridView; // Information about a pending KOReader sync session JsonObject sync = doc["koReaderSyncSession"].to(); sync["active"] = s.koReaderSyncSession.active; @@ -143,6 +144,7 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { s.lastSleepImage = doc["lastSleepImage"] | SIZE_MAX; s.readerActivityLoadCount = doc["readerActivityLoadCount"] | (uint8_t)0; s.lastSleepFromReader = doc["lastSleepFromReader"] | false; + s.recentBooksGridView = doc["recentBooksGridView"] | false; JsonObject sync = doc["koReaderSyncSession"].as(); s.koReaderSyncSession.active = sync["active"] | false; s.koReaderSyncSession.epubPath = sync["epubPath"] | std::string(""); diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index e0eba6a1..7e54bc8a 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -1,11 +1,18 @@ #include "RecentBooksActivity.h" +#include +#include #include #include #include #include +#include +#include +#include +#include #include +#include #include "../ActivityManager.h" #include "../util/ConfirmationActivity.h" @@ -17,24 +24,119 @@ #include "components/UITheme.h" #include "fontIds.h" +namespace { +// Mirror of HomeActivity::convertSidecarToBmp — converts a JPG/PNG sidecar cover to a +// 1-bit BMP at the requested WxH size and caches it under /.crosspoint/sidecar_/. +std::string convertSidecarToBmp(const std::string& bookPath, const std::string& sidecarPath, int width, int height, + const std::string& fileName) { + const std::string cacheDir = "/.crosspoint/sidecar_" + std::to_string(std::hash{}(bookPath)); + Storage.mkdir(cacheDir.c_str()); + const std::string bmpPath = cacheDir + "/" + fileName; + if (Storage.exists(bmpPath.c_str())) return bmpPath; + + FsFile src; + if (!Storage.openFileForRead("RBA", sidecarPath, src)) return ""; + FsFile dst; + if (!Storage.openFileForWrite("RBA", bmpPath, dst)) { + src.close(); + return ""; + } + + bool ok = false; + if (FsHelpers::hasJpgExtension(sidecarPath)) { + ok = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(src, dst, width, height); + } else if (FsHelpers::hasPngExtension(sidecarPath)) { + ok = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(src, dst, width, height); + } + src.close(); + dst.close(); + if (!ok) { + Storage.remove(bmpPath.c_str()); + return ""; + } + return bmpPath; +} + +// Compute the grid thumbnail width from the available content width. +int gridThumbWidth(int contentWidth) { + const int margin = RecentBooksActivity::GRID_THUMB_MARGIN; + const int cols = RecentBooksActivity::GRID_COLS; + return (contentWidth - (cols + 1) * margin) / cols; +} + +// Resolve the [HEIGHT] placeholder in a cover BMP path to the WxH thumbnail path. +std::string gridThumbPath(const std::string& coverBmpPath, int tw, int th) { + return UITheme::getCoverThumbPath(coverBmpPath, tw, th); +} +} // namespace + void RecentBooksActivity::loadRecentBooks() { recentBooks.clear(); const auto& books = RECENT_BOOKS.getBooks(); recentBooks.reserve(books.size()); - for (const auto& book : books) { - // Skip if file no longer exists - if (!Storage.exists(book.path.c_str())) { - continue; - } + if (!Storage.exists(book.path.c_str())) continue; recentBooks.push_back(book); } } +bool RecentBooksActivity::loadNextCover() { + const Rect contentRect = UITheme::getContentRect(renderer, true, true); + const int tw = gridThumbWidth(contentRect.width); + const int th = GRID_THUMB_HEIGHT; + + for (; nextCoverIndex < recentBooks.size(); nextCoverIndex++) { + RecentBook& book = recentBooks[nextCoverIndex]; + if (book.coverBmpPath.empty()) continue; + + const bool isSidecar = + FsHelpers::hasJpgExtension(book.coverBmpPath) || FsHelpers::hasPngExtension(book.coverBmpPath); + + if (isSidecar) { + if (!Storage.exists(book.coverBmpPath.c_str())) { + RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, ""); + book.coverBmpPath = ""; + continue; + } + const std::string cacheBase = + "/.crosspoint/sidecar_" + std::to_string(std::hash{}(book.path)); + const std::string placeholder = cacheBase + "/[HEIGHT].bmp"; + const std::string name = std::to_string(tw) + "x" + std::to_string(th) + ".bmp"; + const std::string result = convertSidecarToBmp(book.path, book.coverBmpPath, tw, th, name); + if (!result.empty()) { + RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, placeholder); + book.coverBmpPath = placeholder; + } + nextCoverIndex++; + return false; // yield — one conversion per loop tick + } + + const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); + if (!Storage.exists(thumbPath.c_str())) { + bool ok = false; + if (FsHelpers::hasEpubExtension(book.path)) { + Epub epub(book.path, "/.crosspoint"); + epub.load(false, true); + ok = epub.generateThumbBmp(tw, th); + } else if (FsHelpers::hasXtcExtension(book.path)) { + Xtc xtc(book.path, "/.crosspoint"); + if (xtc.load()) ok = xtc.generateThumbBmp(tw, th); + } + if (!ok) { + RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, ""); + book.coverBmpPath = ""; + } + nextCoverIndex++; + return false; // yield — one generation per loop tick + } + } + + return true; // all thumbnails are ready +} + void RecentBooksActivity::onEnter() { Activity::onEnter(); - // Load data loadRecentBooks(); selectorIndex = 0; @@ -42,6 +144,12 @@ void RecentBooksActivity::onEnter() { selectorIndex = initialFocusIndex; } initialFocusIndex = -1; + + coversLoaded = false; + coversLoading = false; + firstRenderDone = false; + nextCoverIndex = 0; + requestUpdate(); } @@ -51,15 +159,15 @@ void RecentBooksActivity::onExit() { } void RecentBooksActivity::loop() { + const bool gridView = APP_STATE.recentBooksGridView; + + // --- Consume named button events first --- ButtonEventManager::ButtonEvent ev; while (buttonEvents.consumeEvent(ev)) { + // Open book (Confirm short/long) if (ev.button == MappedInputManager::Button::Confirm && (ev.type == ButtonEventManager::PressType::Short || ev.type == ButtonEventManager::PressType::Long)) { - if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) { - return; - } - // Long-press Confirm signals "open with KOReader sync" only for EPUBs. - // Short-press is unchanged direct open. + if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; const bool longPress = (ev.type == ButtonEventManager::PressType::Long) && KOREADER_STORE.hasCredentials(); const std::string& selectedPath = recentBooks[selectorIndex].path; const bool isEpubBook = FsHelpers::hasEpubExtension(selectedPath); @@ -78,58 +186,130 @@ void RecentBooksActivity::loop() { return; } + // Back → home if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { onGoHome(); return; } + // Left short: switch view or (in list view) remove book if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Short) { - if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; - const std::string bookPath = recentBooks[selectorIndex].path; - const std::string bookTitle = recentBooks[selectorIndex].title; - - auto handler = [this, bookPath](const ActivityResult& res) { - if (!res.isCancelled) { - LOG_DBG("RBA", "Removing from recent books: %s", bookPath.c_str()); - RECENT_BOOKS.removeBook(bookPath); - loadRecentBooks(); - if (recentBooks.empty()) { - selectorIndex = 0; - } else if (selectorIndex >= static_cast(recentBooks.size())) { - selectorIndex = static_cast(recentBooks.size()) - 1; - } - requestUpdate(true); - } else { - LOG_DBG("RBA", "Remove cancelled by user"); - } - }; - - std::string heading = tr(STR_REMOVE) + std::string("? "); - startActivityForResult(std::make_unique(renderer, mappedInput, heading, bookTitle), - handler); + if (!gridView) { + // List view → switch to grid view + APP_STATE.recentBooksGridView = true; + APP_STATE.saveToFile(); + coversLoaded = false; + coversLoading = false; + firstRenderDone = false; + nextCoverIndex = 0; + requestUpdate(true); + return; + } + // Grid view: Left navigates to the previous item (column nav). The event is already + // consumed from the queue, so handle it directly here. + if (recentBooks.empty()) return; + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, static_cast(recentBooks.size())); + requestUpdate(); return; } + // Right short (list view only): show book info if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) { - if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; - const std::string& path = recentBooks[selectorIndex].path; - if (FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path)) { - startActivityForResult(std::make_unique(renderer, mappedInput, path), - [this](const ActivityResult&) { requestUpdate(); }); + if (!gridView) { + if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; + const std::string& path = recentBooks[selectorIndex].path; + if (FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path)) { + startActivityForResult(std::make_unique(renderer, mappedInput, path), + [this](const ActivityResult&) { requestUpdate(); }); + return; + } + } else { + // Grid view: Right → move to next item (column navigation) + if (recentBooks.empty()) return; + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, static_cast(recentBooks.size())); + requestUpdate(); + return; + } + } + + // Left long (list view only): remove book + if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) { + if (!gridView) { + if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; + const std::string bookPath = recentBooks[selectorIndex].path; + const std::string bookTitle = recentBooks[selectorIndex].title; + + auto handler = [this, bookPath](const ActivityResult& res) { + if (!res.isCancelled) { + LOG_DBG("RBA", "Removing from recent books: %s", bookPath.c_str()); + RECENT_BOOKS.removeBook(bookPath); + loadRecentBooks(); + if (recentBooks.empty()) { + selectorIndex = 0; + } else if (selectorIndex >= static_cast(recentBooks.size())) { + selectorIndex = static_cast(recentBooks.size()) - 1; + } + requestUpdate(true); + } + }; + std::string heading = tr(STR_REMOVE) + std::string("? "); + startActivityForResult( + std::make_unique(renderer, mappedInput, heading, bookTitle), handler); return; } } } - // Navigator is restricted to Up/Down so it cannot race the Left/Right Short - // handlers above (Left/Right actions are not in the onNextList/onPreviousList button sets). + // --- ButtonNavigator for Up/Down (both views), and list-view Remove shortcut --- const int listSize = static_cast(recentBooks.size()); - buttonNavigator.onNextList({MappedInputManager::Button::Down}, selectorIndex, listSize, [this] { requestUpdate(); }); - buttonNavigator.onPreviousList({MappedInputManager::Button::Up}, selectorIndex, listSize, - [this] { requestUpdate(); }); + + if (gridView) { + // Up/Down navigate by full row (GRID_COLS items at a time) + if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { + if (!recentBooks.empty()) { + selectorIndex = std::max(0, selectorIndex - GRID_COLS); + requestUpdate(); + } + } + if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { + if (!recentBooks.empty()) { + selectorIndex = std::min(listSize - 1, selectorIndex + GRID_COLS); + requestUpdate(); + } + } + + // Lazy cover loading after first render + if (firstRenderDone && !coversLoaded && !coversLoading) { + coversLoading = true; + if (loadNextCover()) { + coversLoaded = true; + } else { + requestUpdate(); + } + coversLoading = false; + } + } else { + // List view: Up/Down with double-click and long-press special behaviour + buttonNavigator.onNextList({MappedInputManager::Button::Down}, selectorIndex, listSize, + [this] { requestUpdate(); }); + buttonNavigator.onPreviousList({MappedInputManager::Button::Up}, selectorIndex, listSize, + [this] { requestUpdate(); }); + } } -void RecentBooksActivity::render(RenderLock&&) { +void RecentBooksActivity::render(RenderLock&& lock) { + if (APP_STATE.recentBooksGridView) { + renderGridView(std::move(lock)); + if (!firstRenderDone) { + firstRenderDone = true; + requestUpdate(); + } + } else { + renderListView(std::move(lock)); + } +} + +void RecentBooksActivity::renderListView(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); @@ -141,13 +321,13 @@ void RecentBooksActivity::render(RenderLock&&) { const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; - // Recent tab if (recentBooks.empty()) { renderer.drawText(UI_10_FONT_ID, contentRect.x + metrics.contentSidePadding, contentTop + 20, tr(STR_NO_RECENT_BOOKS)); } else { GUI.drawList( - renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, recentBooks.size(), selectorIndex, + renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, + static_cast(recentBooks.size()), selectorIndex, [this](int index) { return recentBooks[index].title; }, [this](int index) { const auto& book = recentBooks[index]; @@ -158,16 +338,130 @@ void RecentBooksActivity::render(RenderLock&&) { [this](int index) { return UITheme::getFileIcon(recentBooks[index].path); }); } - // Help text - const bool hasInfo = !recentBooks.empty() && selectorIndex < recentBooks.size() && + const bool hasInfo = !recentBooks.empty() && selectorIndex < static_cast(recentBooks.size()) && (FsHelpers::hasEpubExtension(recentBooks[selectorIndex].path) || FsHelpers::hasXtcExtension(recentBooks[selectorIndex].path)); const bool hasBooks = !recentBooks.empty(); - const auto labels = mappedInput.mapLabels(tr(STR_HOME), hasBooks ? tr(STR_OPEN) : "", hasBooks ? tr(STR_REMOVE) : "", - hasInfo ? tr(STR_INFO) : ""); + // Left short switches to grid view; label it accordingly. Long Left = remove. + const auto labels = + mappedInput.mapLabels(tr(STR_HOME), hasBooks ? tr(STR_OPEN) : "", tr(STR_VIEW_GRID), + hasInfo ? tr(STR_INFO) : ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - // Side buttons (Up/Down) navigate; show their hints on the side + GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + + renderer.displayBuffer(); +} + +void RecentBooksActivity::renderGridView(RenderLock&&) { + renderer.clearScreen(); + + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect contentRect = UITheme::getContentRect(renderer, true, true); + + GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight}, + tr(STR_MENU_RECENT_BOOKS)); + + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; + + if (recentBooks.empty()) { + renderer.drawText(UI_10_FONT_ID, contentRect.x + metrics.contentSidePadding, contentTop + 20, + tr(STR_NO_RECENT_BOOKS)); + const auto labels = mappedInput.mapLabels(tr(STR_HOME), "", tr(STR_VIEW_LIST), ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } + + const int margin = GRID_THUMB_MARGIN; + const int tw = gridThumbWidth(contentRect.width); + const int th = GRID_THUMB_HEIGHT; + const int cellHeight = th + GRID_LABEL_HEIGHT + margin; + + const int visibleRows = std::max(1, contentHeight / cellHeight); + const int totalRows = (static_cast(recentBooks.size()) + GRID_COLS - 1) / GRID_COLS; + + const int selectedRow = selectorIndex / GRID_COLS; + const int pageStartRow = (selectedRow / visibleRows) * visibleRows; + + const int startIndex = pageStartRow * GRID_COLS; + const int endIndex = std::min(startIndex + visibleRows * GRID_COLS, static_cast(recentBooks.size())); + + for (int i = startIndex; i < endIndex; i++) { + const int row = (i / GRID_COLS) - pageStartRow; + const int col = i % GRID_COLS; + + const int cellX = contentRect.x + margin + col * (tw + margin); + const int cellY = contentTop + row * cellHeight; + + const bool selected = (i == selectorIndex); + + // Draw thumbnail border (double rect when selected) + if (selected) { + renderer.drawRect(cellX - 2, cellY - 2, tw + 4, th + 4); + renderer.drawRect(cellX - 1, cellY - 1, tw + 2, th + 2); + } else { + renderer.drawRect(cellX, cellY, tw, th); + } + + // Draw cover BMP thumbnail + const auto& book = recentBooks[i]; + bool coverDrawn = false; + if (!book.coverBmpPath.empty()) { + const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); + FsFile file; + if (Storage.openFileForRead("RBA", thumbPath, file)) { + Bitmap bmp(file); + if (bmp.parseHeaders() == BmpReaderError::Ok) { + renderer.drawBitmap1Bit(bmp, cellX, cellY, tw, th); + coverDrawn = true; + } + file.close(); + } + } + + if (!coverDrawn) { + // Empty placeholder — just a white box inside the border + renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); + } + + // Label: title on line 1, author on line 2 + const int labelY = cellY + th + 3; + const int labelW = tw - 4; + const bool invert = !selected; + + std::string titleStr = renderer.truncatedText(SMALL_FONT_ID, book.title.c_str(), labelW); + renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY, titleStr.c_str(), invert); + + if (!book.author.empty()) { + std::string authorStr = renderer.truncatedText(SMALL_FONT_ID, book.author.c_str(), labelW); + renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY + 17, authorStr.c_str(), invert); + } + } + + // Scroll arrows when content spans multiple pages + if (totalRows > visibleRows) { + constexpr int arrowSize = 6; + const int centerX = contentRect.x + contentRect.width / 2; + + if (pageStartRow > 0) { + const int arrowY = contentTop + 2; + for (int j = 0; j < arrowSize; ++j) { + const int half = arrowSize - 1 - j; + renderer.drawLine(centerX - half, arrowY + j, centerX + half, arrowY + j); + } + } + if (pageStartRow + visibleRows < totalRows) { + const int arrowY = contentTop + contentHeight - arrowSize - 2; + for (int j = 0; j < arrowSize; ++j) { + renderer.drawLine(centerX - j, arrowY + j, centerX + j, arrowY + j); + } + } + } + + const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_VIEW_LIST), ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); renderer.displayBuffer(); diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index b5044d25..f11bafb6 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -10,17 +11,32 @@ #include "util/ButtonNavigator.h" class RecentBooksActivity final : public Activity { + public: + static constexpr int GRID_COLS = 3; + static constexpr int GRID_THUMB_HEIGHT = 180; + static constexpr int GRID_THUMB_MARGIN = 10; + static constexpr int GRID_LABEL_HEIGHT = 36; // two small-font lines below each thumbnail + private: ButtonNavigator buttonNavigator; int selectorIndex = 0; int initialFocusIndex = -1; // applied once in onEnter(), then cleared - // Recent tab state std::vector recentBooks; - // Data loading + // Lazy cover loading state for grid view + bool coversLoaded = false; + bool coversLoading = false; + bool firstRenderDone = false; + size_t nextCoverIndex = 0; + void loadRecentBooks(); + // Generates the next missing grid thumbnail (one per call). Returns true when all done. + bool loadNextCover(); + + void renderListView(RenderLock&&); + void renderGridView(RenderLock&&); public: explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, int focusIndex = -1) From 3eeee72d237d8f46d1ba5e4e00b65a4512fae806 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 14 May 2026 16:15:32 +0200 Subject: [PATCH 2/4] Properly distinguish double / single clicks --- src/activities/home/RecentBooksActivity.cpp | 400 +++++++++++--------- src/activities/home/RecentBooksActivity.h | 13 +- 2 files changed, 230 insertions(+), 183 deletions(-) diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 7e54bc8a..a105a80f 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -25,8 +26,6 @@ #include "fontIds.h" namespace { -// Mirror of HomeActivity::convertSidecarToBmp — converts a JPG/PNG sidecar cover to a -// 1-bit BMP at the requested WxH size and caches it under /.crosspoint/sidecar_/. std::string convertSidecarToBmp(const std::string& bookPath, const std::string& sidecarPath, int width, int height, const std::string& fileName) { const std::string cacheDir = "/.crosspoint/sidecar_" + std::to_string(std::hash{}(bookPath)); @@ -57,14 +56,12 @@ std::string convertSidecarToBmp(const std::string& bookPath, const std::string& return bmpPath; } -// Compute the grid thumbnail width from the available content width. int gridThumbWidth(int contentWidth) { const int margin = RecentBooksActivity::GRID_THUMB_MARGIN; const int cols = RecentBooksActivity::GRID_COLS; return (contentWidth - (cols + 1) * margin) / cols; } -// Resolve the [HEIGHT] placeholder in a cover BMP path to the WxH thumbnail path. std::string gridThumbPath(const std::string& coverBmpPath, int tw, int th) { return UITheme::getCoverThumbPath(coverBmpPath, tw, th); } @@ -98,8 +95,7 @@ bool RecentBooksActivity::loadNextCover() { book.coverBmpPath = ""; continue; } - const std::string cacheBase = - "/.crosspoint/sidecar_" + std::to_string(std::hash{}(book.path)); + const std::string cacheBase = "/.crosspoint/sidecar_" + std::to_string(std::hash{}(book.path)); const std::string placeholder = cacheBase + "/[HEIGHT].bmp"; const std::string name = std::to_string(tw) + "x" + std::to_string(th) + ".bmp"; const std::string result = convertSidecarToBmp(book.path, book.coverBmpPath, tw, th, name); @@ -108,7 +104,7 @@ bool RecentBooksActivity::loadNextCover() { book.coverBmpPath = placeholder; } nextCoverIndex++; - return false; // yield — one conversion per loop tick + return false; } const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); @@ -127,11 +123,11 @@ bool RecentBooksActivity::loadNextCover() { book.coverBmpPath = ""; } nextCoverIndex++; - return false; // yield — one generation per loop tick + return false; } } - return true; // all thumbnails are ready + return true; } void RecentBooksActivity::onEnter() { @@ -149,6 +145,8 @@ void RecentBooksActivity::onEnter() { coversLoading = false; firstRenderDone = false; nextCoverIndex = 0; + prevSelectorIndex = -1; + fullRedrawNeeded = true; requestUpdate(); } @@ -158,13 +156,57 @@ void RecentBooksActivity::onExit() { recentBooks.clear(); } +void RecentBooksActivity::switchViewMode(bool grid) { + APP_STATE.recentBooksGridView = grid; + APP_STATE.saveToFile(); + coversLoaded = false; + coversLoading = false; + firstRenderDone = false; + nextCoverIndex = 0; + prevSelectorIndex = -1; + fullRedrawNeeded = true; + requestUpdate(true); +} + +void RecentBooksActivity::removeSelectedBook() { + if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; + const std::string bookPath = recentBooks[selectorIndex].path; + const std::string bookTitle = recentBooks[selectorIndex].title; + auto handler = [this, bookPath](const ActivityResult& res) { + if (!res.isCancelled) { + LOG_DBG("RBA", "Removing from recent books: %s", bookPath.c_str()); + RECENT_BOOKS.removeBook(bookPath); + loadRecentBooks(); + if (recentBooks.empty()) { + selectorIndex = 0; + } else if (selectorIndex >= static_cast(recentBooks.size())) { + selectorIndex = static_cast(recentBooks.size()) - 1; + } + prevSelectorIndex = -1; + fullRedrawNeeded = true; + requestUpdate(true); + } + }; + std::string heading = tr(STR_REMOVE) + std::string("? "); + startActivityForResult(std::make_unique(renderer, mappedInput, heading, bookTitle), handler); +} + +void RecentBooksActivity::showSelectedBookInfo() { + if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; + const std::string& path = recentBooks[selectorIndex].path; + if (FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path)) { + startActivityForResult(std::make_unique(renderer, mappedInput, path), + [this](const ActivityResult&) { requestUpdate(); }); + } +} + void RecentBooksActivity::loop() { const bool gridView = APP_STATE.recentBooksGridView; + const int listSize = static_cast(recentBooks.size()); - // --- Consume named button events first --- ButtonEventManager::ButtonEvent ev; while (buttonEvents.consumeEvent(ev)) { - // Open book (Confirm short/long) + // Confirm short/long: open book (long = KOReader sync for EPUBs) if (ev.button == MappedInputManager::Button::Confirm && (ev.type == ButtonEventManager::PressType::Short || ev.type == ButtonEventManager::PressType::Long)) { if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; @@ -186,113 +228,69 @@ void RecentBooksActivity::loop() { return; } - // Back → home + // Back short: go home if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) { onGoHome(); return; } - // Left short: switch view or (in list view) remove book - if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Short) { - if (!gridView) { - // List view → switch to grid view - APP_STATE.recentBooksGridView = true; - APP_STATE.saveToFile(); - coversLoaded = false; - coversLoading = false; - firstRenderDone = false; - nextCoverIndex = 0; - requestUpdate(true); - return; + // Up short: navigate (row up in grid, previous in list) + if (ev.button == MappedInputManager::Button::Up && ev.type == ButtonEventManager::PressType::Short) { + if (!recentBooks.empty()) { + if (gridView) { + selectorIndex = std::max(0, selectorIndex - GRID_COLS); + } else { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, listSize); + } + requestUpdate(); } - // Grid view: Left navigates to the previous item (column nav). The event is already - // consumed from the queue, so handle it directly here. - if (recentBooks.empty()) return; - selectorIndex = ButtonNavigator::previousIndex(selectorIndex, static_cast(recentBooks.size())); - requestUpdate(); + continue; + } + + // Down short: navigate (row down in grid, next in list) + if (ev.button == MappedInputManager::Button::Down && ev.type == ButtonEventManager::PressType::Short) { + if (!recentBooks.empty()) { + if (gridView) { + selectorIndex = std::min(listSize - 1, selectorIndex + GRID_COLS); + } else { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, listSize); + } + requestUpdate(); + } + continue; + } + + // Up long: toggle between list and grid view + if (ev.button == MappedInputManager::Button::Up && ev.type == ButtonEventManager::PressType::Long) { + switchViewMode(!gridView); return; } - // Right short (list view only): show book info - if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) { - if (!gridView) { - if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; - const std::string& path = recentBooks[selectorIndex].path; - if (FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path)) { - startActivityForResult(std::make_unique(renderer, mappedInput, path), - [this](const ActivityResult&) { requestUpdate(); }); - return; - } - } else { - // Grid view: Right → move to next item (column navigation) - if (recentBooks.empty()) return; - selectorIndex = ButtonNavigator::nextIndex(selectorIndex, static_cast(recentBooks.size())); - requestUpdate(); - return; - } + // Left long: remove selected book (both views) + if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) { + removeSelectedBook(); + return; } - // Left long (list view only): remove book - if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) { - if (!gridView) { - if (recentBooks.empty() || selectorIndex >= static_cast(recentBooks.size())) return; - const std::string bookPath = recentBooks[selectorIndex].path; - const std::string bookTitle = recentBooks[selectorIndex].title; - - auto handler = [this, bookPath](const ActivityResult& res) { - if (!res.isCancelled) { - LOG_DBG("RBA", "Removing from recent books: %s", bookPath.c_str()); - RECENT_BOOKS.removeBook(bookPath); - loadRecentBooks(); - if (recentBooks.empty()) { - selectorIndex = 0; - } else if (selectorIndex >= static_cast(recentBooks.size())) { - selectorIndex = static_cast(recentBooks.size()) - 1; - } - requestUpdate(true); - } - }; - std::string heading = tr(STR_REMOVE) + std::string("? "); - startActivityForResult( - std::make_unique(renderer, mappedInput, heading, bookTitle), handler); - return; - } + // Right long: show book info (both views) + if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Long) { + showSelectedBookInfo(); + return; } } - // --- ButtonNavigator for Up/Down (both views), and list-view Remove shortcut --- - const int listSize = static_cast(recentBooks.size()); - if (gridView) { - // Up/Down navigate by full row (GRID_COLS items at a time) - if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { - if (!recentBooks.empty()) { - selectorIndex = std::max(0, selectorIndex - GRID_COLS); - requestUpdate(); - } - } - if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { - if (!recentBooks.empty()) { - selectorIndex = std::min(listSize - 1, selectorIndex + GRID_COLS); - requestUpdate(); - } - } - - // Lazy cover loading after first render - if (firstRenderDone && !coversLoaded && !coversLoading) { - coversLoading = true; - if (loadNextCover()) { - coversLoaded = true; - } else { - requestUpdate(); - } - coversLoading = false; - } - } else { - // List view: Up/Down with double-click and long-press special behaviour - buttonNavigator.onNextList({MappedInputManager::Button::Down}, selectorIndex, listSize, + // Left/Right short: column navigation (no long-press conflict on these buttons) + buttonNavigator.onNextList({MappedInputManager::Button::Right}, selectorIndex, listSize, [this] { requestUpdate(); }); - buttonNavigator.onPreviousList({MappedInputManager::Button::Up}, selectorIndex, listSize, + buttonNavigator.onPreviousList({MappedInputManager::Button::Left}, selectorIndex, listSize, + [this] { requestUpdate(); }); + // (Up/Down handled via buttonEvents Short above) + } else { + // Left/Right short: consumed cleanly (no-op in list view but prevents phantom events) + buttonNavigator.onNextList({MappedInputManager::Button::Right}, selectorIndex, listSize, + [this] { requestUpdate(); }); + buttonNavigator.onPreviousList({MappedInputManager::Button::Left}, selectorIndex, listSize, [this] { requestUpdate(); }); } } @@ -303,6 +301,15 @@ void RecentBooksActivity::render(RenderLock&& lock) { if (!firstRenderDone) { firstRenderDone = true; requestUpdate(); + } else if (!coversLoaded && !coversLoading) { + coversLoading = true; + if (loadNextCover()) { + coversLoaded = true; + } else { + fullRedrawNeeded = true; + requestUpdate(); + } + coversLoading = false; } } else { renderListView(std::move(lock)); @@ -327,8 +334,7 @@ void RecentBooksActivity::renderListView(RenderLock&&) { } else { GUI.drawList( renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, - static_cast(recentBooks.size()), selectorIndex, - [this](int index) { return recentBooks[index].title; }, + static_cast(recentBooks.size()), selectorIndex, [this](int index) { return recentBooks[index].title; }, [this](int index) { const auto& book = recentBooks[index]; if (!book.author.empty() && !book.series.empty()) return book.author + "\n" + book.series; @@ -338,113 +344,134 @@ void RecentBooksActivity::renderListView(RenderLock&&) { [this](int index) { return UITheme::getFileIcon(recentBooks[index].path); }); } - const bool hasInfo = !recentBooks.empty() && selectorIndex < static_cast(recentBooks.size()) && - (FsHelpers::hasEpubExtension(recentBooks[selectorIndex].path) || - FsHelpers::hasXtcExtension(recentBooks[selectorIndex].path)); const bool hasBooks = !recentBooks.empty(); - // Left short switches to grid view; label it accordingly. Long Left = remove. - const auto labels = - mappedInput.mapLabels(tr(STR_HOME), hasBooks ? tr(STR_OPEN) : "", tr(STR_VIEW_GRID), - hasInfo ? tr(STR_INFO) : ""); - + const auto labels = mappedInput.mapLabels(tr(STR_HOME), hasBooks ? tr(STR_OPEN) : "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); renderer.displayBuffer(); } -void RecentBooksActivity::renderGridView(RenderLock&&) { - renderer.clearScreen(); +void RecentBooksActivity::renderGridCell(int index, bool selected, int cellX, int cellY, int tw, int th, int labelW) { + const auto& book = recentBooks[index]; + const int labelY = cellY + th + 3; + const int cellFillHeight = th + GRID_LABEL_HEIGHT + 3; + if (selected) { + renderer.fillRect(cellX, cellY, tw, cellFillHeight); + renderer.drawRect(cellX, cellY, tw, th, false); + } else { + // Clear to white before redrawing (needed when deselecting) + renderer.fillRect(cellX, cellY, tw, cellFillHeight, false); + renderer.drawRect(cellX, cellY, tw, th); + } + + // White prefill inside thumbnail frame so 1-bit BMP transparent pixels show white + renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); + + if (!book.coverBmpPath.empty()) { + const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); + FsFile file; + if (Storage.openFileForRead("RBA", thumbPath, file)) { + Bitmap bmp(file); + if (bmp.parseHeaders() == BmpReaderError::Ok) { + const int imgW = bmp.getWidth(); + const int imgH = bmp.getHeight(); + const int innerW = tw - 2; + const int innerH = th - 2; + if (imgW > 0 && imgH > 0) { + // Mirror drawBitmap1Bit scale = min(maxW/imgW, maxH/imgH) to get rendered size, + // then center the image within the frame. + const float scaleX = static_cast(innerW) / imgW; + const float scaleY = static_cast(innerH) / imgH; + const float scale = std::min(scaleX, scaleY); + const int rendW = static_cast(imgW * scale); + const int rendH = static_cast(imgH * scale); + const int offsetX = std::max(1, (tw - rendW) / 2); + const int offsetY = std::max(1, (th - rendH) / 2); + renderer.drawBitmap1Bit(bmp, cellX + offsetX, cellY + offsetY, innerW, innerH); + } + } + file.close(); + } + } + + // Label: title line 1, author line 2; white text on black for selected, black on white otherwise + const bool black = !selected; + std::string titleStr = renderer.truncatedText(SMALL_FONT_ID, book.title.c_str(), labelW); + renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY, titleStr.c_str(), black); + if (!book.author.empty()) { + std::string authorStr = renderer.truncatedText(SMALL_FONT_ID, book.author.c_str(), labelW); + renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY + 17, authorStr.c_str(), black); + } +} + +void RecentBooksActivity::renderGridView(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const Rect contentRect = UITheme::getContentRect(renderer, true, true); + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; + const int margin = GRID_THUMB_MARGIN; + const int tw = gridThumbWidth(contentRect.width); + const int th = GRID_THUMB_HEIGHT; + const int cellHeight = th + GRID_LABEL_HEIGHT + margin; + const int visibleRows = std::max(1, contentHeight / cellHeight); + const int totalRows = (static_cast(recentBooks.size()) + GRID_COLS - 1) / GRID_COLS; + const int selectedRow = selectorIndex / GRID_COLS; + const int pageStartRow = (selectedRow / visibleRows) * visibleRows; + const int startIndex = pageStartRow * GRID_COLS; + const int labelW = tw - 4; + + auto cellPos = [&](int i, int& cx, int& cy) { + const int row = (i / GRID_COLS) - pageStartRow; + const int col = i % GRID_COLS; + cx = contentRect.x + margin + col * (tw + margin); + cy = contentTop + row * cellHeight; + }; + + // Partial fast path: only the selection changed within the same page + const int prevPage = prevSelectorIndex >= 0 ? (prevSelectorIndex / GRID_COLS / visibleRows) : -1; + const int curPage = selectedRow / visibleRows; + if (!fullRedrawNeeded && prevSelectorIndex >= 0 && prevSelectorIndex != selectorIndex && prevPage == curPage) { + int cx, cy; + cellPos(prevSelectorIndex, cx, cy); + renderGridCell(prevSelectorIndex, false, cx, cy, tw, th, labelW); + cellPos(selectorIndex, cx, cy); + renderGridCell(selectorIndex, true, cx, cy, tw, th, labelW); + prevSelectorIndex = selectorIndex; + renderer.displayBuffer(); + return; + } + + // Full redraw + fullRedrawNeeded = false; + prevSelectorIndex = selectorIndex; + + renderer.clearScreen(); GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight}, tr(STR_MENU_RECENT_BOOKS)); - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; - if (recentBooks.empty()) { renderer.drawText(UI_10_FONT_ID, contentRect.x + metrics.contentSidePadding, contentTop + 20, tr(STR_NO_RECENT_BOOKS)); - const auto labels = mappedInput.mapLabels(tr(STR_HOME), "", tr(STR_VIEW_LIST), ""); + const auto labels = mappedInput.mapLabels(tr(STR_HOME), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); return; } - const int margin = GRID_THUMB_MARGIN; - const int tw = gridThumbWidth(contentRect.width); - const int th = GRID_THUMB_HEIGHT; - const int cellHeight = th + GRID_LABEL_HEIGHT + margin; - - const int visibleRows = std::max(1, contentHeight / cellHeight); - const int totalRows = (static_cast(recentBooks.size()) + GRID_COLS - 1) / GRID_COLS; - - const int selectedRow = selectorIndex / GRID_COLS; - const int pageStartRow = (selectedRow / visibleRows) * visibleRows; - - const int startIndex = pageStartRow * GRID_COLS; const int endIndex = std::min(startIndex + visibleRows * GRID_COLS, static_cast(recentBooks.size())); - for (int i = startIndex; i < endIndex; i++) { - const int row = (i / GRID_COLS) - pageStartRow; - const int col = i % GRID_COLS; - - const int cellX = contentRect.x + margin + col * (tw + margin); - const int cellY = contentTop + row * cellHeight; - - const bool selected = (i == selectorIndex); - - // Draw thumbnail border (double rect when selected) - if (selected) { - renderer.drawRect(cellX - 2, cellY - 2, tw + 4, th + 4); - renderer.drawRect(cellX - 1, cellY - 1, tw + 2, th + 2); - } else { - renderer.drawRect(cellX, cellY, tw, th); - } - - // Draw cover BMP thumbnail - const auto& book = recentBooks[i]; - bool coverDrawn = false; - if (!book.coverBmpPath.empty()) { - const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); - FsFile file; - if (Storage.openFileForRead("RBA", thumbPath, file)) { - Bitmap bmp(file); - if (bmp.parseHeaders() == BmpReaderError::Ok) { - renderer.drawBitmap1Bit(bmp, cellX, cellY, tw, th); - coverDrawn = true; - } - file.close(); - } - } - - if (!coverDrawn) { - // Empty placeholder — just a white box inside the border - renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); - } - - // Label: title on line 1, author on line 2 - const int labelY = cellY + th + 3; - const int labelW = tw - 4; - const bool invert = !selected; - - std::string titleStr = renderer.truncatedText(SMALL_FONT_ID, book.title.c_str(), labelW); - renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY, titleStr.c_str(), invert); - - if (!book.author.empty()) { - std::string authorStr = renderer.truncatedText(SMALL_FONT_ID, book.author.c_str(), labelW); - renderer.drawText(SMALL_FONT_ID, cellX + 2, labelY + 17, authorStr.c_str(), invert); - } + int cx, cy; + cellPos(i, cx, cy); + renderGridCell(i, i == selectorIndex, cx, cy, tw, th, labelW); } // Scroll arrows when content spans multiple pages if (totalRows > visibleRows) { constexpr int arrowSize = 6; const int centerX = contentRect.x + contentRect.width / 2; - if (pageStartRow > 0) { const int arrowY = contentTop + 2; for (int j = 0; j < arrowSize; ++j) { @@ -460,7 +487,16 @@ void RecentBooksActivity::renderGridView(RenderLock&&) { } } - const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_VIEW_LIST), ""); + // On X4 (taller screen) there is room for a one-line gesture hint below the grid. + if (!gpio.deviceIsX3()) { + const int hintY = contentRect.y + contentRect.height - metrics.verticalSpacing - 14; + const std::string hint = std::string(tr(STR_DIR_UP)) + "+L: " + tr(STR_VIEW_GRID) + "/" + tr(STR_VIEW_LIST) + + " " + tr(STR_DIR_LEFT) + "+L: " + tr(STR_REMOVE) + " " + tr(STR_DIR_RIGHT) + + "+L: " + tr(STR_INFO); + renderer.drawText(SMALL_FONT_ID, contentRect.x + metrics.contentSidePadding, hintY, hint.c_str()); + } + + const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index f11bafb6..0469699a 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -13,7 +13,7 @@ class RecentBooksActivity final : public Activity { public: static constexpr int GRID_COLS = 3; - static constexpr int GRID_THUMB_HEIGHT = 180; + static constexpr int GRID_THUMB_HEIGHT = 160; static constexpr int GRID_THUMB_MARGIN = 10; static constexpr int GRID_LABEL_HEIGHT = 36; // two small-font lines below each thumbnail @@ -31,10 +31,21 @@ class RecentBooksActivity final : public Activity { bool firstRenderDone = false; size_t nextCoverIndex = 0; + // Partial selection repaint: track previous index so we only redraw two cells + int prevSelectorIndex = -1; + bool fullRedrawNeeded = true; + void loadRecentBooks(); // Generates the next missing grid thumbnail (one per call). Returns true when all done. bool loadNextCover(); + void switchViewMode(bool grid); + void removeSelectedBook(); + void showSelectedBookInfo(); + + // Draws a single grid cell (used for both full render and partial selection update). + void renderGridCell(int index, bool selected, int cellX, int cellY, int tw, int th, int labelW); + void renderListView(RenderLock&&); void renderGridView(RenderLock&&); From 5c4d47376b536b2c12664f29bdf788065fe02638 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 14 May 2026 16:31:51 +0200 Subject: [PATCH 3/4] Amend balck rect around image --- src/activities/home/RecentBooksActivity.cpp | 42 ++++++++++++--------- src/activities/home/RecentBooksActivity.h | 3 -- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index a105a80f..85720945 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -266,6 +266,24 @@ void RecentBooksActivity::loop() { return; } + // Left short: column left in grid, previous in list + if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Short) { + if (!recentBooks.empty()) { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, listSize); + requestUpdate(); + } + continue; + } + + // Right short: column right in grid, next in list + if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) { + if (!recentBooks.empty()) { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, listSize); + requestUpdate(); + } + continue; + } + // Left long: remove selected book (both views) if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) { removeSelectedBook(); @@ -278,21 +296,6 @@ void RecentBooksActivity::loop() { return; } } - - if (gridView) { - // Left/Right short: column navigation (no long-press conflict on these buttons) - buttonNavigator.onNextList({MappedInputManager::Button::Right}, selectorIndex, listSize, - [this] { requestUpdate(); }); - buttonNavigator.onPreviousList({MappedInputManager::Button::Left}, selectorIndex, listSize, - [this] { requestUpdate(); }); - // (Up/Down handled via buttonEvents Short above) - } else { - // Left/Right short: consumed cleanly (no-op in list view but prevents phantom events) - buttonNavigator.onNextList({MappedInputManager::Button::Right}, selectorIndex, listSize, - [this] { requestUpdate(); }); - buttonNavigator.onPreviousList({MappedInputManager::Button::Left}, selectorIndex, listSize, - [this] { requestUpdate(); }); - } } void RecentBooksActivity::render(RenderLock&& lock) { @@ -366,9 +369,6 @@ void RecentBooksActivity::renderGridCell(int index, bool selected, int cellX, in renderer.drawRect(cellX, cellY, tw, th); } - // White prefill inside thumbnail frame so 1-bit BMP transparent pixels show white - renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); - if (!book.coverBmpPath.empty()) { const std::string thumbPath = gridThumbPath(book.coverBmpPath, tw, th); FsFile file; @@ -389,11 +389,17 @@ void RecentBooksActivity::renderGridCell(int index, bool selected, int cellX, in const int rendH = static_cast(imgH * scale); const int offsetX = std::max(1, (tw - rendW) / 2); const int offsetY = std::max(1, (th - rendH) / 2); + // Only pre-clear exactly the rendered image area so the selected cell's black + // background shows through around the image instead of a white border. + renderer.fillRect(cellX + offsetX, cellY + offsetY, rendW, rendH, false); renderer.drawBitmap1Bit(bmp, cellX + offsetX, cellY + offsetY, innerW, innerH); } } file.close(); } + } else { + // No cover — clear the whole interior so the placeholder looks clean. + renderer.fillRect(cellX + 1, cellY + 1, tw - 2, th - 2, false); } // Label: title line 1, author line 2; white text on black for selected, black on white otherwise diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index 0469699a..752b6523 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -8,7 +8,6 @@ #include "../Activity.h" #include "RecentBooksStore.h" -#include "util/ButtonNavigator.h" class RecentBooksActivity final : public Activity { public: @@ -18,8 +17,6 @@ class RecentBooksActivity final : public Activity { static constexpr int GRID_LABEL_HEIGHT = 36; // two small-font lines below each thumbnail private: - ButtonNavigator buttonNavigator; - int selectorIndex = 0; int initialFocusIndex = -1; // applied once in onEnter(), then cleared From 28416721b971bec90581c363cf7d7bd9b564fddc Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 14 May 2026 17:10:10 +0200 Subject: [PATCH 4/4] Fix --- src/activities/home/RecentBooksActivity.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 85720945..b3746614 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -24,6 +24,7 @@ #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/ButtonNavigator.h" namespace { std::string convertSidecarToBmp(const std::string& bookPath, const std::string& sidecarPath, int width, int height, @@ -384,15 +385,16 @@ void RecentBooksActivity::renderGridCell(int index, bool selected, int cellX, in // then center the image within the frame. const float scaleX = static_cast(innerW) / imgW; const float scaleY = static_cast(innerH) / imgH; - const float scale = std::min(scaleX, scaleY); + // Cap at 1.0: never upscale (drawBitmap1Bit also won't upscale beyond maxW/maxH). + const float scale = std::min(1.0f, std::min(scaleX, scaleY)); const int rendW = static_cast(imgW * scale); const int rendH = static_cast(imgH * scale); const int offsetX = std::max(1, (tw - rendW) / 2); const int offsetY = std::max(1, (th - rendH) / 2); - // Only pre-clear exactly the rendered image area so the selected cell's black - // background shows through around the image instead of a white border. + // Pre-clear only the exact rendered image area; the black selection background + // shows through in the surrounding space. renderer.fillRect(cellX + offsetX, cellY + offsetY, rendW, rendH, false); - renderer.drawBitmap1Bit(bmp, cellX + offsetX, cellY + offsetY, innerW, innerH); + renderer.drawBitmap1Bit(bmp, cellX + offsetX, cellY + offsetY, rendW, rendH); } } file.close();