From b008baf8aab3ffd999de70d97e9025a8e591e82b Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 14:28:02 +0200 Subject: [PATCH 1/4] Paginate through bookinfo if required --- lib/I18n/translations/english.yaml | 2 + src/activities/home/BookInfoActivity.cpp | 88 ++++++++++++++++++++---- src/activities/home/BookInfoActivity.h | 20 ++++++ 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 44ed6a6f..a9632ca9 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -234,6 +234,8 @@ STR_FETCH_FEED_FAILED: "Failed to fetch feed" STR_PARSE_FEED_FAILED: "Failed to parse feed" STR_NEXT_PAGE: "Next Page »" STR_PREV_PAGE: "« Previous Page" +STR_NEXT: "Next" +STR_PREV: "Prev" STR_SEARCH: "Search" STR_NETWORK_PREFIX: "Network: " STR_IP_ADDRESS_PREFIX: "IP Address: " diff --git a/src/activities/home/BookInfoActivity.cpp b/src/activities/home/BookInfoActivity.cpp index 7c883cc6..7cf571a3 100644 --- a/src/activities/home/BookInfoActivity.cpp +++ b/src/activities/home/BookInfoActivity.cpp @@ -98,6 +98,7 @@ void BookInfoActivity::loadData() { void BookInfoActivity::onEnter() { Activity::onEnter(); + fullRenderDone = false; { RenderLock lock(*this); renderLoading(); @@ -109,10 +110,33 @@ void BookInfoActivity::onEnter() { void BookInfoActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { finish(); + } else if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { + if (descPage > 0) { + descPage--; + requestUpdate(true); + } + } else if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { + if (descPage + 1 < descTotalPages) { + descPage++; + requestUpdate(true); + } } } void BookInfoActivity::render(RenderLock&&) { + // Fast path: only the description page / button hints changed. Reuse the + // already-rendered header/cover/meta by clearing just those two bands. Cap + // consecutive partial renders to bound e-ink ghosting buildup. + if (fullRenderDone && loadSucceeded && !description.empty() && partialRenderCount < MAX_PARTIAL_RENDERS) { + renderer.fillRect(descBandX, descBandY, descBandWidth, descBandHeight, false); + renderer.fillRect(0, hintsBandY, renderer.getScreenWidth(), hintsBandHeight, false); + renderDescriptionAndHints(); + ++partialRenderCount; + renderer.displayBuffer(); + return; + } + + partialRenderCount = 0; renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); @@ -240,28 +264,66 @@ void BookInfoActivity::render(RenderLock&&) { topSectionBottom = std::max(topSectionBottom, metaY); - // --- Description: full width below the top section --- + // --- Description: full width below the top section, paged via Left/Right --- + descTotalPages = 0; + descLinesPerPage = 0; + descBandX = textX; + descBandWidth = textWidth; + descBandY = contentBottom; + descBandHeight = 0; if (!description.empty()) { int y = topSectionBottom + metrics.verticalSpacing; if (y + lineHeightSmall + 4 < contentBottom) { renderer.drawLine(textX, y, contentRect.x + contentRect.width - metrics.contentSidePadding, y); y += 4; - const int descMaxLines = (contentBottom - y) / lineHeightSmall; - if (descMaxLines > 0) { - const auto lines = renderer.wrappedText(UI_10_FONT_ID, description.c_str(), textWidth, descMaxLines); - for (const auto& line : lines) { - if (y + lineHeightSmall > contentBottom) break; - renderer.drawText(UI_10_FONT_ID, textX, y, line.c_str()); - y += lineHeightSmall; - } + // Record the description band (below the separator) for partial redraws. + descBandY = y; + descBandHeight = contentBottom - y; + } + } + + // Hints band spans from the bottom of the content area to the screen bottom. + hintsBandY = contentRect.y + contentRect.height; + hintsBandHeight = renderer.getScreenHeight() - hintsBandY; + + renderDescriptionAndHints(); + + fullRenderDone = true; + renderer.displayBuffer(); +} + +void BookInfoActivity::renderDescriptionAndHints() { + const int lineHeightSmall = renderer.getLineHeight(UI_10_FONT_ID); + + descTotalPages = 0; + descLinesPerPage = 0; + + if (!description.empty() && descBandHeight > 0) { + descLinesPerPage = descBandHeight / lineHeightSmall; + if (descLinesPerPage > 0) { + if (descLines.empty() || descWrappedWidth != descBandWidth) { + descLines = renderer.wrappedText(UI_10_FONT_ID, description.c_str(), descBandWidth, 1000); + descWrappedWidth = descBandWidth; + } + const int totalLines = static_cast(descLines.size()); + descTotalPages = (totalLines + descLinesPerPage - 1) / descLinesPerPage; + if (descPage >= descTotalPages) descPage = std::max(0, descTotalPages - 1); + + const int startLine = descPage * descLinesPerPage; + const int endLine = std::min(totalLines, startLine + descLinesPerPage); + int y = descBandY; + const int bandBottom = descBandY + descBandHeight; + for (int i = startLine; i < endLine; ++i) { + if (y + lineHeightSmall > bandBottom) break; + renderer.drawText(UI_10_FONT_ID, descBandX, y, descLines[i].c_str()); + y += lineHeightSmall; } } } - // Button hints - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + const char* prevLabel = (descPage > 0) ? tr(STR_PREV) : ""; + const char* nextLabel = (descPage + 1 < descTotalPages) ? tr(STR_NEXT) : ""; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", prevLabel, nextLabel); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - - renderer.displayBuffer(); } diff --git a/src/activities/home/BookInfoActivity.h b/src/activities/home/BookInfoActivity.h index e2249c46..a93ba407 100644 --- a/src/activities/home/BookInfoActivity.h +++ b/src/activities/home/BookInfoActivity.h @@ -19,9 +19,29 @@ class BookInfoActivity final : public Activity { bool loadSucceeded = false; size_t fileSizeBytes = 0; + // Description paging (populated lazily on first render) + std::vector descLines; + int descWrappedWidth = 0; + int descPage = 0; + int descLinesPerPage = 0; + int descTotalPages = 0; + + // Partial-render caches: set on the first full render, reused when only the + // description page changes. + bool fullRenderDone = false; + int descBandX = 0; + int descBandY = 0; + int descBandWidth = 0; + int descBandHeight = 0; + int hintsBandY = 0; + int hintsBandHeight = 0; + int partialRenderCount = 0; + static constexpr int MAX_PARTIAL_RENDERS = 10; + static std::string formatFileSize(size_t bytes); void renderLoading(); void loadData(); + void renderDescriptionAndHints(); public: explicit BookInfoActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string filePath) From f7db9f9d5895b0830d27685c8db8990a21ac956c Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 14:52:15 +0200 Subject: [PATCH 2/4] Update German --- lib/I18n/translations/german.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 4525d70f..24be6a92 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -146,6 +146,15 @@ STR_INVERTED: "Invertiert" STR_LANDSCAPE_CCW: "Querformat links" STR_PREV_NEXT: "Zurück/Weiter" STR_NEXT_PREV: "Weiter/Zurück" +STR_ABC: "abc" +STR_FORCE_REFRESH: "Bildschirm aktualisieren" +STR_NEXT: "Weiter" +STR_NEXT_PAGE: "Nächste Seite »" +STR_PREV: "Zurück" +STR_PREV_PAGE: "« Vorherige Seite" +STR_SEARCH: "Suche" +STR_SHIFT: "Shift" +STR_SHIFT_CAPS: "SHIFT" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" From e0afe7de9881a0f4a13196ced0eb63964260bdaf Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 14:58:11 +0200 Subject: [PATCH 3/4] Some more languages --- lib/I18n/translations/french.yaml | 9 +++++++++ lib/I18n/translations/italian.yaml | 9 +++++++++ lib/I18n/translations/russian.yaml | 9 +++++++++ lib/I18n/translations/spanish.yaml | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index de9ec54c..bceccbf4 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -143,6 +143,15 @@ STR_INVERTED: "Inversé" STR_LANDSCAPE_CCW: "Paysage inversé" STR_PREV_NEXT: "Préc/Suiv" STR_NEXT_PREV: "Suiv/Préc" +STR_ABC: "abc" +STR_FORCE_REFRESH: "Actualiser l'écran" +STR_NEXT: "Suiv" +STR_NEXT_PAGE: "Page suivante »" +STR_PREV: "Préc" +STR_PREV_PAGE: "« Page précédente" +STR_SEARCH: "Rechercher" +STR_SHIFT: "maj" +STR_SHIFT_CAPS: "MAJ" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index c8c60457..198e8b91 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -143,6 +143,15 @@ STR_INVERTED: "Invertito" STR_LANDSCAPE_CCW: "Orizzontale ↺" STR_PREV_NEXT: "Prec/Succ" STR_NEXT_PREV: "Succ/Prec" +STR_ABC: "abc" +STR_FORCE_REFRESH: "Aggiorna schermo" +STR_NEXT: "Succ" +STR_NEXT_PAGE: "Pagina successiva »" +STR_PREV: "Prec" +STR_PREV_PAGE: "« Pagina precedente" +STR_SEARCH: "Cerca" +STR_SHIFT: "Maiusc" +STR_SHIFT_CAPS: "MAIUSC" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 6907d14b..056f7c0b 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -140,6 +140,15 @@ STR_INVERTED: "Инверсия" STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_PREV_NEXT: "Назад/Вперёд" STR_NEXT_PREV: "Вперёд/Назад" +STR_ABC: "abc" +STR_FORCE_REFRESH: "Обновить экран" +STR_NEXT: "Далее" +STR_NEXT_PAGE: "Следующая страница »" +STR_PREV: "Назад" +STR_PREV_PAGE: "« Предыдущая страница" +STR_SEARCH: "Поиск" +STR_SHIFT: "Shift" +STR_SHIFT_CAPS: "SHIFT" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index f9a6d042..cd1344b4 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -137,6 +137,15 @@ STR_INVERTED: "Invertido" STR_LANDSCAPE_CCW: "Horizontal (antihorario)" STR_PREV_NEXT: "Ant./Sig." STR_NEXT_PREV: "Sig./Ant." +STR_ABC: "abc" +STR_FORCE_REFRESH: "Actualizar pantalla" +STR_NEXT: "Sig" +STR_NEXT_PAGE: "Página siguiente »" +STR_PREV: "Ant" +STR_PREV_PAGE: "« Página anterior" +STR_SEARCH: "Buscar" +STR_SHIFT: "Mayús" +STR_SHIFT_CAPS: "MAYÚS" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" From f2750356dea880278a5b71e57e1d1ff47236ffb0 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 14 Apr 2026 15:12:29 +0200 Subject: [PATCH 4/4] Full refresh for non-portrait mode --- src/activities/home/BookInfoActivity.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/activities/home/BookInfoActivity.cpp b/src/activities/home/BookInfoActivity.cpp index 7cf571a3..82791c6b 100644 --- a/src/activities/home/BookInfoActivity.cpp +++ b/src/activities/home/BookInfoActivity.cpp @@ -127,7 +127,14 @@ void BookInfoActivity::render(RenderLock&&) { // Fast path: only the description page / button hints changed. Reuse the // already-rendered header/cover/meta by clearing just those two bands. Cap // consecutive partial renders to bound e-ink ghosting buildup. - if (fullRenderDone && loadSucceeded && !description.empty() && partialRenderCount < MAX_PARTIAL_RENDERS) { + // + // Requires Portrait orientation: the hints-gutter location is derived below + // assuming the gutter sits at the bottom, which is only true in Portrait + // (PortraitInverted puts it at the top; landscape orientations put it on a + // side). In non-Portrait we fall through to a full render to avoid leaving + // stale button labels in the real gutter. + if (fullRenderDone && loadSucceeded && !description.empty() && partialRenderCount < MAX_PARTIAL_RENDERS && + renderer.getOrientation() == GfxRenderer::Portrait) { renderer.fillRect(descBandX, descBandY, descBandWidth, descBandHeight, false); renderer.fillRect(0, hintsBandY, renderer.getScreenWidth(), hintsBandHeight, false); renderDescriptionAndHints();