feat: slim footnotes support (#1031)
## Summary **What is the goal of this PR?** Implement support for footnotes in epub files. It is based on #553, but simplified — removed the parts which complicated the code and burden the CPU/RAM. This version supports basic footnotes and lets the user jump from location to location inside the epub. **What changes are included?** - `FootnoteEntry` struct — A small POD struct (number[24], href[64]) shared between parser, page storage, and UI. - Parser: `<a href>` detection (`ChapterHtmlSlimParser`) — During a single parsing pass, internal epub links are detected and collected as footnotes. The link text is underlined to hint navigability. Bracket/whitespace normalization is applied to the display label (e.g. [1] → 1). - Footnote-to-page assignment (`ChapterHtmlSlimParser`, `Page`) — Footnotes are attached to the exact page where their anchor word appears, tracked via a cumulative word counter during layout, surviving paragraph splits and the 750-word mid-paragraph safety flush. - Page serialization (`Page`, `Section`) — Footnotes are serialized/deserialized per page (max 16 per page). Section cache version bumped to 14 to force a clean rebuild. - Href → spine resolution (`Epub`) — `resolveHrefToSpineIndex()` maps an href (e.g. `chapter2.xhtml#note1`) to its spine index by filename matching. - Footnotes menu + activity (`EpubReaderMenuActivity`, `EpubReaderFootnotesActivity`) — A new "Footnotes" entry in the reader menu lists all footnote links found on the current page. The user scrolls and selects to navigate. - Navigate & restore (`EpubReaderActivity`) — `navigateToHref()` saves the current spine index and page number, then jumps to the target. The Back button restores the saved position when the user is done reading the footnote. **Additional Context** **What was removed vs #553:** virtual spine items (`addVirtualSpineItem`, `isVirtualSpineItem`), two-pass parsing, `<aside>` content extraction to temp HTML files, `<p class="note">` paragraph note extraction, `replaceHtmlEntities` (master already has `lookupHtmlEntity`), `footnotePages` / `buildFilteredChapterList`, `noterefCallback` / `Noteref` struct, and the stack size increase from 8 KB to 24 KB (not needed without two-pass parsing and virtual file I/O on the render task). **Performance:** Single-pass parsing. No new heap allocations in the hot path — footnote text is collected into fixed stack buffers (char[24], char[64]). Active runtime memory is ~2.8 KB worst-case (one page × 16 footnotes × 88 bytes, mirrored in `currentPageFootnotes`). Flash usage is unchanged at 97.4%; RAM stays at 31%. **Known limitations:** When clicking a footnote, it jumps to the start of the HTML file instead of the specific anchor. This could be problematic for books that don't have separate files for each footnote. (no element-id-to-page mapping yet - will be another PR soon). --- ### AI Usage Did you use AI tools to help write this code? _**< PARTIALLY>**_ Claude Opus 4.6 was used to do most of the migration, I checked manually its work, and fixed some stuff, but I haven't review all the changes yet, so feedback is welcomed. --------- Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Arthur Tazhitdinov
parent
35f324ccce
commit
e107493bed
@@ -0,0 +1,95 @@
|
||||
#include "EpubReaderFootnotesActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void EpubReaderFootnotesActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EpubReaderFootnotesActivity::onExit() { ActivityWithSubactivity::onExit(); }
|
||||
|
||||
void EpubReaderFootnotesActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
onGoBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(footnotes.size())) {
|
||||
onSelectFootnote(footnotes[selectedIndex].href);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buttonNavigator.onNext([this] {
|
||||
if (!footnotes.empty()) {
|
||||
selectedIndex = (selectedIndex + 1) % footnotes.size();
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
buttonNavigator.onPrevious([this] {
|
||||
if (!footnotes.empty()) {
|
||||
selectedIndex = (selectedIndex - 1 + footnotes.size()) % footnotes.size();
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EpubReaderFootnotesActivity::render(Activity::RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FOOTNOTES), true, EpdFontFamily::BOLD);
|
||||
|
||||
if (footnotes.empty()) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 90, tr(STR_NO_FOOTNOTES));
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr int startY = 50;
|
||||
constexpr int lineHeight = 36;
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int marginLeft = 20;
|
||||
|
||||
const int visibleCount = std::max(1, (renderer.getScreenHeight() - startY) / lineHeight);
|
||||
if (selectedIndex < scrollOffset) scrollOffset = selectedIndex;
|
||||
if (selectedIndex >= scrollOffset + visibleCount) scrollOffset = selectedIndex - visibleCount + 1;
|
||||
|
||||
for (int i = scrollOffset; i < static_cast<int>(footnotes.size()) && i < scrollOffset + visibleCount; i++) {
|
||||
const int y = startY + (i - scrollOffset) * lineHeight;
|
||||
const bool isSelected = (i == selectedIndex);
|
||||
|
||||
if (isSelected) {
|
||||
renderer.fillRect(0, y, screenWidth, lineHeight, true);
|
||||
}
|
||||
|
||||
// Show footnote number and abbreviated href
|
||||
std::string label = footnotes[i].number;
|
||||
if (label.empty()) {
|
||||
label = tr(STR_LINK);
|
||||
}
|
||||
renderer.drawText(UI_10_FONT_ID, marginLeft, y + 4, label.c_str(), !isSelected);
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
Reference in New Issue
Block a user