feat: create a chapter from the page grid's selection (#115) #116

Merged
bjoern merged 1 commit from feat/115-new-chapter-from-selection into main 2026-09-09 12:52:23 +02:00
Member

Closes #115.

What's in

  • New chapter from N page(s)… as the first entry of the page grid's right-click menu. It prompts for a title and creates a chapter that already holds exactly the selection — previously this was three steps (+ Chapter, re-select the pages, Move to chapter…). The entry is always offered, so it also works on a variant that has no chapters yet.
  • CreateChapterAction gains pageIds (defaulting to empty, so the chapter panel's + Chapter is unchanged). The pages travel on the action rather than in a follow-up dispatch because their move needs the id of a chapter that does not exist yet; _chapterEpics fills the chapter from the create response, then refreshes once.
  • Partial-failure behaviour: if the chapter is created but a page move fails, the epic surfaces the error and still refreshes, so the new (empty) chapter appears in the grid and the user can drag the pages in — rather than a chapter that looks lost. Same philosophy as the folder-upload epic's "keep what already succeeded".
  • The title prompt moves to presentation/pages/editor/chapter_title_dialog.dart, shared by the chapter panel (add/rename) and the new grid entry. It is the same widget as before, so the dialog-owns-its-controller fix from #106 is carried over verbatim rather than re-implemented.
  • Selection order is preserved: the pages are sent in reading order (ReadingOrder-derived display order), not set-iteration order.
  • README's "Variants & Pages tab" feature line mentions the new menu entry.

No backend change — the flow composes the existing POST /api/variants/{id}/chapters and PUT /api/pages/{id}/chapter endpoints, per ADR 0009's "the client maps structure explicitly via the composable endpoints". No ADR is affected.

Verification

scripts/verify.sh green on both layers (app: 836 tests; backend: 584 tests, untouched but run to confirm).

Layers exercised:

  • UI (widget) — page_grid_test.dart: right-click on a two-page shift-selection offers the entry, and Save dispatches CreateChapterAction with the trimmed title, sortOrder appended after the existing chapter, and both page ids in reading order; a second test pins that Cancel dispatches nothing.
  • Redux/epic — editor_epics_test.dart: the epic moves each page to the id returned by createChapter (not one from the action), and the partial-failure case asserts both the error text and that the refresh still runs.
  • Persistence/REST — unchanged and not re-exercised; the repository methods (createChapter, setPageChapter) already have wire coverage.
  • Not verified: no manual run against a live server; the flow is covered by the widget + epic tests only.

Mutation probes run against the new tests (each reverted after):

Probe Result
Epic drops the page-fill loop both new epic tests red
Epic skips the refresh after a failed fill partial-failure test red (Expected: <2>, Actual: <1>)
Grid sends only the clicked page instead of the selection widget test red (['p3','p4'] vs ['p3'])
Dialog's Cancel submits cancel test red

Follow-ups

None — nothing was deferred, so there is no BACKLOG.md entry in this PR.

Closes #115. ## What's in - **`New chapter from N page(s)…`** as the first entry of the page grid's right-click menu. It prompts for a title and creates a chapter that already holds exactly the selection — previously this was three steps (`+ Chapter`, re-select the pages, `Move to chapter…`). The entry is always offered, so it also works on a variant that has no chapters yet. - **`CreateChapterAction` gains `pageIds`** (defaulting to empty, so the chapter panel's `+ Chapter` is unchanged). The pages travel on the action rather than in a follow-up dispatch because their move needs the id of a chapter that does not exist yet; `_chapterEpics` fills the chapter from the create response, then refreshes once. - **Partial-failure behaviour**: if the chapter is created but a page move fails, the epic surfaces the error *and* still refreshes, so the new (empty) chapter appears in the grid and the user can drag the pages in — rather than a chapter that looks lost. Same philosophy as the folder-upload epic's "keep what already succeeded". - **The title prompt moves to `presentation/pages/editor/chapter_title_dialog.dart`**, shared by the chapter panel (add/rename) and the new grid entry. It is the same widget as before, so the dialog-owns-its-controller fix from #106 is carried over verbatim rather than re-implemented. - Selection order is preserved: the pages are sent in reading order (`ReadingOrder`-derived display order), not set-iteration order. - README's "Variants & Pages tab" feature line mentions the new menu entry. No backend change — the flow composes the existing `POST /api/variants/{id}/chapters` and `PUT /api/pages/{id}/chapter` endpoints, per ADR 0009's "the client maps structure explicitly via the composable endpoints". No ADR is affected. ## Verification `scripts/verify.sh` green on both layers (app: 836 tests; backend: 584 tests, untouched but run to confirm). Layers exercised: - **UI (widget)** — `page_grid_test.dart`: right-click on a two-page shift-selection offers the entry, and Save dispatches `CreateChapterAction` with the trimmed title, `sortOrder` appended after the existing chapter, and both page ids in reading order; a second test pins that Cancel dispatches nothing. - **Redux/epic** — `editor_epics_test.dart`: the epic moves each page to the id returned by `createChapter` (not one from the action), and the partial-failure case asserts both the error text and that the refresh still runs. - **Persistence/REST** — unchanged and not re-exercised; the repository methods (`createChapter`, `setPageChapter`) already have wire coverage. - **Not verified**: no manual run against a live server; the flow is covered by the widget + epic tests only. Mutation probes run against the new tests (each reverted after): | Probe | Result | |---|---| | Epic drops the page-fill loop | both new epic tests red | | Epic skips the refresh after a failed fill | partial-failure test red (`Expected: <2>, Actual: <1>`) | | Grid sends only the clicked page instead of the selection | widget test red (`['p3','p4']` vs `['p3']`) | | Dialog's Cancel submits | cancel test red | ## Follow-ups None — nothing was deferred, so there is no BACKLOG.md entry in this PR.
feat: create a chapter from the page grid's selection (#115)
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 4m47s
0ed83ba1c7
Right-clicking a page selection now offers "New chapter from N page(s)…",
which prompts for a title and creates a chapter holding exactly that
selection — previously this took three steps (+ Chapter, re-select, move).

The pages travel on CreateChapterAction rather than in a follow-up dispatch:
their move needs the id of a chapter that does not exist yet, so the epic
fills the chapter from the create response. A fill that fails still refreshes,
so a created-but-empty chapter shows up in the grid instead of looking lost.

The title prompt moves out of chapter_panel.dart into chapter_title_dialog.dart
so both entry points share one implementation, keeping the dialog-owns-its-
controller fix from #106.

Co-Authored-By: Claude Opus 5 (1M context) <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/presentation/state/app_state.dart 80.0% (24 of 30)
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/epics.dart 91.3% (380 of 416)
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 100.0% (8 of 8)
lib/data/models/server_models.g.dart 47.5% (19 of 40)
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 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% (33 of 33)
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 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/server_actions.dart 72.7% (8 of 11)
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% (122 of 122)
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 100.0% (47 of 47)
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 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 29.0% (111 of 383)
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/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 80.1% (173 of 216)
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.4% (156 of 157)
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 100.0% (154 of 154)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 81.7% (8012 of 9812)

<!-- 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/presentation/state/app_state.dart | 80.0% (24 of 30) | | 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/epics.dart | 91.3% (380 of 416) | | 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 | 100.0% (8 of 8) | | lib/data/models/server_models.g.dart | 47.5% (19 of 40) | | 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 | 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% (33 of 33) | | 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 | 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/server_actions.dart | 72.7% (8 of 11) | | 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% (122 of 122) | | 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 | 100.0% (47 of 47) | | 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 | 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 | 29.0% (111 of 383) | | 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/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 | 80.1% (173 of 216) | | 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.4% (156 of 157) | | 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 | 100.0% (154 of 154) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 81.7% (8012 of 9812)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A three-step workflow collapsed into one right-click~ That's the kind of feature crafting I adore — and you even made the failure path honest instead of hiding the corpse. Let me look closer… fufu~, yes. Yes, this is very good.

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The pages-travel-on-the-action design (CreateChapterAction.pageIds + epic fills after create). The move needs an id that doesn't exist yet — putting pageIds on the action and letting _chapterEpics do the move with chapter.id from the create response is exactly right, and avoids a follow-up dispatch that would race. The doc comment on pageIds explains the why beautifully.
  • asyncMap → asyncExpand in _chapterEpics is load-bearing and correctly done: partial failure yields EditorErrorAction and RefreshVariantAction (not either/or). I traced the reducer chain: VariantLoadedAction (editor_reducer.dart:214) doesn't touch editor.error, and the SnackBar fires on the error transition in editor_page.onWillChange — so the error genuinely survives the refresh. No wipe. ♡
  • Sibling discipline is immaculate: sortOrder: variant.chapters.length matches ChapterPanel._addChapter; _displayOrder.where(_selected.contains) matches _dragIdsFor; + Chapter stays untouched via the const [] default. The dialog extraction to chapter_title_dialog.dart is a verbatim move — the #106 controller-dispose fix carried, not re-implemented (chapter_panel_test still green proves it).
  • Tests are directional, not tautological. I ran my own mutation probes in a review clone (each reverted): (1) making the epic move pages into a bogus chapter id → only CreateChapterAction with pages fills the chapter it just created goes RED; (2) skipping the refresh → the partial-failure test goes RED (Expected: <2>, Actual: <1>). The create-response-id claim and the refresh-after-failure claim are both genuinely pinned. Reading-order assertion ['p3','p4'] (not set order) is the cherry~

💡 Little ideas (non-blocking)~

  1. page_grid_test.dart — the PR body highlights that the entry works on a variant with no chapters yet (that's the always-offered part of the design), but the fixture always has c1. A tiny test with a chapter-less variant would pin that claim and the sortOrder: 0 arm. Purely additive — same code path, so I won't chain you to it~ ♡

Local checks (CI bot hasn't posted for 0ed83ba yet): flutter analyze 0 issues; targeted suites editor_epics_test + page_grid_test + chapter_panel_test 25/25 pass. Your verify.sh full-suite claim (836) not independently re-run — analyze + the touched suites + probes were enough for me this time~


Automated review by Jibril · 2026-09-09
CI/CD: absent for head SHA · Local checks: analyze clean, 25/25 targeted tests, 2 mutation probes RED→restored→green

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A three-step workflow collapsed into one right-click~ That's the kind of feature crafting I adore — and you even made the failure path *honest* instead of hiding the corpse. Let me look closer… fufu~, yes. Yes, this is very good. ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The pages-travel-on-the-action design** (`CreateChapterAction.pageIds` + epic fills after create). The move needs an id that doesn't exist yet — putting `pageIds` on the action and letting `_chapterEpics` do the move with `chapter.id` from the create *response* is exactly right, and avoids a follow-up dispatch that would race. The doc comment on `pageIds` explains the why beautifully. - **`asyncMap` → `asyncExpand`** in `_chapterEpics` is load-bearing and correctly done: partial failure yields `EditorErrorAction` **and** `RefreshVariantAction` (not either/or). I traced the reducer chain: `VariantLoadedAction` (editor_reducer.dart:214) doesn't touch `editor.error`, and the SnackBar fires on the error transition in `editor_page.onWillChange` — so the error genuinely survives the refresh. No wipe. ♡ - **Sibling discipline is immaculate**: `sortOrder: variant.chapters.length` matches `ChapterPanel._addChapter`; `_displayOrder.where(_selected.contains)` matches `_dragIdsFor`; `+ Chapter` stays untouched via the `const []` default. The dialog extraction to `chapter_title_dialog.dart` is a verbatim move — the #106 controller-dispose fix carried, not re-implemented (chapter_panel_test still green proves it). - **Tests are directional, not tautological.** I ran my own mutation probes in a review clone (each reverted): (1) making the epic move pages into a bogus chapter id → only `CreateChapterAction with pages fills the chapter it just created` goes RED; (2) skipping the refresh → the partial-failure test goes RED (`Expected: <2>, Actual: <1>`). The create-response-id claim and the refresh-after-failure claim are both genuinely pinned. Reading-order assertion `['p3','p4']` (not set order) is the cherry~ #### 💡 Little ideas (non-blocking)~ 1. **page_grid_test.dart** — the PR body highlights that the entry works on a variant with *no chapters yet* (that's the always-offered part of the design), but the fixture always has `c1`. A tiny test with a chapter-less variant would pin that claim and the `sortOrder: 0` arm. Purely additive — same code path, so I won't chain you to it~ ♡ Local checks (CI bot hasn't posted for `0ed83ba` yet): `flutter analyze` 0 issues; targeted suites `editor_epics_test` + `page_grid_test` + `chapter_panel_test` 25/25 pass. Your verify.sh full-suite claim (836) not independently re-run — analyze + the touched suites + probes were enough for me this time~ --- *Automated review by Jibril · 2026-09-09* *CI/CD: absent for head SHA · Local checks: analyze clean, 25/25 targeted tests, 2 mutation probes RED→restored→green*
bjoern merged commit dfaf89c057 into main 2026-09-09 12:52:23 +02:00
bjoern deleted branch feat/115-new-chapter-from-selection 2026-09-09 12:52:24 +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!116
No description provided.