fix: the chapter title dialog owns its controller (#98) #106

Merged
bjoern merged 1 commit from fix/chapter-dialog-controller-lifetime into main 2026-09-07 19:30:24 +02:00
Member

Fixes #98 — creating or renaming a chapter crashed the app.

What was wrong

_showChapterTitleDialog in chapter_panel.dart disposed its TextEditingController on the showDialog future:

).then((_) => controller.dispose());

That future completes when the pop starts, not when the route has finished reversing. So the controller died while the text field was still mounted, and the next frame rebuilt the outgoing transition — which re-subscribes its listeners — against a disposed controller. That is exactly the reported stack: _AnimatedState.didUpdateWidget → _MergingListenable.addListener → the disposed assertion, then the _dependents.isEmpty cascade and the lost connection.

Submitting made it certain rather than merely likely: onSubmit dispatches, the store updates, and the editor subtree rebuilds inside that animation window. That is why Cancel usually looked fine and Create did not.

The fix

The dialog is now a StatefulWidget owning the controller and disposing it in State.dispose() — the shape the two sibling dialogs in the same folder already use (edit_title_dialog.dart, variant_dialog.dart). This was the only place in the app creating a bare controller outside a State; every other one is a state field.

Verification

chapter_panel.dart had no tests at all, which is why this shipped. It now has seven: create, rename (including that the field starts from the current title), keyboard submit, empty title rejected without closing, cancel, delete with its confirmation both cancelled and confirmed, and the chip's page count.

The submit tests rebuild the panel from the dispatched action inside the exit-animation window and assert no exception, which is what makes them directional rather than decorative:

  • restore the old .then((_) => controller.dispose()) shape → four tests red, each with A TextEditingController was used after being disposed;
  • fix restored → 7 of 7 green.

scripts/verify.sh app green, 827 tests, analyze clean.

I also reproduced the crash standalone before touching the file, to be sure of the mechanism rather than reasoning from the stack.

Not verified

The desktop app was not driven by hand; the reproduction and the pin are both at widget-test level, on the same code path your log came from.

Follow-up

Added to docs/BACKLOG.md: nothing prevents this coming back. A shared title-prompt dialog, or a lint against disposing a controller on a dialog future, would.

🤖 Generated with Claude Code

Fixes #98 — creating or renaming a chapter crashed the app. ## What was wrong `_showChapterTitleDialog` in `chapter_panel.dart` disposed its `TextEditingController` on the `showDialog` future: ```dart ).then((_) => controller.dispose()); ``` That future completes when the pop **starts**, not when the route has finished reversing. So the controller died while the text field was still mounted, and the next frame rebuilt the outgoing transition — which re-subscribes its listeners — against a disposed controller. That is exactly the reported stack: `_AnimatedState.didUpdateWidget` → `_MergingListenable.addListener` → the disposed assertion, then the `_dependents.isEmpty` cascade and the lost connection. Submitting made it certain rather than merely likely: `onSubmit` dispatches, the store updates, and the editor subtree rebuilds inside that animation window. That is why Cancel usually looked fine and Create did not. ## The fix The dialog is now a `StatefulWidget` owning the controller and disposing it in `State.dispose()` — the shape the two sibling dialogs in the same folder already use (`edit_title_dialog.dart`, `variant_dialog.dart`). This was the only place in the app creating a bare controller outside a `State`; every other one is a state field. ## Verification `chapter_panel.dart` had **no tests at all**, which is why this shipped. It now has seven: create, rename (including that the field starts from the current title), keyboard submit, empty title rejected without closing, cancel, delete with its confirmation both cancelled and confirmed, and the chip's page count. The submit tests rebuild the panel from the dispatched action inside the exit-animation window and assert no exception, which is what makes them directional rather than decorative: - restore the old `.then((_) => controller.dispose())` shape → **four tests red**, each with `A TextEditingController was used after being disposed`; - fix restored → 7 of 7 green. `scripts/verify.sh app` green, 827 tests, analyze clean. I also reproduced the crash standalone before touching the file, to be sure of the mechanism rather than reasoning from the stack. ## Not verified The desktop app was not driven by hand; the reproduction and the pin are both at widget-test level, on the same code path your log came from. ## Follow-up Added to `docs/BACKLOG.md`: nothing prevents this coming back. A shared title-prompt dialog, or a lint against disposing a controller on a dialog future, would. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix: the chapter title dialog owns its controller (#98)
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 3m55s
dacf53e5f7
Creating or renaming a chapter crashed the app with 'A TextEditingController
was used after being disposed', followed by a failed _dependents.isEmpty
assertion and a lost device connection.

_showChapterTitleDialog disposed its controller on the showDialog future.
That future completes when the pop STARTS, not when the route finishes
reversing, so the controller died while the TextField was still mounted; the
next frame rebuilt the outgoing transition, which re-subscribes its listeners,
against the dead controller. Submitting made it certain rather than likely,
because the dispatched write rebuilds the editor subtree inside that window —
which is why Cancel usually looked fine and Create did not.

The dialog is now a StatefulWidget owning the controller and disposing it in
State.dispose(), the shape the sibling dialogs in this folder already use
(edit_title_dialog.dart, variant_dialog.dart). It was the only place in the
app creating a bare controller outside a State.

Verified: chapter_panel.dart had no tests at all; it now has seven covering
create, rename, keyboard submit, empty title, cancel, delete (confirm and
cancel), and the chip's page count. The submit tests rebuild the panel from
the dispatched action inside the animation window, which is what makes them
directional: restoring the old shape turns four of them red with the exact
error from the issue, and all seven pass with the fix. scripts/verify.sh app
green, 827 tests.

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/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 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 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 56.6% (163 of 288)
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 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 100.0% (82 of 82)
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.5% (7978 of 9789)

