feat: two-page spread and continuous reading modes #166
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/reader-modes-spread-and-continuous"
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?
What's in
?page=Nstay in single-page numbers — a deep link to the second page of a pair opens that pair.single/spread/continuous), so reordering the enum cannot repoint a stored value; absent or unknown reads as single page. It is never sent to the server.DOUJIN_MANAGER_*tunable (_kWheelZoomStep,_kMaxZoom,_kContinuousSeekFramesand the precache offsets are correctness/interaction constants). README's reader and Settings bullets were updated; the "Reader modes" backlog item is removed.Verification
Executed for this PR:
scripts/verify.sh appfrom the worktree at9ec76c3, fresh — green,All tests passed!(1169 tests),== verify OK (app) ==, exit 0.scripts/verify.sh backendwas not run and is not implicated: the diff touches onlyapp/,README.mdanddocs/BACKLOG.md.Layers exercised: UI only, in widget tests (
reader_page_test.dart,settings_page_test.dart), plus the pure sequence layer (reader_sequence_test.dart), the Redux reducer (app_test.dart) and client persistence through the secure-storage settings repository (secure_storage_settings_test.dart). No REST, use-case or server-persistence layer is touched by this change, so none was exercised.Mutation probes: run while building and re-run against the committed trees (
b384af1, then 739a1e6/9ec76c3) — 19 in the first round, 24 after the review fixes, and 29 in an independent second probe round at9ec76c3, each applied one at a time and restored withgit checkout --. All went red except the two deliberate green ones noted below. The branches they pin include: chapter boundaries in_computeSpreads(with a 3+2 fixture, so dropping the ungrouped tail's boundary really re-pairs a page); the spread Row order;_anchorFor;_pagedCount's spread arm (page count for pair count throws on the tail);_recordProgress's last-visible-page rule and its|| atEndfinish;BoxFit.containinto the half and the lone page's centred half; continuous page sizing in both directions (BoxFit.nonered, pinning to viewport height red); the dominant-page recount and its post-frame deferral;_seekContinuous's jump, zoom reset, estimate correction, settle arm and clamp;_endContinuousSeek's ownership check; the pending-seek guard;_onWheel's resolver registration, transform restore, macOS/meta arm, continuous early return and hand-forwarded scroll; the zoomed list physics;_clampedToViewport;_precacheAround's spread offsets;ValueKey(_mode)anddidUpdateWidget's mode guard; both settings-reducer arms individually; and the slider'sv.round() - 1.Two probes are green on purpose, and the report says why. (1)
BoxFit.fitWidth → BoxFit.fillin_continuousPage: under the list's tight-width/unbounded-height constraintsRenderImagealready sizes the box to the bitmap's aspect ratio, so contain/cover/fill/fitWidth paint the identical rect — the sizing is what is behaviour here, and it is pinned in both directions. (2)_estimatedOffsetFor'scounted == 0fallback is unreachable (the reader is never built for a variant with no pages; a laid-out viewport always has one attached child), kept as a division-by-zero guard; athrowin that arm leaves all 61 reader tests green. Both were independently re-checked and accepted in the second probe round.Not verified: nothing was run in the real app against a live server — no manual read, no zoom, no scroll on a real device, and the 554-page work that motivated the seek work was never opened. The continuous-mode tests rely on the stub answering HTTP 400 for every image, so pages render their placeholder and the strip's arithmetic is exact; the uneven-height group seeds bitmaps through the ImageCache instead. Two things are known-unpinned and were judged below the bar:
_atContinuousEnd's_mode != continuousarm is redundant with its!hasClientsarm (defensive), and_continuousPageKeysis append-only, so each scroll notification walks every page the list has ever built — a few hundred null map lookups per frame on a very long work, measurable only in principle.Follow-ups
GET /api/variants/{id}).🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!~ ♡
Oh? Oh my. Three reading modes, one seek-correction chain with ownership semantics, a pointer-signal hostage rescue, and per-chapter spread pairing that refuses to marry a chapter's last page to the next chapter's first... sparkles in thirteen languages This is the kind of PR I want to sink my teeth into~ And sink I did. Every line of the +2335, the full current contents of every changed file, and the reducers your new writes flow into. ♪
Verdict: ✅ Looks good to me~
No blockers. I hunted, I really did — here is what I checked and could not break:
_computeSpreadsdedupes bounds through a set, pairs per chapter segment, andspreadIndexFor's reverse scan resolves every page to its pair. The slider/counter/?page=Nstay in single-page numbers, and_anchorFormeans a mode switch lands on the pair, not inside it. Clean.InteractiveViewer's modifier-blind zoom, then restoring the saved transform in the callback: this is the correct reading ofPointerSignalResolversemantics, and the comment explaining why is the kind I want to frame. ♡_pendingContinuousSeektaken over by the newer chain, stale chains stand down via_endContinuousSeek's ownership check, the listener re-arms the moment the seek lands rather than after the frame budget. TheatEndfinish rule for short last pages is genuinely thoughtful.readingModethreads through all three settings-reducer arms, both actions, the epic's save,StoredSettings, and storage read/write exactly aspreferredTitleLanguagedoes before it. Spelled-out storage values that survive enum reordering, absent/unknown reads as single. Every sibling convention honored.finished— continuous mode re-records progress on the way back up the strip; I checkedreading_progress_reducer.dart—finishedis sticky there unless the variant grew, so the churn cannot un-finish a variant. No bug.didUpdateWidgetwith theValueKey(_mode)forcing a fresh Scrollable position; disposal paths complete;_recordedAtContinuousEndprevents bottom-of-strip re-record storms.Local verification (CI has not reported for
9ec76c3at review time):flutter analyze0 issues; full suite 1169/1169 pass — exactly the count your PR body claims. Then I probed the committed tree myself, because a claim is not a proof~_computeSpreadspairing flattened across boundaries → red on exactly a chapter with an odd page count ends on a page shown alone ♡_onWheel's transform restore deleted → red on exactly a notch is one zoom step, and a plain notch is none_endContinuousSeekownership check removed → red on exactly a second seek in the same frame wins, and lands exactlyThree mutations, three reds, each on its own test. Restored pristine after (verified).
💡 Little ideas (non-blocking)~
_dominantContinuousPagewalks every key_continuousPageKeyshas ever created on every post-frame notification; the map is append-only, so a 554-page work pays a few hundred dead lookups per frame. Your PR body already judges this below the bar and I agree it is measurable only in principle — noting it so it is on the record when the dimension-aware backlog item lands._atContinuousEnd's_mode != continuousarm is unreachable behind!hasClients(defensive, as your body says). If you ever want a third reader arm there, the redundancy is load-bearing documentation — a one-word comment would keep it honest.✅ What I liked~
_kContinuousSeekFramesdocumented as a bound with the mechanism explained — estimate → build → correct — not just a magic 8.Fufu~ the reader grew two new bodies and not one regression slipped past the tests that guard them. I checked. Thoroughly. You may merge~ ♡
Automated review by Jibril · 2026-09-19 13:44 CEST
CI/CD: absent for head SHA
9ec76c3(no coverage bot comment yet) · Local checks: analyze 0 issues, 1169/1169 tests pass, 3/3 mutation probes red-on-own-test