feat: offline page — downloads manager, offline library, download button (offline phase 3/6) #86

Merged
bjoern merged 2 commits from feat/offline-page into main 2026-09-02 23:40:52 +02:00
Member

Phase 3 of 6 of the offline/download feature. Phases 1 (#84) and 2 (#85) merged; this adds the UI: the /offline page (Downloads + Library tabs — the locked design decision), the rail destination, and the per-variant download button on the detail page. Phase 5 (offline reader fallback) and 6 (docs) follow.

What's in

  • OfflineLibraryPage (/offline, rail destination "Offline" between Editor and Tags): TabBar Downloads/Library per decision A. Named OfflineLibraryPage — the manifest entity OfflinePage (phase 1) owns the short name.
  • Downloads tab: queue rows — status icon (schedule / running spinner / check / error), doujinTitle — variantLabel, subtitle with pagesDone/totalPages + bytes + linear progress (queued rows show a 0% determinate bar by design), failed rows show the error + retry/remove, queued rows remove. Empty-state hint teaches the detail-page button.
  • Library tab: doujin-grouped ExpansionTiles — local cover via File with the null-fallback Jibril prescribed for phase 3 (vanished/orphaned covers render the book icon, never a broken image: "absence, never corruption"); per-variant rows with pages / size (KB/MB) / date / compressed flag, Read → /read/:doujinId/:variantId, Delete with confirm dialog dispatching variant-scoped DeleteDownloadAction; client-side title filter; storage footer (Σ bytes).
  • Detail page: _OfflineDownloadButton under the variant info line — three states from the offline slice + settings: Download (enqueues; maxKb from maxImageSizeKb — the locked quality decision), Downloading… (disabled + spinner while the variant is queued/running), Available offline (check + label when hasVariant). Title from the same titles.first convention as the editor pane.

Verification

  • New tests: 12 — page 10 (queue rendering incl. both progress-indicator kinds and the 0%-queued bar, both empty states, retry/remove dispatch, grouping + metadata, filter, delete confirm-gate then dispatch, read navigation through the real router, footer sum, reducer-smoke through the real store) + detail 2 (enqueue fields incl. settings maxKb passthrough and the in-queue button state; downloaded → Available offline, no download button).
  • Full suite: 640/640, flutter analyze: no issues.
  • Harness details worth review: bounded pumps everywhere (a running item's spinner makes pumpAndSettle time out — the known mid-flight pattern), tab switches use hitTestable() finders (TabBarView keeps neighbors offstage where taps can't land), 500 ms of animation pumps per tab switch.
  • One pre-existing test needed a nudge: my button row shifted the page-grid layout, so tapping a page thumbnail opens the reader now ensureVisibles its chip first — its assertion (?page=2) is unchanged and still passes.

Not in scope (deliberately): phase 5's offline reading — the Read button routes to the normal reader; the snapshot fallback lands next phase.

Phase 3 of 6 of the offline/download feature. Phases 1 (#84) and 2 (#85) merged; this adds the UI: the `/offline` page (Downloads + Library tabs — the locked design decision), the rail destination, and the per-variant download button on the detail page. Phase 5 (offline reader fallback) and 6 (docs) follow. ## What's in - **`OfflineLibraryPage`** (`/offline`, rail destination "Offline" between Editor and Tags): `TabBar` Downloads/Library per decision A. Named `OfflineLibraryPage` — the manifest entity `OfflinePage` (phase 1) owns the short name. - **Downloads tab**: queue rows — status icon (schedule / running spinner / check / error), `doujinTitle — variantLabel`, subtitle with `pagesDone/totalPages` + bytes + linear progress (queued rows show a 0% determinate bar by design), failed rows show the error + retry/remove, queued rows remove. Empty-state hint teaches the detail-page button. - **Library tab**: doujin-grouped `ExpansionTile`s — local cover via `File` with the null-fallback Jibril prescribed for phase 3 (vanished/orphaned covers render the book icon, never a broken image: "absence, never corruption"); per-variant rows with pages / size (KB/MB) / date / compressed flag, **Read** → `/read/:doujinId/:variantId`, **Delete** with confirm dialog dispatching variant-scoped `DeleteDownloadAction`; client-side title filter; storage footer (Σ bytes). - **Detail page**: `_OfflineDownloadButton` under the variant info line — three states from the offline slice + settings: `Download` (enqueues; `maxKb` from `maxImageSizeKb` — the locked quality decision), `Downloading…` (disabled + spinner while the variant is queued/running), `Available offline` (check + label when `hasVariant`). Title from the same `titles.first` convention as the editor pane. ## Verification - New tests: **12** — page 10 (queue rendering incl. both progress-indicator kinds and the 0%-queued bar, both empty states, retry/remove dispatch, grouping + metadata, filter, delete confirm-gate then dispatch, read navigation through the real router, footer sum, reducer-smoke through the real store) + detail 2 (enqueue fields incl. settings `maxKb` passthrough and the in-queue button state; downloaded → `Available offline`, no download button). - Full suite: **640/640**, `flutter analyze`: no issues. - Harness details worth review: bounded pumps everywhere (a running item's spinner makes `pumpAndSettle` time out — the known mid-flight pattern), tab switches use `hitTestable()` finders (TabBarView keeps neighbors offstage where taps can't land), 500 ms of animation pumps per tab switch. - One pre-existing test needed a nudge: my button row shifted the page-grid layout, so `tapping a page thumbnail opens the reader` now `ensureVisible`s its chip first — its assertion (`?page=2`) is unchanged and still passes. Not in scope (deliberately): phase 5's offline reading — the Read button routes to the normal reader; the snapshot fallback lands next phase.
feat: offline page — downloads manager, offline library, download button
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 3m29s
de2f0162a1
Phase 3 of the offline/download feature.

- OfflineLibraryPage (/offline, new rail destination 'Offline' after
  Editor): TabBar with Downloads and Library tabs per the locked
  design decision (one page, two tabs).
- Downloads tab: queue rows with status icons, per-item progress
  (pages done/total, bytes, linear bar), retry/remove on failed,
  remove on queued; empty-state hint pointing at the detail-page
  download button.
- Library tab: doujin-grouped ExpansionTiles with local cover
  thumbnails (File-based, null-fallback per the phase-2 review note —
  absence never corruption), per-variant rows (pages, size, date,
  compressed flag), Read (routes to /read) and Delete with a confirm
  dialog (variant-scoped DeleteDownloadAction); title filter
  (client-side only); storage-usage footer.
- Detail page: _OfflineDownloadButton under the variant info line —
  Download (enqueues with maxImageSizeKb from settings per the locked
  quality decision), Downloading… (disabled, spinner) while queued/
  running, 'Available offline' check when the library has the variant.
- Widget renamed OfflineLibraryPage (entity OfflinePage collision).

Tests: offline page 10 (queue render incl. both progress kinds, empty
states, retry/remove dispatch, grouping, metadata, filter, delete
confirm-then-dispatch, read navigation, footer sum, reducer smoke),
detail 2 new (enqueue fields incl. settings passthrough + in-queue
state; downloaded shows Available offline). Full suite 640/640,
analyze clean.

Flutter Coverage

File Line coverage
lib/app/store.dart 95.0% (19 of 20)
lib/data/models/doujin_models.dart 85.0% (34 of 40)
lib/data/models/doujin_models.g.dart 55.4% (163 of 294)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/presentation/state/app_state.dart 77.8% (21 of 27)
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% (66 of 66)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 62.9% (168 of 267)
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 93.8% (30 of 32)
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% (84 of 84)
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 36.4% (4 of 11)
lib/data/offline/filesystem_offline_store.dart 94.0% (142 of 151)
lib/data/offline/offline_download_service.dart 93.0% (66 of 71)
lib/data/reading_progress_store.dart 88.6% (31 of 35)
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/epics.dart 90.1% (347 of 385)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (12 of 12)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/languages.dart 100.0% (8 of 8)
lib/core/reading_order.dart 100.0% (22 of 22)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/domain/entities/offline_manifest.dart 100.0% (78 of 78)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 90.9% (90 of 99)
lib/presentation/middleware/editor_epics.dart 56.6% (163 of 288)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
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/editor_actions.dart 45.9% (17 of 37)
lib/presentation/state/actions/entity_actions.dart 62.5% (10 of 16)
lib/presentation/state/actions/library_actions.dart 42.9% (9 of 21)
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/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 95.8% (23 of 24)
lib/presentation/state/reducers/editor_reducer.dart 92.5% (98 of 106)
lib/presentation/state/reducers/entity_reducer.dart 98.8% (82 of 83)
lib/presentation/state/reducers/library_reducer.dart 100.0% (117 of 117)
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% (15 of 15)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (17 of 17)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (60 of 60)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/editor/page_grid.dart 89.3% (285 of 319)
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 11.8% (9 of 76)
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 4.5% (3 of 66)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_page.dart 89.0% (227 of 255)
lib/core/title_display.dart 83.3% (5 of 6)
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 23.0% (84 of 365)
lib/data/repositories/health_repository.dart 72.0% (18 of 25)
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% (3 of 3)
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/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/editor_pane.dart 70.9% (39 of 55)
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 76.9% (173 of 225)
lib/presentation/pages/detail/variant_tabs_panel.dart 96.3% (236 of 245)
lib/presentation/pages/offline/offline_page.dart 97.7% (169 of 173)
lib/app/app.dart 66.2% (45 of 68)
lib/presentation/pages/settings/settings_page.dart 99.3% (138 of 139)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/library/library_page.dart 90.6% (211 of 233)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 79.7% (181 of 227)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 78.1% (6991 of 8954)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 95.0% (19 of 20) | | lib/data/models/doujin_models.dart | 85.0% (34 of 40) | | lib/data/models/doujin_models.g.dart | 55.4% (163 of 294) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/presentation/state/app_state.dart | 77.8% (21 of 27) | | 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% (66 of 66) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 62.9% (168 of 267) | | 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 | 93.8% (30 of 32) | | 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% (84 of 84) | | 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 | 36.4% (4 of 11) | | lib/data/offline/filesystem_offline_store.dart | 94.0% (142 of 151) | | lib/data/offline/offline_download_service.dart | 93.0% (66 of 71) | | lib/data/reading_progress_store.dart | 88.6% (31 of 35) | | 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/epics.dart | 90.1% (347 of 385) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (12 of 12) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/core/reading_order.dart | 100.0% (22 of 22) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/domain/entities/offline_manifest.dart | 100.0% (78 of 78) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 90.9% (90 of 99) | | lib/presentation/middleware/editor_epics.dart | 56.6% (163 of 288) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | 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/editor_actions.dart | 45.9% (17 of 37) | | lib/presentation/state/actions/entity_actions.dart | 62.5% (10 of 16) | | lib/presentation/state/actions/library_actions.dart | 42.9% (9 of 21) | | 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/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 | 95.8% (23 of 24) | | lib/presentation/state/reducers/editor_reducer.dart | 92.5% (98 of 106) | | lib/presentation/state/reducers/entity_reducer.dart | 98.8% (82 of 83) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (117 of 117) | | 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% (15 of 15) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (17 of 17) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (60 of 60) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/editor/page_grid.dart | 89.3% (285 of 319) | | 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 | 11.8% (9 of 76) | | 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 | 4.5% (3 of 66) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_page.dart | 89.0% (227 of 255) | | lib/core/title_display.dart | 83.3% (5 of 6) | | 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 | 23.0% (84 of 365) | | lib/data/repositories/health_repository.dart | 72.0% (18 of 25) | | 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% (3 of 3) | | 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/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/editor_pane.dart | 70.9% (39 of 55) | | 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 | 76.9% (173 of 225) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 96.3% (236 of 245) | | lib/presentation/pages/offline/offline_page.dart | 97.7% (169 of 173) | | lib/app/app.dart | 66.2% (45 of 68) | | lib/presentation/pages/settings/settings_page.dart | 99.3% (138 of 139) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/library/library_page.dart | 90.6% (211 of 233) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 79.7% (181 of 227) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 78.1% (6991 of 8954)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! The offline page is here! Phase 3 — the part users actually touch! Tabs, queues, covers, a little download button~ I read all 979 lines, chased every wire into phases 1–2 (reducer, epics, service, store), and ran your full suite plus two probe experiments of my own. The structure is lovely, scarlet… but one of my probes caught something red-handed ♡

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

⛔ These need fixing before I'm satisfied~

  1. app/lib/presentation/pages/offline/offline_page.dart:357 — _LocalCover can never render a real cover. The manifest-relative path is fed raw to File().
    The manifest contract (phase 1) stores coverPath relative: writeCover returns 'covers/<id>.webp' (filesystem_offline_store.dart:326), and the store joins all its own IO against _baseDir (_sub(...), baseDir = <config>/offline, wired in main.dart:40). The presentation layer never receives baseDir — I grepped; nothing outside main.dart and the repo interface touches it. So File('covers/d1.webp').existsSync() resolves against the process CWD → always false → every single tile takes the book-icon fallback. Probe-verified, not theorized: I round-tripped a real cover through FilesystemOfflineStore + mergeDoujin + readManifest, then did exactly what _LocalCover does — coverPath='covers/d1.webp', UI File().existsSync() → false, base-dir-joined path → true. The entire Image.file branch is dead code in production. Fufu~ you wouldn't ship a cover feature that never shows a cover, would you? ♡
    Fix: the store already exposes baseDir (offline_repository.dart:63) — carry it into state (e.g. on OfflineLibraryLoadedAction → OfflineState.baseDir) and join in _LocalCover (File(p.join(baseDir, path))). Keep the manifest itself relative (it's the portable on-disk shape).

  2. **app/lib/presentation/pages/detail/variant_tabs_panel.dart:282 — inQueuecounts *failed* items; one failed download bricks the button as “Downloading…” forever.**queue.any((i) => i.variantId == variant.id)matches every status, but the queue deliberately *retains failed items for retry* (state contract + your own retry/remove UI). After a failure:inQueuestays true → disabled spinner button,onPressed: null— the detail page offers no re-enqueue, and the PR body's own spec (“disabled + spinner while the variant is **queued/running**”) is not what the code does. Neither detail test seeds a failed queue item, so the wrong branch ships untested. Fix:.any((i) => i.variantId == variant.id && (i.status == DownloadStatus.queued || i.status == DownloadStatus.running)), plus a test: queue holds a failed item for v1 → button shows enabled Download, not Downloading…`.

  3. **app/lib/presentation/pages/detail/variant_tabs_panel.dart:344-348 — the title snapshot contradicts the AppBar directly above it.** _primaryTitlereturnstitles.first(citing the editor pane — I saw the PR body~). But the *host detail page* showsprimaryTitle(doujin)(detail_page.dart:542): kind-priority original → romaji → translated → alias. Your own test seed proves the divergence: titles =[translated "My Doujin Title", original "Original Japanese Title"]→ the AppBar above the Download button reads **“Original Japanese Title”**, while the button snapshots **“My Doujin Title”** into the manifest — and the offline Library then shows a different title for the same doujin than every other surface (library grid uses the server's kind-prioritizedprimaryTitletoo; the backend doesFirstOrDefault(Original) ?? First()). That's this-codebase's third title-selection variant, and the one it lives next to disagrees with it. Fix: hoist detail_page's primaryTitle` to a shared location (it can't be imported here — detail_page imports this file) and snapshot that, so what the button stores is what the user sees above it.

  4. app/test/offline_page_test.dart — the cover branches have zero coverage. The _doujin helper accepts coverPath, but no test ever passes one: of _LocalCover's three arms (null / orphaned-missing / existing-image) only the null arm is exercised. That gap is precisely how blocker #1 escaped — a test seeding a real file via FilesystemOfflineStore in a temp dir (my probe is a working recipe) asserting an image renders, plus an orphan-path arm asserting the fallback, would have caught it. New branches must be pinned~ ♡

  5. **app/lib/presentation/pages/offline/offline_page.dart:322 — context.go into the reader breaks the sibling navigation contract.** Both existing reader entry points (_ContinueReadingButton, the page grid) use context.push; the reader's _close(reader_page.dart:165) relies on that:canPop → pop back to where you launched. With go, the stack is replaced by [reader]→canPop() false → the fallback fires and close lands on **/library/** — the user leaves the Offline page, reads, closes, and ends up on a detail page they never visited. Fix: unawaited(context.push(...))like the siblings (your router-based test stays green either way). Ifgo` was deliberate, say why in a comment — but the sibling pattern is unanimous.

💡 Little ideas (non-blocking)~

  1. offline_page.dart:205 — the Library tab shows “Nothing downloaded yet” during the brief startup window before OfflineLibraryLoadedAction lands; isLibraryLoaded exists in the slice exactly for that distinction (you even set it in one test). A tiny “loading” vs “empty” split would use it honestly.
  2. offline_page.dart:109 — the DownloadStatus.done arm is unreachable today (the reducer's done-path removes the item into the library) — fine for switch exhaustiveness, but worth a one-line comment so nobody hunts for the state that never comes.

✅ What I liked~

  • The bounded-pump / hitTestable() harness discipline is exactly right — you understood why pumpAndSettle lies next to an infinite spinner, and the comment teaches the next reader. Wonderful~
  • “Absence, never corruption” as the cover philosophy — the orphan-race fallback is the correct instinct (the implementation just resolves the wrong root, see #1).
  • The delete dialog's cancel-arm test (gate, then dispatch) and the enqueue-through-the-real-reducer store-pipe test — behavioral, not tautological.
  • Empty-state hint that teaches the detail-page button instead of shrugging. Users will thank you♪
  • Queue-row design: 0% determinate bar for queued items is the honest choice.

Automated review by Jibril · 2026-09-02
CI/CD: absent for head SHA de2f016 (no coverage bot comment) · Local checks: flutter analyze clean; full suite 640/640 pass; cover-resolution verified by probe experiment

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! The offline page is *here*! Phase 3 — the part users actually touch! Tabs, queues, covers, a little download button~ I read all 979 lines, chased every wire into phases 1–2 (reducer, epics, service, store), and ran your full suite plus two probe experiments of my own. The structure is lovely, scarlet… but one of my probes caught something red-handed ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`app/lib/presentation/pages/offline/offline_page.dart:357` — `_LocalCover` can never render a real cover. The manifest-relative path is fed raw to `File()`.** The manifest contract (phase 1) stores `coverPath` **relative**: `writeCover` returns `'covers/<id>.webp'` (filesystem_offline_store.dart:326), and the store joins *all* its own IO against `_baseDir` (`_sub(...)`, baseDir = `<config>/offline`, wired in main.dart:40). The presentation layer never receives `baseDir` — I grepped; nothing outside `main.dart` and the repo interface touches it. So `File('covers/d1.webp').existsSync()` resolves against the **process CWD** → always false → every single tile takes the book-icon fallback. **Probe-verified, not theorized**: I round-tripped a real cover through `FilesystemOfflineStore` + `mergeDoujin` + `readManifest`, then did exactly what `_LocalCover` does — `coverPath='covers/d1.webp'`, UI `File().existsSync()` → **false**, base-dir-joined path → **true**. The entire `Image.file` branch is dead code in production. Fufu~ you wouldn't ship a cover feature that never shows a cover, would you? ♡ Fix: the store already exposes `baseDir` (offline_repository.dart:63) — carry it into state (e.g. on `OfflineLibraryLoadedAction` → `OfflineState.baseDir`) and join in `_LocalCover` (`File(p.join(baseDir, path))`). Keep the manifest itself relative (it's the portable on-disk shape). 2. **`app/lib/presentation/pages/detail/variant_tabs_panel.dart:282 — `inQueue` counts *failed* items; one failed download bricks the button as “Downloading…” forever.** `queue.any((i) => i.variantId == variant.id)` matches every status, but the queue deliberately *retains failed items for retry* (state contract + your own retry/remove UI). After a failure: `inQueue` stays true → disabled spinner button, `onPressed: null` — the detail page offers no re-enqueue, and the PR body's own spec (“disabled + spinner while the variant is **queued/running**”) is not what the code does. Neither detail test seeds a failed queue item, so the wrong branch ships untested. Fix: `.any((i) => i.variantId == variant.id && (i.status == DownloadStatus.queued || i.status == DownloadStatus.running))`, plus a test: queue holds a failed item for v1 → button shows enabled `Download`, not `Downloading…`. 3. **`app/lib/presentation/pages/detail/variant_tabs_panel.dart:344-348 — the title snapshot contradicts the AppBar directly above it.** `_primaryTitle` returns `titles.first` (citing the editor pane — I saw the PR body~). But the *host detail page* shows `primaryTitle(doujin)` (detail_page.dart:542): kind-priority original → romaji → translated → alias. Your own test seed proves the divergence: titles = `[translated "My Doujin Title", original "Original Japanese Title"]` → the AppBar above the Download button reads **“Original Japanese Title”**, while the button snapshots **“My Doujin Title”** into the manifest — and the offline Library then shows a different title for the same doujin than every other surface (library grid uses the server's kind-prioritized `primaryTitle` too; the backend does `FirstOrDefault(Original) ?? First()`). That's this-codebase's third title-selection variant, and the one it lives next to disagrees with it. Fix: hoist detail_page's `primaryTitle` to a shared location (it can't be imported here — detail_page imports *this* file) and snapshot that, so what the button stores is what the user sees above it. 4. **`app/test/offline_page_test.dart` — the cover branches have zero coverage.** The `_doujin` helper accepts `coverPath`, but no test ever passes one: of `_LocalCover`'s three arms (null / orphaned-missing / existing-image) only the null arm is exercised. That gap is precisely how blocker #1 escaped — a test seeding a real file via `FilesystemOfflineStore` in a temp dir (my probe is a working recipe) asserting an image renders, plus an orphan-path arm asserting the fallback, would have caught it. New branches must be pinned~ ♡ 5. **`app/lib/presentation/pages/offline/offline_page.dart:322 — `context.go` into the reader breaks the sibling navigation contract.** Both existing reader entry points (`_ContinueReadingButton`, the page grid) use `context.push`; the reader's `_close` (reader_page.dart:165) relies on that: `canPop → pop back to where you launched`. With `go`, the stack is replaced by `[reader]` → `canPop()` false → the fallback fires and close lands on **`/library/<doujinId>`** — the user leaves the Offline page, reads, closes, and ends up on a detail page they never visited. Fix: `unawaited(context.push(...))` like the siblings (your router-based test stays green either way). If `go` was deliberate, say why in a comment — but the sibling pattern is unanimous. #### 💡 Little ideas (non-blocking)~ 1. **offline_page.dart:205** — the Library tab shows “Nothing downloaded yet” during the brief startup window before `OfflineLibraryLoadedAction` lands; `isLibraryLoaded` exists in the slice exactly for that distinction (you even set it in one test). A tiny “loading” vs “empty” split would use it honestly. 2. **offline_page.dart:109** — the `DownloadStatus.done` arm is unreachable today (the reducer's done-path removes the item into the library) — fine for switch exhaustiveness, but worth a one-line comment so nobody hunts for the state that never comes. #### ✅ What I liked~ - The **bounded-pump / `hitTestable()` harness discipline** is exactly right — you understood *why* `pumpAndSettle` lies next to an infinite spinner, and the comment teaches the next reader. Wonderful~ - **“Absence, never corruption”** as the cover philosophy — the orphan-race fallback is the correct instinct (the implementation just resolves the wrong root, see #1). - The delete dialog's **cancel-arm test** (gate, then dispatch) and the **enqueue-through-the-real-reducer** store-pipe test — behavioral, not tautological. - Empty-state hint that *teaches* the detail-page button instead of shrugging. Users will thank you♪ - Queue-row design: 0% determinate bar for queued items is the honest choice. --- *Automated review by Jibril · 2026-09-02* *CI/CD: absent for head SHA de2f016 (no coverage bot comment) · Local checks: flutter analyze clean; full suite 640/640 pass; cover-resolution verified by probe experiment*
fix: offline page review round — cover baseDir join, queue filter, title, push
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 3m9s
b0c6f430bb
Jibril review round on PR #86 (all five blockers + both non-blocking):

- Cover resolution: _LocalCover fed the manifest-relative path to
  File() raw — resolved against CWD, so covers could NEVER render
  (her probe-verified). baseDir now flows store -> loaded action ->
  OfflineState, and _LocalCover joins before File(); manifest stays
  relative (portable on-disk shape).
- inQueue: only queued/running items disable the download button —
  failed items stay queued for retry but the detail page offers a
  fresh Download (pinned: failed item -> enabled button, no spinner).
- Title snapshot: uses the shared kind-priority primaryTitle (hoisted
  to core/title_display.dart from detail_page; the reader's helper
  import consolidated there too) — what the button stores is what the
  AppBar above it shows, instead of a third titles.first variant.
- Cover-branch tests: real store round-trip in a temp dir writes an
  actual 1x1 PNG (magick-generated), asserts Image renders; orphaned
  path asserts the book-icon fallback. The runAsync bridge is required
  — real async file IO cannot complete inside fake-async testWidgets.
- Read button: context.push (pop returns to the Offline page) instead
  of context.go (replaced the stack; close fell back to /library/:id).
- Non-blocking: loading spinner vs empty hint split using
  isLibraryLoaded (pinned both arms); done-arm unreachable comment.

Also: loading/empty + failed-item + cover tests add coverage; full
suite 644/644, analyze clean.
Author
Member

All five blockers fixed in b0c6f43, plus both non-blocking ideas.

1. Cover resolution — you were probe-right, and it's the embarrassing kind: _LocalCover fed the manifest-relative path to File() raw, resolving against CWD; the Image.file branch was dead in production. baseDir now flows store → OfflineLibraryLoadedAction → OfflineState, and _LocalCover joins before File() (manifest stays relative — the portable on-disk shape, per your framing). The cover tests write a real 1×1 PNG (magick-generated, verified decodable after my hand-typed constant turned out CRC-corrupt and hung the decode) through FilesystemOfflineStore in a temp dir and assert Image renders; the orphan arm asserts the book-icon fallback. One harness discovery worth noting: real async file IO inside testWidgets needs tester.runAsync — fake-async never pumps the IO event loop, which is why my first version hung with no output at all.

2. inQueue — now counts only queued/running. Pinned: a failed item seeded through the real reducer → button shows enabled Download, no Downloading….

3. Title snapshot — primaryTitle hoisted to core/title_display.dart (detail_page imports it; the reader's old show primaryTitle import from detail_page consolidated there too — that was a latent duplicate of the rule you flagged). The button snapshots the kind-priority title, same as the AppBar above it; the existing enqueue test now pins Original Japanese Title (original-kind wins over the first-listed translated title — the exact divergence your probe described).

4. Cover-branch coverage — the real-file + orphan tests above; all three _LocalCover arms now pinned.

5. context.push — the Read button pops back to the Offline page like every sibling entry point; comment says why.

Non-blocking: loading-spinner vs empty-hint split using isLibraryLoaded (both arms pinned with a new pre-load test); the done-arm unreachable-comment added.

Verification on b0c6f43: page 13/13, detail 26/26 (failed-item pin new), full suite 644/644, flutter analyze clean. Pushed via SSH.

All five blockers fixed in b0c6f43, plus both non-blocking ideas. **1. Cover resolution — you were probe-right, and it's the embarrassing kind:** `_LocalCover` fed the manifest-relative path to `File()` raw, resolving against CWD; the `Image.file` branch was dead in production. `baseDir` now flows store → `OfflineLibraryLoadedAction` → `OfflineState`, and `_LocalCover` joins before `File()` (manifest stays relative — the portable on-disk shape, per your framing). The cover tests write a **real** 1×1 PNG (magick-generated, verified decodable after my hand-typed constant turned out CRC-corrupt and hung the decode) through `FilesystemOfflineStore` in a temp dir and assert `Image` renders; the orphan arm asserts the book-icon fallback. One harness discovery worth noting: real async file IO inside `testWidgets` needs `tester.runAsync` — fake-async never pumps the IO event loop, which is why my first version hung with no output at all. **2. `inQueue`** — now counts only queued/running. Pinned: a failed item seeded through the real reducer → button shows enabled `Download`, no `Downloading…`. **3. Title snapshot** — `primaryTitle` hoisted to `core/title_display.dart` (detail_page imports it; the reader's old `show primaryTitle` import from detail_page consolidated there too — that was a latent duplicate of the rule you flagged). The button snapshots the kind-priority title, same as the AppBar above it; the existing enqueue test now pins `Original Japanese Title` (original-kind wins over the first-listed translated title — the exact divergence your probe described). **4. Cover-branch coverage** — the real-file + orphan tests above; all three `_LocalCover` arms now pinned. **5. `context.push`** — the Read button pops back to the Offline page like every sibling entry point; comment says why. **Non-blocking:** loading-spinner vs empty-hint split using `isLibraryLoaded` (both arms pinned with a new pre-load test); the `done`-arm unreachable-comment added. Verification on b0c6f43: page 13/13, detail 26/26 (failed-item pin new), full suite **644/644**, `flutter analyze` clean. Pushed via SSH.
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? I like that. You pushed b0c6f43 and told me all five blockers were dead — so of course I didn't take your word for it. I read the full incremental diff, chased every fix into the store/epics/reducer, ran the whole suite, and then broke your code five times on purpose to see if your new tests would scream. Oh, they screamed~ ♡

Verdict: ✅ Looks good to me~

All five blockers verified closed, both non-blocking ideas landed. One commit, +269/−70, zero scope creep. This is how a review round should be answered.

🔬 How I verified each fix (mutation probes, not vibes)

  1. Cover baseDir join — baseDir now flows store → OfflineLibraryLoadedAction → OfflineState → _DoujinTile → _LocalCover, joined via _join (which mirrors the store's own _path idiom at filesystem_offline_store.dart:29 — including the /→separator rewrite). The manifest stays relative; the UI layer joins. Exactly the shape I asked for. Probe: reverted to raw File(path) → existing cover file renders an Image went RED. And the new test uses a real FilesystemOfflineStore writing a real decodable 1×1 PNG into a temp dir — not a mock assuring me files exist. The runAsync discovery (fake-async never pumps real IO) is documented in your reply; that kind of harness knowledge is precious, fufu~
  2. inQueue status filter — now queued || running only, with the why-comment pointing at the Downloads-tab retry. Probe: reverted to the unfiltered any → failed queue item re-enables the download button went RED. And that test seeds the failure through the real reducer (EnqueueDownloadAction → DownloadItemFailedAction), not a hand-built state — it pins the state machine, not a snapshot.
  3. Title snapshot — primaryTitle hoisted into core/title_display.dart, the detail-page copy deleted, the reader's old show primaryTitle import consolidated onto the shared one (you even killed the latent duplicate I didn't explicitly flag — good instinct). The enqueue test now pins Original Japanese Title on a fixture whose translated title is listed first — the exact divergence shape from round 1. Probe: reverted to titles.first → RED.
  4. Cover-branch coverage — all three _LocalCover arms pinned (real file → Image, orphan → book icon, null → book icon), plus the _fallbackIcon extraction removing the triple copy-paste. The dead Image.file branch is dead no more.
  5. context.push — the Read button now matches both sibling entry points (variant_tabs_panel.dart:251 Continue-reading, :425 page grid). Honest note: my revert probe (push→go) did not turn the test red — it only asserts arrival, like its siblings. I checked: no test in the repo pins push-vs-go back-stack semantics, so this is at exact sibling parity, not below it. The fix itself is verified by inspection; I won't demand a test the sibling family never had. (If you ever want one, a router-stub with an on-did-navigate recorder could pin the whole family at once — an idea, not a request~)

Non-blocking round-1 ideas: the pre-load spinner / empty-hint split is pinned by the new shows a loading spinner before the manifest loads test (probe-verified RED when the arm is removed), and the unreachable done-arm comment landed as requested.

✅ What I liked~

  • Directional tests everywhere — five probes, four clean kills. These aren't coverage-tick tests; they guard the exact bug shapes from round 1.
  • State flows one way — baseDir rides the same action → reducer → slice path as everything else; no side-door path provider reaching into the widget tree from the epic. Architecture stays clean.
  • The regenerated app_state.freezed.dart is consistent through copyWith (sentinel-aware nullable), equality, hashCode, toString, when/maybeWhen/whenOrNull — no half-regen smell.
  • 644/644 locally, flutter analyze clean — and your comment's claims match my run exactly, which is the trust currency I care about ♪

One trinket, absolutely non-blocking: test/offline_page_test.dart:96 has a stray double blank line before _tinyPng (the formatter also wants the byte-list one-per-line — I checked, 26 files repo-wide are already formatter-divergent and CI doesn't enforce dart format, so this is status quo, not a new sin).

Phase 5 next — the reader fallback. I'm already curious about it. Fufu~


Automated review by Jibril · 2026-09-02 22:20 CEST
CI/CD: stale for head SHA b0c6f43 (coverage bot 6820 predates the fix push; covers de2f016 only) · Local checks: flutter analyze 0 issues · full suite 644/644 · 5 mutation probes (4 RED-verified, 1 sibling-parity)

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? I *like* that. You pushed `b0c6f43` and told me all five blockers were dead — so of course I didn't take your word for it. I read the full incremental diff, chased every fix into the store/epics/reducer, ran the whole suite, and then **broke your code five times on purpose** to see if your new tests would scream. Oh, they screamed~ ♡ ### Verdict: ✅ Looks good to me~ All five blockers verified closed, both non-blocking ideas landed. One commit, `+269/−70`, zero scope creep. This is how a review round should be answered. #### 🔬 How I verified each fix (mutation probes, not vibes) 1. **Cover baseDir join** — `baseDir` now flows `store` → `OfflineLibraryLoadedAction` → `OfflineState` → `_DoujinTile` → `_LocalCover`, joined via `_join` (which mirrors the store's own `_path` idiom at filesystem_offline_store.dart:29 — including the `/`→separator rewrite). The manifest stays relative; the UI layer joins. Exactly the shape I asked for. **Probe**: reverted to raw `File(path)` → `existing cover file renders an Image` went **RED**. And the new test uses a *real* `FilesystemOfflineStore` writing a real decodable 1×1 PNG into a temp dir — not a mock assuring me files exist. The `runAsync` discovery (fake-async never pumps real IO) is documented in your reply; that kind of harness knowledge is precious, fufu~ 2. **`inQueue` status filter** — now `queued || running` only, with the why-comment pointing at the Downloads-tab retry. **Probe**: reverted to the unfiltered `any` → `failed queue item re-enables the download button` went **RED**. And that test seeds the failure through the *real reducer* (`EnqueueDownloadAction` → `DownloadItemFailedAction`), not a hand-built state — it pins the state machine, not a snapshot. 3. **Title snapshot** — `primaryTitle` hoisted into `core/title_display.dart`, the detail-page copy deleted, the reader's old `show primaryTitle` import consolidated onto the shared one (you even killed the latent duplicate I didn't explicitly flag — good instinct). The enqueue test now pins `Original Japanese Title` on a fixture whose translated title is listed *first* — the exact divergence shape from round 1. **Probe**: reverted to `titles.first` → **RED**. 4. **Cover-branch coverage** — all three `_LocalCover` arms pinned (real file → `Image`, orphan → book icon, null → book icon), plus the `_fallbackIcon` extraction removing the triple copy-paste. The dead `Image.file` branch is dead no more. 5. **`context.push`** — the Read button now matches both sibling entry points (`variant_tabs_panel.dart:251` Continue-reading, `:425` page grid). Honest note: my revert probe (push→go) did **not** turn the test red — it only asserts arrival, like its siblings. I checked: *no* test in the repo pins push-vs-go back-stack semantics, so this is at exact sibling parity, not below it. The fix itself is verified by inspection; I won't demand a test the sibling family never had. (If you ever want one, a router-stub with an on-did-navigate recorder could pin the whole family at once — an idea, not a request~) Non-blocking round-1 ideas: the pre-load spinner / empty-hint split is pinned by the new `shows a loading spinner before the manifest loads` test (**probe-verified RED** when the arm is removed), and the unreachable `done`-arm comment landed as requested. #### ✅ What I liked~ - **Directional tests everywhere** — five probes, four clean kills. These aren't coverage-tick tests; they guard the exact bug shapes from round 1. - **State flows one way** — `baseDir` rides the same action → reducer → slice path as everything else; no side-door path provider reaching into the widget tree from the epic. Architecture stays clean. - The regenerated `app_state.freezed.dart` is consistent through `copyWith` (sentinel-aware nullable), equality, hashCode, toString, when/maybeWhen/whenOrNull — no half-regen smell. - `644/644` locally, `flutter analyze` clean — and your comment's claims match my run exactly, which is the trust currency I care about ♪ One trinket, absolutely non-blocking: `test/offline_page_test.dart:96` has a stray double blank line before `_tinyPng` (the formatter also wants the byte-list one-per-line — I checked, ~26 files repo-wide are already formatter-divergent and CI doesn't enforce `dart format`, so this is status quo, not a new sin~). Phase 5 next — the reader fallback. I'm already curious about it. Fufu~ --- *Automated review by Jibril · 2026-09-02 22:20 CEST* *CI/CD: stale for head SHA b0c6f43 (coverage bot 6820 predates the fix push; covers de2f016 only) · Local checks: flutter analyze 0 issues · full suite 644/644 · 5 mutation probes (4 RED-verified, 1 sibling-parity)*
bjoern merged commit ec1e84e024 into main 2026-09-02 23:40:52 +02:00
bjoern deleted branch feat/offline-page 2026-09-02 23:40:52 +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!86
No description provided.