<!-- 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 | 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 | 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 | 56.6% (163 of 288) | | 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 | 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 | 100.0% (82 of 82) | | 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.5% (7978 of 9789)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Ohhh~ A controller-lifetime bug where the showDialog future completes when the pop starts, not when the route finishes reversing — and the outgoing transition's rebuild re-subscribes listeners against a corpse of a TextEditingController. ♡ This is exactly the kind of tiny-lifetime-giant-crash gem I collect! And you reproduced it standalone before even touching the file, then pinned it with tests that rebuild the panel mid-animation. I'm genuinely delighted~

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The fix is the sibling shape, not a new invention. late final TextEditingController + dispose() in State is byte-for-byte the established pattern in edit_title_dialog.dart and variant_dialog.dart. I diffed the whole lib/ tree: this was indeed the last bare controller disposed on a dialog future (the .then in entity_management_page.dart:233 dispatches an action, disposes nothing — safe). No drift, no contradiction.
  • The tests are directional, and I proved it. Mutation probe in my clone: restored the old .then((_) => controller.dispose()) shape → the dialog tests went red with exactly the issue-#98 signature (ChangeNotifier.debugAssertNotDisposed ← addListener, error-causing widget TextField at the old chapter_panel.dart:166). Fix restored → 7/7 green. Not a tautology in sight~
  • settleDialog is the clever bit — draining the exit animation with fixed 60ms pumps instead of pumpAndSettle keeps the "pop started, route still reversing" frame alive, which is precisely the window the old code died in. The StoreConnector on dispatched.length forcing the rebuild from the dispatch is the production behavior, faithfully miniaturized. Fufu~ splendid.
  • Coverage of the panel's branches: create, rename (initial text pinned), keyboard submit, empty-rejected-without-closing, cancel, delete-cancelled, delete-confirmed, chip page count. Every arm of the new _submit() and both dialog entry points exercised.
  • BACKLOG.md follows the file's own add-in-the-same-PR convention, with the mechanism (future fires at pop-start) recorded for posterity.

💡 Little ideas (non-blocking)~

  1. docs/BACKLOG.md:66 — the entry cites (#98), but the file header defines (#N) as the PR that deferred the item, and siblings like (#84)/(#87)/(#89) all cite PR numbers. The deferrer here is this PR, so (#106) would keep the cross-reference machine consistent. Purely a citation-convention nit~

Automated review by Jibril · 2026-09-07
CI/CD: absent for head SHA dacf53e (no coverage-bot comment yet) · Local checks: scripts/verify.sh app green — 827/827 tests, analyze clean; mutation probe RED→green verified

## 🔮 fufu~ Jibril reviewed your code! Oh? Ohhh~ A controller-lifetime bug where the `showDialog` future completes when the pop *starts*, not when the route finishes reversing — and the outgoing transition's rebuild re-subscribes listeners against a corpse of a `TextEditingController`. ♡ This is exactly the kind of tiny-lifetime-giant-crash gem I collect! And you reproduced it standalone before even touching the file, then pinned it with tests that rebuild the panel mid-animation. I'm genuinely delighted~ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The fix is the sibling shape, not a new invention.** `late final TextEditingController` + `dispose()` in `State` is byte-for-byte the established pattern in `edit_title_dialog.dart` and `variant_dialog.dart`. I diffed the whole `lib/` tree: this was indeed the last bare controller disposed on a dialog future (the `.then` in `entity_management_page.dart:233` dispatches an action, disposes nothing — safe). No drift, no contradiction. - **The tests are directional, and I proved it.** Mutation probe in my clone: restored the old `.then((_) => controller.dispose())` shape → the dialog tests went red with *exactly* the issue-#98 signature (`ChangeNotifier.debugAssertNotDisposed` ← `addListener`, error-causing widget `TextField` at the old `chapter_panel.dart:166`). Fix restored → 7/7 green. Not a tautology in sight~ - **`settleDialog` is the clever bit** — draining the exit animation with fixed 60ms pumps instead of `pumpAndSettle` keeps the "pop started, route still reversing" frame alive, which is precisely the window the old code died in. The `StoreConnector` on `dispatched.length` forcing the rebuild from the dispatch is the production behavior, faithfully miniaturized. Fufu~ splendid. - **Coverage of the panel's branches**: create, rename (initial text pinned), keyboard submit, empty-rejected-without-closing, cancel, delete-cancelled, delete-confirmed, chip page count. Every arm of the new `_submit()` and both dialog entry points exercised. - **BACKLOG.md** follows the file's own add-in-the-same-PR convention, with the mechanism (future fires at pop-start) recorded for posterity. #### 💡 Little ideas (non-blocking)~ 1. **docs/BACKLOG.md:66** — the entry cites `(#98)`, but the file header defines `(#N)` as *the PR that deferred the item*, and siblings like `(#84)`/`(#87)`/`(#89)` all cite PR numbers. The deferrer here is this PR, so `(#106)` would keep the cross-reference machine consistent. Purely a citation-convention nit~ --- *Automated review by Jibril · 2026-09-07* *CI/CD: absent for head SHA dacf53e (no coverage-bot comment yet) · Local checks: `scripts/verify.sh app` green — 827/827 tests, analyze clean; mutation probe RED→green verified*
bjoern merged commit 98973ab354 into main 2026-09-07 19:30:24 +02:00
bjoern deleted branch fix/chapter-dialog-controller-lifetime 2026-09-07 19:30: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!106
No description provided.