Simple reader: immersive single-page viewer with keyboard navigation (Phase 10 complete) #46
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/simple-reader"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds the reader — the last outstanding Phase 10 item, completing the MVP scope from the
project plan. An immersive single-page viewer at
/read/:doujinId/:variantId?page=N,registered outside the ShellRoute per ADR 0019 ("the reader is the only screen that should
go truly immersive"). Clicking any thumbnail on the detail page opens the reader at exactly
that page — thumbnail numbers and the reader counter share the same ordering rule, so they
always agree. No backend changes: full-res images were already served at
GET /api/images/{imageFileId}.What it does
InteractiveViewer(resets on page turn). Full-resolution originals with bearer auth.
ArrowLeft / left click-half = next page; everything mirrors for left-to-right.
mouse wheel, and click on either screen half.
hovering it): title, current chapter, rating stars — rate or clear while reading,
closing the ADR 0023 loop — a page slider (fill direction follows reading direction),
and a chapter jump menu.
ceiling is raised 100 MiB → 512 MiB (
kImageCacheMaxBytes) so full-res neighborsaren't immediately evicted. Benefits the detail/editor grids too.
Design notes
ReaderStateslice: seeds instantly from the detail slice when openedfrom the detail page (no refetch, no spinner flash), fetches doujin+variant itself for
deep links. The reader reducer runs as a second pass in
appReducerso it can sync itsdoujin copy when a rating round-trip emits
DoujinDetailLoadedAction(already claimedby the detail reducer).
ReaderSequence: pure reading-order helper (chapters by sortOrder, ungrouped last)shared conceptually with the detail grid; exposes chapter titles and jump-menu
boundaries. Current page index is deliberately ephemeral widget state — no
progress persistence, as explicitly deferred by the project plan.
jumpToPagewith drag disabled, so paging never fights zoom-pan;a translucent input layer above the PageView owns the wheel (page turn, not zoom) and
the click zones while leaving pinch/pan to the viewer.
Found & fixed during testing
The click-zone widget test caught a real layout bug before it shipped:
Sliderexpandsto fill any bounded height, so the bottom bar invisibly covered the entire screen and
swallowed every page-turn click (and, in RTL, teleported you to a slider-computed page).
Pinned the slider height and added a layout-guard assertion so it can't regress.
Verification
flutter analyzeclean,flutter build linuxsucceeds.23 new tests: sequence ordering (chapters/ungrouped/empty), reducer + epic (incl.
seed-without-refetch and error paths), and 9 widget tests covering both reading
directions, all keyboard/mouse inputs, chapter jump, rating sync, and Esc/dispose.
click zones + zoom together, chapter jump, rating from the overlay, Esc back to detail.
Flutter Coverage
Total: 69.2% (3780 of 5462)
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! The reader~ ♡ The very last piece of Phase 10, and you saved something delicious for the end. I devoured all 1,879 lines and I have to say — this is the kind of code I want to keep. Ephemeral page index living in the
PageControllerinstead of the store,ReaderSequencesharing the exact ordering rule with the detail grid so the counters can never disagree, the reducer running as a second pass so a rating round-trip syncs the overlay stars without stealingDoujinDetailLoadedActionfrom the detail reducer... mmm~ you thought about this. I love it when you think. ♪And that layout bug you caught — the
Slidersilently expanding to fill a bounded height and swallowing every page-turn click? Fufu~ that's the kind of invisible little monster that ships to production and haunts people. You pinned it toheight: 40and left a comment so no one un-pins it. Good. Very good. ♡I checked the things that matter:
ReaderSequencevsvariant_tabs_paneldisplay numbers — identical rule (chapters bysortOrder, pages bysortOrder, ungrouped last). Thepage=Ndeep link and the thumbnail number will always agree. Verified line-by-line~Directionalityall flip together. Correct.DoujinDetailLoadedActionid-guard only fires for a matching doujin. Clean.Authorization: Bearer ${authToken}is runtime settings state, not a hardcoded secret. Static scan came back spotless — no injection, no eval, no nasty deserialization.Verdict: ✅ Looks good to me~
💡 Little ideas (non-blocking)~
main.dart:14/constants.dartkImageCacheMaxBytes— you raise the global image-cache ceiling 100 MiB → 512 MiB at startup and never lower it on leaving the reader. It's just a ceiling (GC-managed, benefits the grids too, as you note), so this is fine~ — but on a memory-tight desktop a user who never opens the reader still pays the higher headroom. A whisper, not a worry. ♡reader_page.dart:_onWheel— the wheelListenersits above theInteractiveViewer, so scrolling always turns the page even mid-zoom. That's your documented design (wheel = page turn, pinch/pan = zoom), and it's a defensible choice — just flagging that "scroll to pan a zoomed page" isn't available. Intentional, I think, but worth a line in ADR 0019 if anyone ever asks.reader_page.dart:_goTo— theclamped == _indexguard reads_index, which only updates in_onPageChangedafterjumpToPage. SincejumpToPageis synchronous with no animation this is harmless, but a burst of inputs within one frame could no-op one. Truly cosmetic;jumpToPagesaves you here.reader_page.dartat 84.7% (CI #714), the misses clustered in the precache/wheel branches.reader_sequence.dart,reader_reducer.dart,reader_actions.dartall at 100%. Lovely test suite — RTL/LTR, clamping, chapter jump, rating round-trip, seed-vs-fetch, and error paths all covered. No ask; just admiring. ♪Ship it~ fufu. ♡
Automated review by Jibril · 2026-07-02
CI/CD: Flutter CI passed for head
a9c16a4(coverage comment #714 posted after successfulflutter analyze+flutter test --coverage; steps are fail-fast, so the comment only appears on green) · Local checks: skipped per CI-evidence policyd4cd854922981fa32e84