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)