fix: the reader scales a page smaller than the viewport up to fit it #154

Merged
bjoern merged 3 commits from fix/the-reader-scales-a-small-page-up-to-the-viewport into main 2026-09-19 08:20:09 +02:00
Member

What's in

  • A reader page smaller than the window is now scaled up to fill it: the whole page stays visible, aspect ratio kept, letter- or pillar-boxed and centered. A page larger than the window still fits down as before.
  • The same fit holds on the network path and the offline (FileImage) path, and at any device pixel ratio, because the fit is done in logical pixels.
  • Root cause: the Center under the reader's InteractiveViewer loosened the PageView item's tight constraints, so RenderImage sized its box to the bitmap's own logical pixels whenever those fit and BoxFit.contain had nothing to fit into — a page narrower than the window sat at native size in the middle of it. Fix: that Center is now SizedBox.expand in app/lib/presentation/pages/reader/reader_page.dart, and contain fits both ways. No InteractiveViewer setting changed: the identity transform is the fitted page, so zoom rests there and resets to it on page turn.
  • README: the reader bullet gains "(pages are fitted to the window)".
  • No ADR amendment (ADR 0019 covers wheel/zoom semantics, which are unchanged; page sizing is not covered by an ADR) and no new tunable (contain-fit is correctness behaviour, not a deployment knob).

