Simple reader: immersive single-page viewer with keyboard navigation (Phase 10 complete) #46

Merged
bjoern merged 5 commits from feat/simple-reader into main 2026-07-02 14:49:35 +02:00
Owner

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

  • Single page, fit to window, on black, with pan/zoom via InteractiveViewer
    (resets on page turn). Full-resolution originals with bearer auth.
  • Reading direction honored: right-to-left doujins page in manga order —
    ArrowLeft / left click-half = next page; everything mirrors for left-to-right.
  • Navigation: direction-aware arrows, Space, Home/End, Esc (back to detail),
    mouse wheel, and click on either screen half.
  • Overlay chrome appears on mouse movement and auto-hides after 3 s (suspended while
    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.
  • Preloading: two pages ahead and one behind are precached; the global image cache
    ceiling is raised 100 MiB → 512 MiB (kImageCacheMaxBytes) so full-res neighbors
    aren't immediately evicted. Benefits the detail/editor grids too.

Design notes

  • Dedicated ReaderState slice: seeds instantly from the detail slice when opened
    from the detail page (no refetch, no spinner flash), fetches doujin+variant itself for
    deep links. The reader reducer runs as a second pass in appReducer so it can sync its
    doujin copy when a rating round-trip emits DoujinDetailLoadedAction (already claimed
    by 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.
  • Page turns go through jumpToPage with 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: Slider expands
to 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

  • App: 311 tests green, flutter analyze clean, flutter build linux succeeds.
    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.
  • Manually verified against the local test server: RTL and LTR doujins, wheel + keys +
    click zones + zoom together, chapter jump, rating from the overlay, Esc back to detail.
## 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 - **Single page, fit to window, on black**, with pan/zoom via `InteractiveViewer` (resets on page turn). Full-resolution originals with bearer auth. - **Reading direction honored**: right-to-left doujins page in manga order — ArrowLeft / left click-half = next page; everything mirrors for left-to-right. - **Navigation**: direction-aware arrows, Space, Home/End, Esc (back to detail), mouse wheel, and click on either screen half. - **Overlay chrome** appears on mouse movement and auto-hides after 3 s (suspended while 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. - **Preloading**: two pages ahead and one behind are precached; the global image cache ceiling is raised 100 MiB → 512 MiB (`kImageCacheMaxBytes`) so full-res neighbors aren't immediately evicted. Benefits the detail/editor grids too. ## Design notes - **Dedicated `ReaderState` slice**: seeds instantly from the detail slice when opened from the detail page (no refetch, no spinner flash), fetches doujin+variant itself for deep links. The reader reducer runs as a second pass in `appReducer` so it can sync its doujin copy when a rating round-trip emits `DoujinDetailLoadedAction` (already claimed by 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. - Page turns go through `jumpToPage` with 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: `Slider` expands to 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 - App: **311 tests** green, `flutter analyze` clean, `flutter build linux` succeeds. 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. - Manually verified against the local test server: RTL and LTR doujins, wheel + keys + click zones + zoom together, chapter jump, rating from the overlay, Esc back to detail.
ReaderState holds the doujin + variant being read; the load epic seeds
from the detail slice when it already has the pair (instant open) and
fetches both otherwise (deep links). The reader reducer runs as a
second pass in appReducer so it can sync its doujin copy when a rating
round-trip emits DoujinDetailLoadedAction (already claimed by the
detail reducer). ReaderSequence flattens pages into reading order with
the same chapter rule as the detail grid, exposing chapter titles and
jump-menu boundaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the reader at /read/:doujinId/:variantId?page=N, registered outside
the ShellRoute per ADR 0019 — the only screen without the navigation
rail. One full-res page at a time on black, honoring reading direction
(RTL = manga order): direction-aware arrows, Space, Home/End, Esc,
mouse wheel, and click-to-turn screen halves; pan/zoom via
InteractiveViewer (reset each page). Overlay chrome (title, chapter,
rating stars, page slider with RTL fill, chapter jump menu) appears on
mouse movement and auto-hides after 3s. Adjacent pages are precached;
the global image cache ceiling is raised to 512 MiB since full-res
decodes dwarf the 100 MiB default. Detail thumbnails now push the
reader at the tapped page's number.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test(app): reader widget coverage + slider layout fix
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 1m24s
a9c16a4378
Widget suite: open-at-page with chapter title, direction-aware arrows
for RTL and LTR, Space/Home/End with clamping, click-to-turn zones,
chapter jump menu, rating round-trip syncing the overlay, Escape
popping back, and the error path. Detail-page test asserts thumbnails
push /read/:doujinId/:variantId?page=N.

The click-zone test caught a real bug: Slider expands to any bounded
height, so the bottom bar invisibly filled the screen and swallowed
page-turn clicks. Pinned the slider height and added a layout guard
assertion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Flutter Coverage

File Line coverage
lib/app/store.dart 100.0% (10 of 10)
lib/data/models/doujin_models.dart 83.3% (35 of 42)
lib/data/models/doujin_models.g.dart 39.7% (120 of 302)
lib/domain/entities/stored_settings.dart 100.0% (6 of 6)
lib/presentation/state/app_state.dart 60.0% (9 of 15)
lib/presentation/middleware/epics.dart 83.7% (256 of 306)
lib/presentation/state/reducers.dart 100.0% (9 of 9)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/languages.dart 100.0% (8 of 8)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/domain/entities/entity_model.dart 100.0% (1 of 1)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 34.5% (19 of 55)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 89.5% (77 of 86)
lib/presentation/middleware/editor_epics.dart 56.2% (155 of 276)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/upload_epics.dart 99.0% (102 of 103)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/editor_actions.dart 45.9% (17 of 37)
lib/presentation/state/actions/entity_actions.dart 50.0% (8 of 16)
lib/presentation/state/actions/library_actions.dart 33.3% (6 of 18)
lib/presentation/state/actions/metadata_actions.dart 100.0% (3 of 3)
lib/presentation/state/actions/reader_actions.dart 100.0% (4 of 4)
lib/presentation/state/actions/settings_actions.dart 100.0% (5 of 5)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/detail_reducer.dart 95.8% (23 of 24)
lib/presentation/state/reducers/editor_reducer.dart 92.5% (99 of 107)
lib/presentation/state/reducers/entity_reducer.dart 98.5% (66 of 67)
lib/presentation/state/reducers/library_reducer.dart 98.0% (98 of 100)
lib/presentation/state/reducers/metadata_reducer.dart 100.0% (15 of 15)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (15 of 15)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (25 of 25)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/people/people_page.dart 46.7% (7 of 15)
lib/presentation/widgets/entity_editor.dart 88.4% (122 of 138)
lib/presentation/widgets/entity_management_page.dart 80.8% (172 of 213)
lib/presentation/pages/reader/reader_page.dart 84.8% (168 of 198)
lib/core/constants.dart 22.2% (2 of 9)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/detail/detail_page.dart 76.3% (167 of 219)
lib/presentation/pages/detail/variant_tabs_panel.dart 94.4% (168 of 178)
lib/presentation/widgets/cover_thumbnail.dart 82.4% (28 of 34)
lib/presentation/widgets/star_rating.dart 100.0% (70 of 70)
lib/presentation/pages/reader/reader_overlay.dart 94.2% (49 of 52)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (27 of 27)
lib/presentation/pages/characters/characters_page.dart 46.7% (7 of 15)
lib/presentation/pages/editor/editor_page.dart 77.6% (59 of 76)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/presentation/pages/editor/association_picker.dart 94.7% (107 of 113)
lib/presentation/pages/editor/associations_tab.dart 75.0% (90 of 120)
lib/presentation/pages/editor/chapter_panel.dart 0.0% (0 of 73)
lib/presentation/pages/editor/doujin_list_pane.dart 66.2% (43 of 65)
lib/presentation/pages/editor/edit_title_dialog.dart 91.5% (54 of 59)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/editor/editor_pane.dart 72.2% (39 of 54)
lib/presentation/pages/editor/new_doujin_dialog.dart 68.2% (30 of 44)
lib/presentation/pages/editor/metadata_tab.dart 72.4% (92 of 127)
lib/presentation/pages/editor/variants_tab.dart 1.1% (1 of 89)
lib/presentation/pages/editor/page_grid.dart 88.2% (277 of 314)
lib/presentation/pages/editor/upload_panel.dart 39.7% (60 of 151)
lib/presentation/pages/editor/variant_dialog.dart 0.0% (0 of 64)
lib/presentation/pages/tags/tags_page.dart 100.0% (14 of 14)
lib/core/theme.dart 96.9% (31 of 32)
lib/data/api_client.dart 88.9% (8 of 9)
lib/data/repositories/health_repository.dart 69.2% (18 of 26)
lib/app/app.dart 67.4% (29 of 43)
lib/presentation/pages/settings/settings_page.dart 100.0% (86 of 86)
lib/presentation/layout/main_layout.dart 90.5% (19 of 21)
lib/presentation/pages/circles/circles_page.dart 46.7% (7 of 15)
lib/presentation/pages/library/library_page.dart 77.4% (123 of 159)
lib/presentation/pages/series/series_page.dart 46.7% (7 of 15)
lib/presentation/widgets/smart_filter_bar.dart 52.8% (122 of 231)
lib/data/repositories/doujin_api_repository.dart 22.3% (80 of 358)

Total: 69.2% (3780 of 5462)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 100.0% (10 of 10) | | lib/data/models/doujin_models.dart | 83.3% (35 of 42) | | lib/data/models/doujin_models.g.dart | 39.7% (120 of 302) | | lib/domain/entities/stored_settings.dart | 100.0% (6 of 6) | | lib/presentation/state/app_state.dart | 60.0% (9 of 15) | | lib/presentation/middleware/epics.dart | 83.7% (256 of 306) | | lib/presentation/state/reducers.dart | 100.0% (9 of 9) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/domain/entities/entity_model.dart | 100.0% (1 of 1) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 34.5% (19 of 55) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 89.5% (77 of 86) | | lib/presentation/middleware/editor_epics.dart | 56.2% (155 of 276) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/upload_epics.dart | 99.0% (102 of 103) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/editor_actions.dart | 45.9% (17 of 37) | | lib/presentation/state/actions/entity_actions.dart | 50.0% (8 of 16) | | lib/presentation/state/actions/library_actions.dart | 33.3% (6 of 18) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (3 of 3) | | lib/presentation/state/actions/reader_actions.dart | 100.0% (4 of 4) | | lib/presentation/state/actions/settings_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/detail_reducer.dart | 95.8% (23 of 24) | | lib/presentation/state/reducers/editor_reducer.dart | 92.5% (99 of 107) | | lib/presentation/state/reducers/entity_reducer.dart | 98.5% (66 of 67) | | lib/presentation/state/reducers/library_reducer.dart | 98.0% (98 of 100) | | lib/presentation/state/reducers/metadata_reducer.dart | 100.0% (15 of 15) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (15 of 15) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (25 of 25) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/people/people_page.dart | 46.7% (7 of 15) | | lib/presentation/widgets/entity_editor.dart | 88.4% (122 of 138) | | lib/presentation/widgets/entity_management_page.dart | 80.8% (172 of 213) | | lib/presentation/pages/reader/reader_page.dart | 84.8% (168 of 198) | | lib/core/constants.dart | 22.2% (2 of 9) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/detail/detail_page.dart | 76.3% (167 of 219) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 94.4% (168 of 178) | | lib/presentation/widgets/cover_thumbnail.dart | 82.4% (28 of 34) | | lib/presentation/widgets/star_rating.dart | 100.0% (70 of 70) | | lib/presentation/pages/reader/reader_overlay.dart | 94.2% (49 of 52) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (27 of 27) | | lib/presentation/pages/characters/characters_page.dart | 46.7% (7 of 15) | | lib/presentation/pages/editor/editor_page.dart | 77.6% (59 of 76) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/presentation/pages/editor/association_picker.dart | 94.7% (107 of 113) | | lib/presentation/pages/editor/associations_tab.dart | 75.0% (90 of 120) | | lib/presentation/pages/editor/chapter_panel.dart | 0.0% (0 of 73) | | lib/presentation/pages/editor/doujin_list_pane.dart | 66.2% (43 of 65) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.5% (54 of 59) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/editor/editor_pane.dart | 72.2% (39 of 54) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 68.2% (30 of 44) | | lib/presentation/pages/editor/metadata_tab.dart | 72.4% (92 of 127) | | lib/presentation/pages/editor/variants_tab.dart | 1.1% (1 of 89) | | lib/presentation/pages/editor/page_grid.dart | 88.2% (277 of 314) | | lib/presentation/pages/editor/upload_panel.dart | 39.7% (60 of 151) | | lib/presentation/pages/editor/variant_dialog.dart | 0.0% (0 of 64) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (14 of 14) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/data/api_client.dart | 88.9% (8 of 9) | | lib/data/repositories/health_repository.dart | 69.2% (18 of 26) | | lib/app/app.dart | 67.4% (29 of 43) | | lib/presentation/pages/settings/settings_page.dart | 100.0% (86 of 86) | | lib/presentation/layout/main_layout.dart | 90.5% (19 of 21) | | lib/presentation/pages/circles/circles_page.dart | 46.7% (7 of 15) | | lib/presentation/pages/library/library_page.dart | 77.4% (123 of 159) | | lib/presentation/pages/series/series_page.dart | 46.7% (7 of 15) | | lib/presentation/widgets/smart_filter_bar.dart | 52.8% (122 of 231) | | lib/data/repositories/doujin_api_repository.dart | 22.3% (80 of 358) | **Total: 69.2% (3780 of 5462)**
Member

🔮 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 PageController instead of the store, ReaderSequence sharing 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 stealing DoujinDetailLoadedAction from the detail reducer... mmm~ you thought about this. I love it when you think. ♪

And that layout bug you caught — the Slider silently 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 to height: 40 and left a comment so no one un-pins it. Good. Very good. ♡

I checked the things that matter:

  • ReaderSequence vs variant_tabs_panel display numbers — identical rule (chapters by sortOrder, pages by sortOrder, ungrouped last). The page=N deep link and the thumbnail number will always agree. Verified line-by-line~
  • RTL mirroring — arrows, click-halves, and slider Directionality all flip together. Correct.
  • The reducer second pass — reader actions fall through the main chain untouched; the DoujinDetailLoadedAction id-guard only fires for a matching doujin. Clean.
  • AuthAuthorization: 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)~

  1. main.dart:14 / constants.dart kImageCacheMaxBytes — 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. ♡
  2. reader_page.dart:_onWheel — the wheel Listener sits above the InteractiveViewer, 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.
  3. reader_page.dart:_goTo — the clamped == _index guard reads _index, which only updates in _onPageChanged after jumpToPage. Since jumpToPage is synchronous with no animation this is harmless, but a burst of inputs within one frame could no-op one. Truly cosmetic; jumpToPage saves you here.
  4. Coveragereader_page.dart at 84.7% (CI #714), the misses clustered in the precache/wheel branches. reader_sequence.dart, reader_reducer.dart, reader_actions.dart all 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 successful flutter analyze + flutter test --coverage; steps are fail-fast, so the comment only appears on green) · Local checks: skipped per CI-evidence policy

## 🔮 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 `PageController` instead of the store, `ReaderSequence` sharing 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 stealing `DoujinDetailLoadedAction` from the detail reducer... mmm~ you *thought* about this. I love it when you think. ♪ And that layout bug you caught — the `Slider` silently 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 to `height: 40` *and* left a comment so no one un-pins it. Good. Very good. ♡ I checked the things that matter: - **`ReaderSequence` vs `variant_tabs_panel` display numbers** — identical rule (chapters by `sortOrder`, pages by `sortOrder`, ungrouped last). The `page=N` deep link and the thumbnail number will always agree. Verified line-by-line~ - **RTL mirroring** — arrows, click-halves, and slider `Directionality` all flip together. Correct. - **The reducer second pass** — reader actions fall through the main chain untouched; the `DoujinDetailLoadedAction` id-guard only fires for a matching doujin. Clean. - **Auth** — `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)~ 1. **`main.dart:14` / `constants.dart` `kImageCacheMaxBytes`** — 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. ♡ 2. **`reader_page.dart:_onWheel`** — the wheel `Listener` sits *above* the `InteractiveViewer`, 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. 3. **`reader_page.dart:_goTo`** — the `clamped == _index` guard reads `_index`, which only updates in `_onPageChanged` *after* `jumpToPage`. Since `jumpToPage` is synchronous with no animation this is harmless, but a burst of inputs within one frame could no-op one. Truly cosmetic; `jumpToPage` saves you here. 4. **Coverage** — `reader_page.dart` at 84.7% (CI #714), the misses clustered in the precache/wheel branches. `reader_sequence.dart`, `reader_reducer.dart`, `reader_actions.dart` all 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 successful `flutter analyze` + `flutter test --coverage`; steps are fail-fast, so the comment only appears on green) · Local checks: skipped per CI-evidence policy*
polish(app): reader review feedback
Some checks failed
Flutter CI / analyze-and-test (pull_request) Failing after 1m22s
04f9bd6bb8
Raise the image-cache ceiling lazily on first reader open instead of at
startup, so sessions that never read keep the 100 MiB default. Base the
_goTo guard on the PageController (the position's source of truth)
rather than the onPageChanged-lagged index. Document the wheel-always-
turns-pages choice in ADR 0019.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test(app): deharden upload queue tests against CI timing
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 1m26s
d4cd854922
The queue tests poll real timers with a fixed 100x5ms budget that a
loaded CI runner can exceed, and the drained-variant-refresh assertion
raced the refresh epic (an extra async hop after the last item
completes). Replace the counted loop with a deadline-based waitFor
(15s, pump-before-check) and explicitly wait for the refresh instead
of asserting it immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bjoern force-pushed feat/simple-reader from d4cd854922
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 1m26s
to 981fa32e84
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 1m23s
2026-07-02 14:45:24 +02:00
Compare
bjoern merged commit c9c66d660e into main 2026-07-02 14:49:35 +02:00
bjoern deleted branch feat/simple-reader 2026-07-02 14:49:35 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!46
No description provided.