EPUB reader (#280)
EPUBs open in the note pane, beside pictures and PDFs
(lib/src/ui/attachment_view.dart), on Android, Linux and Windows.
Why not a package
epub_view was tried and dropped (spike result in #280): every epubx
wants xml ^6, image ^3 and archive ^3 against the app’s xml ^7 and
pdfrx’s image ^4.8 / archive ^4, and with overrides it still does not
compile (epubx imports xml’s removed src/xml/builder.dart;
flutter_html 3.0.0 does not compile on the current html/csslib).
flutter_epub_viewer_kit sits on the same two packages, has no Windows or
Linux, and loads google_fonts from the network.
The way taken
Read the EPUB with what the app has, draw it with the app’s read view.
How it is built
lib/src/epub/xhtml_markdown.dart—XhtmlMarkdown: one chapter’s XHTML to Markdown (ChapterMarkdown: the text, andid→ line for the links into it). Headings, paragraphs, emphasis, strike, inline code,<br>as a hard break, lists (nested,start=), blockquotes,<pre>,<hr>, tables (GFM, first row as header), pictures (img, SVGimage— inline, as a browser has them), links. Every text character Markdown or the app’s own extensions read as syntax is backslash-escaped (#tags,$math,[[links,*,|,=,~,^,&,!…; the extension masker andfindInlineMathhonour the backslash), and a paragraph opening with-,+or1.is escaped too. Pictures and links go through callbacks that name them (picture(src),link(href)). The book’s CSS is not read.lib/src/epub/epub_document.dart—readEpub(path, pictureDir)/openEpub(path, cacheDir)(on an isolate, the file stat’ed there too):META-INF/container.xml→ OPF (elements matched by local name,<opf:item>too) → manifest and spine → every chapter converted and joined in spine order with a---between, line numbers tracked. Table of contents from the EPUB 3 nav (propertiescontainingnav,nav[epub:type=toc] ol) or the EPUB 2 NCX (spine@toc), asEpubContentsEntry (title, line, depth). Pictures namedepub-picture:Nand extracted once topictureDir/N.ext, the folder named by a sha1 of the book’s path, size and mtime (pictureDirOf) under<app cache>/epub. Links into the book namedepub-link:N(EpubDocument.lineOfLink), external ones (http:,mailto:…) kept as they are.lib/src/ui/epub_pane.dart—EpubPane:AttachmentViewhands it every.epub. A spinner while the book is read, the unreadable message on any error. The book is aMarkdownReadViewin the shell’s note column; its pictures resolve throughembedResolver,epub-link:NtapsjumpToLine, the rest go tolaunchUrl. The whole pane, its row included, sits in aThemeof the books’ look (epubThemeOf) and aMediaQueryat their text size (epubTextScalerOf), rebuilt onEpubLooks.revision.- The books’ look —
lib/src/epub/epub_look.dart:EpubLook(theme id or null for the app’s,AppBrightnessor null for the app’s,EpubFont, text scale), four device keys of the library’s settings (epubTheme,epubBrightness,epubFont,epubTextScale), part ofLibraryConfigasepubLook.LibrarySession.epubLook/setEpubLookread and keep it; the session publishes it, with the theme it names resolved (a custom theme from the app database, none when it is gone), to the globalEpubLooks(epub_looks.dart) when the library opens, when it is set, and when a custom theme is saved or deleted; closing the library resets it.lib/src/ui/epub_theme.dartturns it into the pane’sThemeData: the app’s own theme untouched when the books ask for nothing else, otherwisebuildAppThemeat the books’ brightness, itstextThemein the books’ face. The faces: Literata, bundled (assets/fonts/literata/, regular, italic, bold, bold italic, SIL OFL 1.1 with itsOFL.txtas an asset);serifandmonospacewith the common platform faces as fallbacks (Windows has noserifalias); the app’s own for sans serif. Code blocks stay monospace whatever the face. lib/src/ui/epub_look_sheet.dart—showEpubLookSheet/EpubLookPanel: theme and brightness dropdowns, a chip per face (each named in itself), a text-size slider that publishes toEpubLookswhile it moves and keeps the size when let go. The book’s Aa button opens it (EpubPane.onEditLook, from the shell throughShellDetailPane/AttachmentView), and so does Settings → Appearance → Book appearance (SettingsKeys.epubLook, in the settings search too), whose value isepubLookSummary.lib/src/ui/attachment_bar.dart—AttachmentBar, the row under every attachment, with anactionsslot left of Open in default app. The book puts its Aa and Outline buttons there (Outline disabled while the book is read or when it has no contents): a sheet of the contents indented by depth, the chapter at the top of the view marked, a tap jumps.
Tests: test/unit/xhtml_markdown_test.dart,
test/unit/epub_document_test.dart (books built by
test/fakes/epub_builder.dart), test/unit/epub_look_test.dart,
test/widget/epub_pane_test.dart, test/widget/epub_look_sheet_test.dart,
test/widget/epub_look_settings_test.dart.
Where the reader was left (#281)
lib/src/reading/: a BookLocation is a place in a document in its own
terms — a PDF’s page and how far down it, an EPUB’s chapter (its spine
path), a line of that chapter and how far into it. Per chapter, not per
line of the whole book: the book is Markdown converted from its XHTML, so
a change to XhtmlMarkdown moves lines, and kept per chapter the place
moves within one chapter at most (EpubDocument.chapters,
locationAt, lineOfLocation, which keeps a place past a shortened
chapter inside it). Links into a document (#282) and annotations (#284)
point with the same type, with room for a character range (#283).
A link names a place in its #fragment (#282): BookLocation.fromFragment
reads page=N (the PDF open-parameters form Obsidian uses, other keys
passed over) and chapter=<path>&line=N, values percent-decoded
(core/percent.dart); toFragment writes them, escaping what would end
the fragment or the link. EpubDocument.lineOfLocation finds a chapter
by its exact path, else whatever its case (a Markdown href’s fragment),
else its file name alone. The shell hands the fragment to the pane as the
tab’s anchor, the one a note gets for a heading, with the reload token
for the same link followed again; the pane goes there on open, over the
saved position, and on a new fragment or token later — not on the same
fragment handed again as a tab comes back.
The link button on the row of a book (EpubBar) and of a PDF
(PdfDocumentView, which now brings its own row) is a PlaceLinkButton:
it asks the pane for the place being read — the book’s top line, the next
one when the view is mostly past it, labelled with the contents entry
being read; the PDF’s page, labelled pdfPageLabel — and copies
placeLink (lib/src/links/place_link.dart): the library-relative path,
which always resolves, the fragment, and the label as the alias or the
Markdown text, the href percent-encoded as Obsidian writes one.
ReadingPositions keeps them in .niman/reading.json, by library-relative
path, each with the time it was read (at); it is a library state file,
synced and merged book by book (mergeReadingJson, see sync.md).
Nothing is cached: a pane reads the file when it opens a document, and the
writes of a library run one after the other, each reading the file afresh.
NoteOps carries the entries of a file or folder it moves or renames.
ReadingTracker is what a pane feeds: placed once the document is set
where it was left, moved as the view moves (the EPUB pane from the read
view’s topAnchor on every scroll, the PDF view from pdfrx’s
visibleRect against its page layout), flush when it lets the document
go. It writes a place once the reader rests a second, and never a place
the view merely settled on (BookLocation.isNear): a book opened and not
moved is not a reading, and must not outdate, by its newer at, the place
another device wrote.
Tests: test/unit/reading_positions_test.dart,
test/unit/pdf_location_test.dart, test/unit/note_ops_reading_test.dart,
the #281 groups of test/widget/epub_pane_test.dart and
test/unit/state_merge_test.dart. The PDF view has no widget test:
pdfium does not load under flutter test.
Later, maybe
- A chapter-per-scan cache if very long books open slowly: today every open converts the whole book on an isolate.