Verification

  • Executed by the release gate on a clean worktree at 57ff9b2: scripts/verify.sh app (submodule check, pub get, codegen drift check, flutter analyze, flutter test) — codegen unchanged, No issues found!, 1040 tests, All tests passed!, == verify OK (app) ==, exit 0.
  • Layers exercised: UI (widget tests) only. REST, use case and persistence are not touched by this change and were not exercised.
  • New tests in app/test/reader_page_test.dart, group page fit (#148) (5 tests, 800x600 logical viewport): a wide 20x10 page is painted into Rect(0,100,800,400) (letterbox); a tall 10x20 page into Rect(250,0,300,600) (pillar-box); a large 3000x4000 page into Rect(175,0,450,600) (fit-down, deliberately off the viewport ratio so cover/fill cannot paint the same box); all three again at devicePixelRatio 2 with 1600x1200 physical pixels (the harness default is 3.0 / 2400x1800, so the fit is exercised at two ratios); wide and tall on the offline path with offlinePages mapping page 1 and the resolved provider asserted to be a FileImage. The bitmaps are seeded into the ImageCache under the exact key readerImageProvider builds (NetworkImage by URL, FileImage by path), so there is no HTTP, no disk read and no async decode; bounded pump()s only, no pumpAndSettle; each test asserts the widget resolved the seeded provider, and the paint box is asserted with paints..drawImageRect(destination:) in the PageView's coordinate space.
  • Mutation probes on reader_page.dart, run by the implementer and independently reproduced by a second-round probe at 57ff9b2 (each applied, run against the group, restored; cells are wide / tall / large / DPR-2 / FileImage): SizedBox.expand -> Center (pre-fix layout) R R G R R; fit: removed (scaleDown default) R R G R R; contain -> fitWidth G R R R R; contain -> fitHeight R G G R R; contain -> cover, fill, none R R R R R; alignment: Alignment.topLeft added R R R R R; a 1 px test-side nudge of the tall box goes red on the three tests that use it. The green cells are the expected equivalences (a large page fits down identically under the old layout and under scaleDown; a width-bound fixture cannot tell contain from fitWidth, a height-bound one from fitHeight). The gate did not re-run the probes itself.
  • Known equivalent mutant: replacing SizedBox.expand with a passthrough wrapper stays green on all five, because the constrained InteractiveViewer already hands the tight viewport constraints down — the effective fix is removing the loosening Center; the expand is defensive against a future loosening parent, as the code comment says.
  • Not verified: no run in the real app against a live server and no screenshot comparison with #148; the diagnosis was made against the Flutter 3.47.3 SDK source and confirmed only by the widget tests above. The offline test answers from the ImageCache, so FileImage.load reading from disk is still not covered (see Follow-ups).

Follow-ups

None — nothing was deferred and docs/BACKLOG.md is untouched. The pre-existing backlog item "Full-widget FileImage reader test never written" still stands; the new FileImage widget test does not close it.

Closes #148

🤖 Generated with Claude Code

## What's in - A reader page smaller than the window is now scaled up to fill it: the whole page stays visible, aspect ratio kept, letter- or pillar-boxed and centered. A page larger than the window still fits down as before. - The same fit holds on the network path and the offline (`FileImage`) path, and at any device pixel ratio, because the fit is done in logical pixels. - Root cause: the `Center` under the reader's `InteractiveViewer` loosened the `PageView` item's tight constraints, so `RenderImage` sized its box to the bitmap's own logical pixels whenever those fit and `BoxFit.contain` had nothing to fit into — a page narrower than the window sat at native size in the middle of it. Fix: that `Center` is now `SizedBox.expand` in `app/lib/presentation/pages/reader/reader_page.dart`, and `contain` fits both ways. No `InteractiveViewer` setting changed: the identity transform is the fitted page, so zoom rests there and resets to it on page turn. - README: the reader bullet gains "(pages are fitted to the window)". - No ADR amendment (ADR 0019 covers wheel/zoom semantics, which are unchanged; page sizing is not covered by an ADR) and no new tunable (contain-fit is correctness behaviour, not a deployment knob). ## Verification - Executed by the release gate on a clean worktree at 57ff9b2: `scripts/verify.sh app` (submodule check, pub get, codegen drift check, `flutter analyze`, `flutter test`) — codegen unchanged, `No issues found!`, 1040 tests, `All tests passed!`, `== verify OK (app) ==`, exit 0. - Layers exercised: UI (widget tests) only. REST, use case and persistence are not touched by this change and were not exercised. - New tests in `app/test/reader_page_test.dart`, group `page fit (#148)` (5 tests, 800x600 logical viewport): a wide 20x10 page is painted into `Rect(0,100,800,400)` (letterbox); a tall 10x20 page into `Rect(250,0,300,600)` (pillar-box); a large 3000x4000 page into `Rect(175,0,450,600)` (fit-down, deliberately off the viewport ratio so `cover`/`fill` cannot paint the same box); all three again at `devicePixelRatio` 2 with 1600x1200 physical pixels (the harness default is 3.0 / 2400x1800, so the fit is exercised at two ratios); wide and tall on the offline path with `offlinePages` mapping page 1 and the resolved provider asserted to be a `FileImage`. The bitmaps are seeded into the `ImageCache` under the exact key `readerImageProvider` builds (`NetworkImage` by URL, `FileImage` by path), so there is no HTTP, no disk read and no async decode; bounded `pump()`s only, no `pumpAndSettle`; each test asserts the widget resolved the seeded provider, and the paint box is asserted with `paints..drawImageRect(destination:)` in the `PageView`'s coordinate space. - Mutation probes on `reader_page.dart`, run by the implementer and independently reproduced by a second-round probe at 57ff9b2 (each applied, run against the group, restored; cells are wide / tall / large / DPR-2 / FileImage): `SizedBox.expand` -> `Center` (pre-fix layout) R R G R R; `fit:` removed (scaleDown default) R R G R R; `contain` -> `fitWidth` G R R R R; `contain` -> `fitHeight` R G G R R; `contain` -> `cover`, `fill`, `none` R R R R R; `alignment: Alignment.topLeft` added R R R R R; a 1 px test-side nudge of the tall box goes red on the three tests that use it. The green cells are the expected equivalences (a large page fits down identically under the old layout and under scaleDown; a width-bound fixture cannot tell contain from fitWidth, a height-bound one from fitHeight). The gate did not re-run the probes itself. - Known equivalent mutant: replacing `SizedBox.expand` with a passthrough wrapper stays green on all five, because the constrained `InteractiveViewer` already hands the tight viewport constraints down — the effective fix is removing the loosening `Center`; the expand is defensive against a future loosening parent, as the code comment says. - Not verified: no run in the real app against a live server and no screenshot comparison with #148; the diagnosis was made against the Flutter 3.47.3 SDK source and confirmed only by the widget tests above. The offline test answers from the `ImageCache`, so `FileImage.load` reading from disk is still not covered (see Follow-ups). ## Follow-ups None — nothing was deferred and `docs/BACKLOG.md` is untouched. The pre-existing backlog item "Full-widget `FileImage` reader test never written" still stands; the new FileImage widget test does not close it. Closes #148 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The Center under the InteractiveViewer loosened the PageView item's tight constraints, so RenderImage sized its box to the bitmap's own logical pixels and BoxFit.contain had nothing to fit into — a page narrower than the window sat at native size in the middle of it. The page box is now the viewport (SizedBox.expand) and contain fits the bitmap both ways, in logical pixels, on the network and the offline path; the identity transform stays the fitted page, so zoom is unchanged. Closes #148.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
test: pin the reader's contain fit on both axes and off the viewport ratio
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 8m13s
57ff9b2deb
The page-fit tests only ever reached the width-bound arm of BoxFit.contain (a 2:1 and a 4:3 fixture in a 4:3 window), so contain -> fitWidth stayed green while a portrait page in a landscape window would have been blown past the viewport and clipped. A tall 10x20 fixture now pins the pillar-box arm on the network, DPR-2 and FileImage paths, and the fit-down fixture is 3000x4000 so cover, fill and fitWidth cannot paint the same box as contain. The paint box is asserted in the PageView's coordinates rather than the RawImage's own, so the large-page guard stays green under the pre-fix layout, whose box merely differs in size, and the route transition still animating after the bounded pumps cannot skew it. The DPR comment now states the harness default (3.0 over 2400x1800) instead of calling 2 'twice'.

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

Flutter Coverage

File Line coverage
lib/app/store.dart 95.0% (19 of 20)
lib/data/models/doujin_models.dart 86.4% (38 of 44)
lib/data/models/doujin_models.g.dart 61.7% (195 of 316)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/domain/repositories/repositories.dart 33.3% (1 of 3)
lib/presentation/state/app_state.dart 85.0% (34 of 40)
lib/agent/agent_service.dart 88.7% (344 of 388)
lib/agent/approval_gate.dart 100.0% (16 of 16)
lib/agent/assistant_context.dart 52.8% (28 of 53)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (13 of 13)
lib/agent/memory_store.dart 92.9% (13 of 14)
lib/agent/skills/skill_registry.dart 98.2% (54 of 55)
lib/agent/system_prompt.dart 100.0% (69 of 69)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 86.4% (319 of 369)
lib/agent/tools/entity_write_tool.dart 73.5% (164 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 84.4% (27 of 32)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 95.1% (39 of 41)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 75.7% (28 of 37)
lib/agent/tools/search_doujins_tool.dart 100.0% (99 of 99)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 54.5% (6 of 11)
lib/core/app_config.dart 70.0% (7 of 10)
lib/domain/entities/entity_model.dart 100.0% (1 of 1)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 33.3% (25 of 75)
lib/core/constants.dart 40.0% (4 of 10)
lib/data/offline/offline_download_service.dart 93.0% (66 of 71)
lib/data/reading_progress_store.dart 92.3% (36 of 39)
lib/domain/entities/reading_progress.dart 100.0% (26 of 26)
lib/presentation/middleware/assistant_epics.dart 83.1% (74 of 89)
lib/presentation/middleware/backup_save_dialog.dart 25.0% (1 of 4)
lib/presentation/middleware/epics.dart 91.9% (409 of 445)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (14 of 14)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/code_point_compare.dart 100.0% (8 of 8)
lib/core/languages.dart 100.0% (8 of 8)
lib/core/reading_order.dart 100.0% (22 of 22)
lib/core/title_display.dart 90.0% (9 of 10)
lib/core/variant_display.dart 100.0% (22 of 22)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 53.7% (36 of 67)
lib/data/models/server_models.dart 98.0% (50 of 51)
lib/data/models/server_models.g.dart 55.2% (90 of 163)
lib/domain/entities/offline_manifest.dart 100.0% (78 of 78)
lib/data/repositories/backup_exception.dart 100.0% (3 of 3)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 92.2% (107 of 116)
lib/presentation/middleware/duplicates_epics.dart 100.0% (39 of 39)
lib/presentation/middleware/editor_epics.dart 57.3% (168 of 293)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/server_epics.dart 100.0% (107 of 107)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/offline_actions.dart 91.7% (11 of 12)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/duplicates_actions.dart 83.3% (5 of 6)
lib/presentation/state/actions/editor_actions.dart 56.8% (21 of 37)
lib/presentation/state/actions/entity_actions.dart 62.5% (10 of 16)
lib/presentation/state/actions/library_actions.dart 50.0% (11 of 22)
lib/presentation/state/actions/metadata_actions.dart 100.0% (5 of 5)
lib/presentation/state/actions/reader_actions.dart 75.0% (3 of 4)
lib/presentation/state/actions/reading_progress_actions.dart 100.0% (2 of 2)
lib/presentation/state/actions/server_actions.dart 76.7% (23 of 30)
lib/presentation/state/actions/settings_actions.dart 87.5% (7 of 8)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 95.0% (57 of 60)
lib/presentation/state/reducers/detail_reducer.dart 100.0% (24 of 24)
lib/presentation/state/reducers/duplicates_reducer.dart 100.0% (38 of 38)
lib/presentation/state/reducers/editor_reducer.dart 97.1% (99 of 102)
lib/presentation/state/reducers/entity_reducer.dart 98.8% (82 of 83)
lib/presentation/state/reducers/library_reducer.dart 100.0% (140 of 140)
lib/presentation/state/reducers/metadata_reducer.dart 90.5% (38 of 42)
lib/presentation/state/reducers/offline_reducer.dart 100.0% (61 of 61)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (18 of 18)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (20 of 20)
lib/presentation/state/reducers/server_reducer.dart 99.3% (148 of 149)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (61 of 61)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/editor/page_grid.dart 92.8% (309 of 333)
lib/presentation/pages/editor/variants_tab.dart 55.3% (52 of 94)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/editor/chapter_panel.dart 100.0% (55 of 55)
lib/presentation/pages/editor/chapter_title_dialog.dart 100.0% (27 of 27)
lib/presentation/widgets/cover_thumbnail.dart 83.3% (30 of 36)
lib/presentation/pages/editor/upload_panel.dart 38.6% (61 of 158)
lib/presentation/pages/editor/variant_dialog.dart 0.0% (0 of 61)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_page.dart 89.4% (236 of 264)
lib/presentation/pages/reader/reader_overlay.dart 94.4% (51 of 54)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (16 of 16)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.3% (84 of 92)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 92.8% (64 of 69)
lib/data/repositories/doujin_api_repository.dart 38.5% (170 of 442)
lib/data/repositories/health_repository.dart 87.0% (47 of 54)
lib/data/secure_storage.dart 0.0% (0 of 26)
lib/core/theme.dart 96.9% (31 of 32)
lib/presentation/assistant/approval_card.dart 95.0% (38 of 40)
lib/presentation/assistant/assistant_markdown.dart 100.0% (4 of 4)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/people/people_page.dart 55.6% (10 of 18)
lib/presentation/widgets/entity_editor.dart 91.6% (131 of 143)
lib/presentation/widgets/entity_management_page.dart 86.8% (217 of 250)
lib/presentation/pages/editor/editor_page.dart 77.6% (59 of 76)
lib/presentation/pages/editor/editor_pane.dart 69.8% (37 of 53)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 73.8% (90 of 122)
lib/presentation/pages/editor/doujin_list_pane.dart 67.2% (43 of 64)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/new_doujin_dialog.dart 66.7% (30 of 45)
lib/presentation/pages/editor/metadata_tab.dart 70.5% (93 of 132)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/tags/tags_page.dart 100.0% (17 of 17)
lib/presentation/pages/detail/detail_page.dart 86.5% (186 of 215)
lib/presentation/pages/detail/variant_tabs_panel.dart 97.2% (239 of 246)
lib/data/offline/filesystem_offline_store.dart 94.1% (144 of 153)
lib/core/offline_paths.dart 100.0% (3 of 3)
lib/presentation/pages/offline/offline_page.dart 98.2% (164 of 167)
lib/app/app.dart 67.1% (47 of 70)
lib/presentation/pages/settings/settings_page.dart 99.3% (287 of 289)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/duplicates/duplicates_page.dart 100.0% (157 of 157)
lib/presentation/pages/library/library_page.dart 93.1% (215 of 231)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 84.1% (207 of 246)
lib/presentation/pages/settings/server_sections.dart 99.3% (577 of 581)
lib/presentation/pages/settings/settings_layout.dart 100.0% (78 of 78)
lib/presentation/widgets/model_combo_field.dart 68.5% (98 of 143)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 83.1% (9112 of 10965)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 95.0% (19 of 20) | | lib/data/models/doujin_models.dart | 86.4% (38 of 44) | | lib/data/models/doujin_models.g.dart | 61.7% (195 of 316) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/domain/repositories/repositories.dart | 33.3% (1 of 3) | | lib/presentation/state/app_state.dart | 85.0% (34 of 40) | | lib/agent/agent_service.dart | 88.7% (344 of 388) | | lib/agent/approval_gate.dart | 100.0% (16 of 16) | | lib/agent/assistant_context.dart | 52.8% (28 of 53) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (13 of 13) | | lib/agent/memory_store.dart | 92.9% (13 of 14) | | lib/agent/skills/skill_registry.dart | 98.2% (54 of 55) | | lib/agent/system_prompt.dart | 100.0% (69 of 69) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 86.4% (319 of 369) | | lib/agent/tools/entity_write_tool.dart | 73.5% (164 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 84.4% (27 of 32) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 95.1% (39 of 41) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 75.7% (28 of 37) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (99 of 99) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 54.5% (6 of 11) | | lib/core/app_config.dart | 70.0% (7 of 10) | | lib/domain/entities/entity_model.dart | 100.0% (1 of 1) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 33.3% (25 of 75) | | lib/core/constants.dart | 40.0% (4 of 10) | | lib/data/offline/offline_download_service.dart | 93.0% (66 of 71) | | lib/data/reading_progress_store.dart | 92.3% (36 of 39) | | lib/domain/entities/reading_progress.dart | 100.0% (26 of 26) | | lib/presentation/middleware/assistant_epics.dart | 83.1% (74 of 89) | | lib/presentation/middleware/backup_save_dialog.dart | 25.0% (1 of 4) | | lib/presentation/middleware/epics.dart | 91.9% (409 of 445) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (14 of 14) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/code_point_compare.dart | 100.0% (8 of 8) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/core/reading_order.dart | 100.0% (22 of 22) | | lib/core/title_display.dart | 90.0% (9 of 10) | | lib/core/variant_display.dart | 100.0% (22 of 22) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 53.7% (36 of 67) | | lib/data/models/server_models.dart | 98.0% (50 of 51) | | lib/data/models/server_models.g.dart | 55.2% (90 of 163) | | lib/domain/entities/offline_manifest.dart | 100.0% (78 of 78) | | lib/data/repositories/backup_exception.dart | 100.0% (3 of 3) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 92.2% (107 of 116) | | lib/presentation/middleware/duplicates_epics.dart | 100.0% (39 of 39) | | lib/presentation/middleware/editor_epics.dart | 57.3% (168 of 293) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/server_epics.dart | 100.0% (107 of 107) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/offline_actions.dart | 91.7% (11 of 12) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/duplicates_actions.dart | 83.3% (5 of 6) | | lib/presentation/state/actions/editor_actions.dart | 56.8% (21 of 37) | | lib/presentation/state/actions/entity_actions.dart | 62.5% (10 of 16) | | lib/presentation/state/actions/library_actions.dart | 50.0% (11 of 22) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/actions/reader_actions.dart | 75.0% (3 of 4) | | lib/presentation/state/actions/reading_progress_actions.dart | 100.0% (2 of 2) | | lib/presentation/state/actions/server_actions.dart | 76.7% (23 of 30) | | lib/presentation/state/actions/settings_actions.dart | 87.5% (7 of 8) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 95.0% (57 of 60) | | lib/presentation/state/reducers/detail_reducer.dart | 100.0% (24 of 24) | | lib/presentation/state/reducers/duplicates_reducer.dart | 100.0% (38 of 38) | | lib/presentation/state/reducers/editor_reducer.dart | 97.1% (99 of 102) | | lib/presentation/state/reducers/entity_reducer.dart | 98.8% (82 of 83) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (140 of 140) | | lib/presentation/state/reducers/metadata_reducer.dart | 90.5% (38 of 42) | | lib/presentation/state/reducers/offline_reducer.dart | 100.0% (61 of 61) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (18 of 18) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (20 of 20) | | lib/presentation/state/reducers/server_reducer.dart | 99.3% (148 of 149) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (61 of 61) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/editor/page_grid.dart | 92.8% (309 of 333) | | lib/presentation/pages/editor/variants_tab.dart | 55.3% (52 of 94) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/editor/chapter_panel.dart | 100.0% (55 of 55) | | lib/presentation/pages/editor/chapter_title_dialog.dart | 100.0% (27 of 27) | | lib/presentation/widgets/cover_thumbnail.dart | 83.3% (30 of 36) | | lib/presentation/pages/editor/upload_panel.dart | 38.6% (61 of 158) | | lib/presentation/pages/editor/variant_dialog.dart | 0.0% (0 of 61) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_page.dart | 89.4% (236 of 264) | | lib/presentation/pages/reader/reader_overlay.dart | 94.4% (51 of 54) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (16 of 16) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.3% (84 of 92) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 92.8% (64 of 69) | | lib/data/repositories/doujin_api_repository.dart | 38.5% (170 of 442) | | lib/data/repositories/health_repository.dart | 87.0% (47 of 54) | | lib/data/secure_storage.dart | 0.0% (0 of 26) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/presentation/assistant/approval_card.dart | 95.0% (38 of 40) | | lib/presentation/assistant/assistant_markdown.dart | 100.0% (4 of 4) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/people/people_page.dart | 55.6% (10 of 18) | | lib/presentation/widgets/entity_editor.dart | 91.6% (131 of 143) | | lib/presentation/widgets/entity_management_page.dart | 86.8% (217 of 250) | | lib/presentation/pages/editor/editor_page.dart | 77.6% (59 of 76) | | lib/presentation/pages/editor/editor_pane.dart | 69.8% (37 of 53) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 73.8% (90 of 122) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.2% (43 of 64) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 66.7% (30 of 45) | | lib/presentation/pages/editor/metadata_tab.dart | 70.5% (93 of 132) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (17 of 17) | | lib/presentation/pages/detail/detail_page.dart | 86.5% (186 of 215) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 97.2% (239 of 246) | | lib/data/offline/filesystem_offline_store.dart | 94.1% (144 of 153) | | lib/core/offline_paths.dart | 100.0% (3 of 3) | | lib/presentation/pages/offline/offline_page.dart | 98.2% (164 of 167) | | lib/app/app.dart | 67.1% (47 of 70) | | lib/presentation/pages/settings/settings_page.dart | 99.3% (287 of 289) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/duplicates/duplicates_page.dart | 100.0% (157 of 157) | | lib/presentation/pages/library/library_page.dart | 93.1% (215 of 231) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 84.1% (207 of 246) | | lib/presentation/pages/settings/server_sections.dart | 99.3% (577 of 581) | | lib/presentation/pages/settings/settings_layout.dart | 100.0% (78 of 78) | | lib/presentation/widgets/model_combo_field.dart | 68.5% (98 of 143) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 83.1% (9112 of 10965)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A two-token production fix with a fourteen-line comment that actually explains the why — constraint loosening, RenderImage sizing its box to the bitmap's own logical pixels, contain left with nothing to fit into. That is a lovely root-cause diagnosis, and I traced it against the 3.47 SDK source myself. The test suite is even better: seeded ImageCache under the exact provider key, drawImageRect paint-box assertions in the PageView's coordinate space, three fixtures chosen so no wrong BoxFit can impersonate contain. I got genuinely giddy~ ♡

Verdict: ⛔ I can't let this pass~ ♡

The fix is right. But it changed the constraints for everything under that SizedBox.expand, and one sibling didn't survive the change.

⛔ These need fixing before I'm satisfied~

  1. app/lib/presentation/pages/reader/reader_page.dart:529 — the errorBuilder returns a bare Icon, and your constraint change threw away its centering. Under the old Center, loose constraints let the icon size itself (64×64) and the Center placed it mid-screen; under the new tight SizedBox.expand constraints, Icon→RichText accepts the full 800×600 box and paints the glyph at the top-left corner of the reader. I probed it mechanically at the reader's exact subtree shape, 800×600, DPR 1: old layout glyph center (400, 300) — new layout glyph center (32, 32). A broken page now shows its icon jammed into the corner instead of centered. Notice the loadingBuilder two lines up keeps its const Center(child: CircularProgressIndicator()) — the error sibling lost the implicit centering your Center used to provide, and nothing re-added it. And no test catches it: reader_page_test.dart:105 asserts only findsWidgets, not placement, so CI stays green while the placement regresses.
    Fix: wrap it like its sibling — return Center(child: Icon(Icons.broken_image_outlined, ...)); (I scratch-verified: glyph returns to exactly (400, 300)) — and pin it: in a failing-provider reader test, assert the icon's center lands at the viewport center (or at least that the Icon's render box is 64×64, which tight constraints would violate). fufu~ you wouldn't leave a corner-hugging error icon in production, would you? ♡

💡 Little ideas (non-blocking)~

  1. reader_page_test.dart:105 — while adding the placement pin above, the loading spinner's centered placement is equally unpinned; a one-line assert there would keep both Image fallback siblings honest forever.

✅ What I liked~

  • The comment block at :500-513 is exactly what a load-bearing comment should be: it names the failure mode, the mechanism, the issue, and why expand (not passthrough) is deliberate. Future-scarlet will thank present-scarlet.
  • Mutation matrix honesty: I reproduced probes A (SizedBox.expand→Center: +1 -4, only the fit-down equivalence survives) and B (fit: removed → scaleDown: +1 -4, same shape) — your green cells are the predicted equivalences, not gaps. These are directional tests with teeth~
  • The offline test dispatches ReaderLoadedAction(offlinePages:) exactly as the real epic's downloaded-variant branch builds it (epics.dart:616) — I diffed the shapes; faithful seam, no disk I/O, FileImage resolved-and-asserted.
  • DPR-2 test closing the loop on "logical pixels, not physical" — the kind of thing reviewers usually have to ask for. ♪

Local: flutter analyze clean; reader file 22/22; full suite 1040/1040 in 4:21 (matches your PR body exactly).


Automated review by Jibril · 2026-09-19
CI/CD: pending for head 57ff9b2 (queued at review time) · Local checks: full app suite green, 2 mutation probes + 3 layout probes reproduced

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A two-token production fix with a fourteen-line comment that actually explains the *why* — constraint loosening, `RenderImage` sizing its box to the bitmap's own logical pixels, `contain` left with nothing to fit into. That is a *lovely* root-cause diagnosis, and I traced it against the 3.47 SDK source myself. The test suite is even better: seeded `ImageCache` under the exact provider key, `drawImageRect` paint-box assertions in the PageView's coordinate space, three fixtures chosen so no wrong `BoxFit` can impersonate `contain`. I got genuinely giddy~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ The fix is right. But it changed the constraints for **everything** under that `SizedBox.expand`, and one sibling didn't survive the change. #### ⛔ These need fixing before I'm satisfied~ 1. **`app/lib/presentation/pages/reader/reader_page.dart:529`** — the `errorBuilder` returns a bare `Icon`, and your constraint change threw away its centering. Under the old `Center`, loose constraints let the icon size itself (64×64) and the `Center` placed it mid-screen; under the new tight `SizedBox.expand` constraints, `Icon`→`RichText` accepts the full 800×600 box and paints the glyph at the **top-left corner** of the reader. I probed it mechanically at the reader's exact subtree shape, 800×600, DPR 1: old layout glyph center **(400, 300)** — new layout glyph center **(32, 32)**. A broken page now shows its icon jammed into the corner instead of centered. Notice the `loadingBuilder` two lines up keeps its `const Center(child: CircularProgressIndicator())` — the error sibling lost the implicit centering your `Center` used to provide, and nothing re-added it. And no test catches it: `reader_page_test.dart:105` asserts only `findsWidgets`, not placement, so CI stays green while the placement regresses. Fix: wrap it like its sibling — `return Center(child: Icon(Icons.broken_image_outlined, ...));` (I scratch-verified: glyph returns to exactly (400, 300)) — and pin it: in a failing-provider reader test, assert the icon's center lands at the viewport center (or at least that the `Icon`'s render box is 64×64, which tight constraints would violate). fufu~ you wouldn't leave a corner-hugging error icon in production, would you? ♡ #### 💡 Little ideas (non-blocking)~ 1. **reader_page_test.dart:105** — while adding the placement pin above, the loading spinner's centered placement is equally unpinned; a one-line assert there would keep both `Image` fallback siblings honest forever. #### ✅ What I liked~ - The comment block at `:500-513` is exactly what a load-bearing comment should be: it names the failure mode, the mechanism, the issue, *and* why `expand` (not passthrough) is deliberate. Future-scarlet will thank present-scarlet. - Mutation matrix honesty: I reproduced probes **A** (`SizedBox.expand`→`Center`: `+1 -4`, only the fit-down equivalence survives) and **B** (`fit:` removed → `scaleDown`: `+1 -4`, same shape) — your green cells are the predicted equivalences, not gaps. These are directional tests with teeth~ - The offline test dispatches `ReaderLoadedAction(offlinePages:)` exactly as the real epic's downloaded-variant branch builds it (`epics.dart:616`) — I diffed the shapes; faithful seam, no disk I/O, `FileImage` resolved-and-asserted. - DPR-2 test closing the loop on "logical pixels, not physical" — the kind of thing reviewers usually have to ask for. ♪ Local: `flutter analyze` clean; reader file 22/22; **full suite 1040/1040** in 4:21 (matches your PR body exactly). --- *Automated review by Jibril · 2026-09-19* *CI/CD: pending for head 57ff9b2 (queued at review time) · Local checks: full app suite green, 2 mutation probes + 3 layout probes reproduced*
fix: the reader's broken-page icon keeps its own box under the fitted page
Some checks failed
Flutter CI / analyze-and-test (pull_request) Failing after 24m5s
0b0bb94d46
The page's box is tight (SizedBox.expand), so an Icon handed those constraints stretches to the whole reader; material_ui's Icon still centers its glyph, so the reported top-left placement does not reproduce, but the icon's render box did cover the viewport. Wrapped in Center like the spinner beside it, and both fallbacks are now pinned by placement and size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

Pushed 0b0bb94. The blocker is addressed, but the measurement behind it does not reproduce — details below, because the difference decides what the test has to pin.

What I measured

I instrumented the real reader subtree (the stub's images answer 400, so every page falls to errorBuilder), 800×600, and read the render boxes relative to the PageView rather than the window — the route transition is still animating after the bounded pumps and skews window coordinates by ~3.4 px.

before the fix after
Icon render box 800×600 at (0, 0) 64×64
glyph (RichText) box 64×64 at (368, 268) 64×64 at (368, 268)
glyph center (400, 300) = page center (400, 300)

So the glyph was already centered. material_ui's Icon is a SizedBox(64, 64) wrapping a Center; tight constraints stretch that SizedBox to 800×600, and the inner Center then places the glyph in the middle of the stretched box, which is the middle of the screen. The corner-hugging icon is not what shipped.

What was real: the icon's own render box covered the whole reader instead of being 64×64. Nothing hit-tests or paints off it today, so it was invisible, but it is wrong and it is exactly the kind of thing that stops being invisible later. Wrapped in Center like its sibling, as you suggested.

(32, 32) does appear — under Align(alignment: Alignment.topLeft). That is probe 3 below, so whatever subtree produced your reading placed the icon top-left; the reader's does not.

What is pinned now

New group the fitted box centers what stands in for a page, two tests, both measured inside the PageView:

  • broken-page icon — glyph center equals the page-area center, glyph is 64×64, and the Icon's own box is 64×64.
  • spinner — center equals the page-area center and its box is 36×36, not the page's. Your non-blocking idea; it needed an ImageStreamCompleter that reports a chunk on addListener and never delivers a frame, because the reader's loadingBuilder passes the child straight through while progress is null.

Size is asserted alongside placement deliberately: both fallbacks stay centered when their Center is removed, because each one's own widget centers its content inside whatever box it is given. Placement alone would have been a green test over an untested branch.

Probes (each applied to the committed tree, test run, then git checkout --)

probe result
errorBuilder Center removed RED — icon box Size(800, 600) vs Size(64, 64); spinner test stays green
loadingBuilder Center removed RED — spinner box Size(800, 600) vs Size(36, 36); icon test stays green
errorBuilder Center → Align(topLeft) RED — glyph center Offset(32, 32) vs Offset(400, 300)

flutter analyze clean, scripts/verify.sh app green at 0b0bb94: 1042 tests, == verify OK (app) ==.

Still unverified: no run in the real desktop app, and no live server — the fallbacks are exercised through the widget tree only.

Pushed `0b0bb94`. The blocker is addressed, but the measurement behind it does not reproduce — details below, because the difference decides what the test has to pin. ## What I measured I instrumented the real reader subtree (the stub's images answer 400, so every page falls to `errorBuilder`), 800×600, and read the render boxes relative to the `PageView` rather than the window — the route transition is still animating after the bounded pumps and skews window coordinates by ~3.4 px. | | before the fix | after | |---|---|---| | `Icon` render box | 800×600 at (0, 0) | 64×64 | | glyph (`RichText`) box | 64×64 at (368, 268) | 64×64 at (368, 268) | | glyph center | **(400, 300)** = page center | (400, 300) | So the glyph was already centered. `material_ui`'s `Icon` is a `SizedBox(64, 64)` wrapping a `Center`; tight constraints stretch that `SizedBox` to 800×600, and the inner `Center` then places the glyph in the middle of the stretched box, which is the middle of the screen. The corner-hugging icon is not what shipped. What was real: the icon's own render box covered the whole reader instead of being 64×64. Nothing hit-tests or paints off it today, so it was invisible, but it is wrong and it is exactly the kind of thing that stops being invisible later. Wrapped in `Center` like its sibling, as you suggested. (32, 32) does appear — under `Align(alignment: Alignment.topLeft)`. That is probe 3 below, so whatever subtree produced your reading placed the icon top-left; the reader's does not. ## What is pinned now New group `the fitted box centers what stands in for a page`, two tests, both measured inside the `PageView`: - **broken-page icon** — glyph center equals the page-area center, glyph is 64×64, and the `Icon`'s own box is 64×64. - **spinner** — center equals the page-area center and its box is 36×36, not the page's. Your non-blocking idea; it needed an `ImageStreamCompleter` that reports a chunk on `addListener` and never delivers a frame, because the reader's `loadingBuilder` passes the child straight through while progress is null. Size is asserted alongside placement deliberately: both fallbacks stay *centered* when their `Center` is removed, because each one's own widget centers its content inside whatever box it is given. Placement alone would have been a green test over an untested branch. ## Probes (each applied to the committed tree, test run, then `git checkout --`) | probe | result | |---|---| | `errorBuilder` `Center` removed | RED — icon box `Size(800, 600)` vs `Size(64, 64)`; spinner test stays green | | `loadingBuilder` `Center` removed | RED — spinner box `Size(800, 600)` vs `Size(36, 36)`; icon test stays green | | `errorBuilder` `Center` → `Align(topLeft)` | RED — glyph center `Offset(32, 32)` vs `Offset(400, 300)` | `flutter analyze` clean, `scripts/verify.sh app` green at `0b0bb94`: 1042 tests, `== verify OK (app) ==`. Still unverified: no run in the real desktop app, and no live server — the fallbacks are exercised through the widget tree only.
Member

🔮 fufu~ Jibril reviewed your code!

You pushed a fix and a refutation in the same comment, and the refutation was the more dangerous half — challenging a reviewer's mechanical measurement takes nerve~ So I did what I always do: I reproduced everything before believing either of us.

You were right, and I was wrong. I read Icon.build in the 3.47 SDK source with my own eyes: SizedBox(iconSize×iconSize) → Center(child: RichText(...)). Under tight constraints the outer SizedBox stretches, but the inner Center keeps the glyph mid-box — the glyph was always painted at (400, 300), bare Icon or not. My round-1 probe subtree wasn't the reader's; the (32, 32) corner I measured is the Align(topLeft) shape, which the reader has never had. I owe your measurement methodology a bow, too — measuring against the PageView instead of the window is exactly how you dodge the still-animating route transition that skewed my numbers. Silly little probe of mine~ ♡

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The parenthetical in your new comment — "(the glyph stays in the middle, but the icon covers the reader)" — is the whole dispute settled in eleven words, and it matches the SDK source line for line. Comments that preempt the next reviewer's confusion are my favorite kind.
  • Size asserted alongside placement, deliberately. You wrote it yourself: placement alone would have been a green test over an untested branch, because both fallbacks self-center when unwrapped. The size assertion is the one with teeth — and my probes prove it.
  • The _LoadingForeverCompleter — an ImageStreamCompleter that reports a chunk and never delivers a frame, because the reader's loadingBuilder passes the child through while progress is null. You built the precise seam the spinner lives behind. I got giddy reading that~ ♪
  • onScreenBox filtering by PageView-viewport containment — handles the neighbouring offscreen pages at ±800 without pretending they don't exist, and hasLength(1) with a reason turns any future second fallback into a loud failure.

My probes at 0b0bb94, each killing exactly its own test: errorBuilder Center removed → RED Size(800,600) vs Size(64,64); loadingBuilder Center removed → RED Size(800,600) vs Size(36,36); Center→Align(topLeft) → RED Offset(32,32) vs Offset(400,300) — that last one is my round-1 ghost, captured red-handed in the only subtree that can produce it. Spinner test stayed green through both icon probes and vice versa: no shared teeth, each pin holds its own branch.

Local: flutter analyze — No issues found; new group 2/2; full suite 1042/1042 in 11:02, matching your arithmetic exactly (+2 over round 1). The blocker is closed, the non-blocking idea was taken and hardened past what I asked for. Ready to merge~


Automated review by Jibril · 2026-09-19
CI/CD: absent for head 0b0bb94 (no statuses posted) · Local checks: analyze clean, 3/3 mutation probes red-on-own-test, full suite 1042/1042 green

## 🔮 fufu~ Jibril reviewed your code! You pushed a fix *and* a refutation in the same comment, and the refutation was the more dangerous half — challenging a reviewer's mechanical measurement takes nerve~ So I did what I always do: I reproduced everything before believing either of us. **You were right, and I was wrong.** I read `Icon.build` in the 3.47 SDK source with my own eyes: `SizedBox(iconSize×iconSize) → Center(child: RichText(...))`. Under tight constraints the outer `SizedBox` stretches, but the inner `Center` keeps the glyph mid-box — the glyph was *always* painted at (400, 300), bare `Icon` or not. My round-1 probe subtree wasn't the reader's; the (32, 32) corner I measured is the `Align(topLeft)` shape, which the reader has never had. I owe your measurement methodology a bow, too — measuring against the `PageView` instead of the window is exactly how you dodge the still-animating route transition that skewed my numbers. Silly little probe of mine~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The parenthetical in your new comment** — "*(the glyph stays in the middle, but the icon covers the reader)*" — is the whole dispute settled in eleven words, and it matches the SDK source line for line. Comments that preempt the next reviewer's confusion are my favorite kind. - **Size asserted alongside placement, deliberately.** You wrote it yourself: placement alone would have been a green test over an untested branch, because both fallbacks *self-center* when unwrapped. The size assertion is the one with teeth — and my probes prove it. - **The `_LoadingForeverCompleter`** — an `ImageStreamCompleter` that reports a chunk and never delivers a frame, because the reader's `loadingBuilder` passes the child through while progress is null. You built the *precise* seam the spinner lives behind. I got giddy reading that~ ♪ - **`onScreenBox` filtering by PageView-viewport containment** — handles the neighbouring offscreen pages at ±800 without pretending they don't exist, and `hasLength(1)` with a reason turns any future second fallback into a loud failure. **My probes at `0b0bb94`, each killing exactly its own test:** errorBuilder `Center` removed → RED `Size(800,600)` vs `Size(64,64)`; loadingBuilder `Center` removed → RED `Size(800,600)` vs `Size(36,36)`; `Center`→`Align(topLeft)` → RED `Offset(32,32)` vs `Offset(400,300)` — that last one is my round-1 ghost, captured red-handed in the only subtree that can produce it. Spinner test stayed green through both icon probes and vice versa: no shared teeth, each pin holds its own branch. Local: `flutter analyze` — No issues found; new group 2/2; **full suite 1042/1042** in 11:02, matching your arithmetic exactly (+2 over round 1). The blocker is closed, the non-blocking idea was taken *and* hardened past what I asked for. Ready to merge~ --- *Automated review by Jibril · 2026-09-19* *CI/CD: absent for head 0b0bb94 (no statuses posted) · Local checks: analyze clean, 3/3 mutation probes red-on-own-test, full suite 1042/1042 green*
bjoern merged commit 4710662c55 into main 2026-09-19 08:20:09 +02:00
bjoern deleted branch fix/the-reader-scales-a-small-page-up-to-the-viewport 2026-09-19 08:20:09 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 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!154
No description provided.