fix: reading-progress un-stick, one title rule per surface, port-typed offline consumers, reader cache and sort guards #93

Merged
bjoern merged 6 commits from fix/app-small-fixes into main 2026-09-06 10:44:29 +02:00
Member

Five small, independently droppable app fixes from the backlog, one commit each, plus review fix-ups.

What's in

  1. Reading progress un-sticks when a finished variant gains pages: the library badge drops to in-progress and "Continue reading" reappears; equal or smaller page counts keep the flag, and a stale offline snapshot (smaller count) recorded after finishing no longer clears it on the next online open.
  2. One URL join: library and detail pages use core/url_utils.dart's joinServerUrl; the library card keeps "no link → placeholder".
  3. One label and one title rule per surface: variantKindLabel lives once in core/variant_display.dart; display surfaces (editor pane header, delete prompt) use core/title_display.dart's kind-priority primaryTitle; the editor's list-pane sync mirrors the server's summary rule (original, else first title) via serverSummaryTitle, so a list title no longer changes on mere selection (review finding — the first version of this branch had diverged from the server rule).
  4. Port instead of implementation: createAppEpic, createOfflineEpics, OfflineDownloadService and createStore take the OfflineStore port; the manifest-relative join lives in a Flutter-free core helper used by the store and the offline page, so only main.dart names FilesystemOfflineStore under lib/; dead StorageKeys in core/constants.dart and the never-assigned DownloadStatus.done (+ its unreachable queue-row arm) are gone.
  5. Guards: the reader restores the image-cache ceiling only if its own raise is still in effect (a value something else set meanwhile is left alone); insertEntitySorted uses code-point order, mirroring the server's BINARY collation, instead of String.compareTo.

Verification

  • scripts/verify.sh app: green (implementer run 697 tests; fixer run of the 12 touched suites 125 tests; full app suite green on the six-branch integration tree).
  • Probes (implementer + adversarial review, fix-ups in bdfda65): un-stick guard dropped / >= / != → red; shrink-then-grow → red without the max rule; placeholder branch → red; each former title helper restored → red; server-rule helper swapped for primaryTitle → red; cache guard removed → red; ordering reverted → red.
  • Decision to revisit: the app's kind-priority title (original > romaji > translated > alias > first) differs from the backend's summary rule (original, else first); aligning MapToSummary with the kind-priority rule is a backend change worth its own line. The offline orphan scan (#84) was left as is (not a one-liner) and stays in the backlog.

🤖 Generated with Claude Code

Five small, independently droppable app fixes from the backlog, one commit each, plus review fix-ups. ## What's in 1. **Reading progress un-sticks** when a finished variant gains pages: the library badge drops to in-progress and "Continue reading" reappears; equal or smaller page counts keep the flag, and a stale offline snapshot (smaller count) recorded after finishing no longer clears it on the next online open. 2. **One URL join**: library and detail pages use `core/url_utils.dart`'s `joinServerUrl`; the library card keeps "no link → placeholder". 3. **One label and one title rule per surface**: `variantKindLabel` lives once in `core/variant_display.dart`; display surfaces (editor pane header, delete prompt) use `core/title_display.dart`'s kind-priority `primaryTitle`; the editor's **list-pane sync** mirrors the server's summary rule (original, else first title) via `serverSummaryTitle`, so a list title no longer changes on mere selection (review finding — the first version of this branch had diverged from the server rule). 4. **Port instead of implementation**: `createAppEpic`, `createOfflineEpics`, `OfflineDownloadService` and `createStore` take the `OfflineStore` port; the manifest-relative join lives in a Flutter-free core helper used by the store and the offline page, so only `main.dart` names `FilesystemOfflineStore` under `lib/`; dead `StorageKeys` in `core/constants.dart` and the never-assigned `DownloadStatus.done` (+ its unreachable queue-row arm) are gone. 5. **Guards**: the reader restores the image-cache ceiling only if its own raise is still in effect (a value something else set meanwhile is left alone); `insertEntitySorted` uses code-point order, mirroring the server's `BINARY` collation, instead of `String.compareTo`. ## Verification - `scripts/verify.sh app`: green (implementer run 697 tests; fixer run of the 12 touched suites 125 tests; full app suite green on the six-branch integration tree). - Probes (implementer + adversarial review, fix-ups in bdfda65): un-stick guard dropped / `>=` / `!=` → red; shrink-then-grow → red without the max rule; placeholder branch → red; each former title helper restored → red; server-rule helper swapped for `primaryTitle` → red; cache guard removed → red; ordering reverted → red. - **Decision to revisit**: the app's kind-priority title (original > romaji > translated > alias > first) differs from the backend's summary rule (original, else first); aligning `MapToSummary` with the kind-priority rule is a backend change worth its own line. The offline orphan scan (#84) was left as is (not a one-liner) and stays in the backlog. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
What: RecordReadingProgressAction now compares the incoming pageCount
with the stored entry; when the variant grew, the sticky `finished` flag
is cleared so the library badge drops back to "in progress" and the
detail page's "Continue reading (page N)" affordance reappears. Reaching
the new last page finishes it again; a shrunk or equal page count keeps
the flag (re-reading earlier pages stays sticky).

Why: a finished variant that later received more pages stayed finished
forever (#72 follow-up), hiding the resume affordance for the new pages.

Verified: flutter test test/reading_progress_reducer_test.dart (11 pass;
3 new). Mutation probe: dropping the `!grew &&` guard turns "finished
un-sticks when the variant gained pages" red. flutter analyze clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What: drop the two private `_normalizeUrl` copies in `library_page.dart`
and `detail_page.dart` in favour of `core/url_utils.dart`'s
`joinServerUrl`. The library card keeps its "no link → placeholder"
behaviour by testing for the empty string `joinServerUrl` returns instead
of the null the local helper returned; the detail page already used ''.

Why: one URL-joining rule instead of three (backlog, App section).

Verified: flutter test test/library_page_test.dart test/detail_page_test.dart
(56 pass; 1 new — a linked card renders CoverThumbnail with the joined
URL, an unlinked card renders NoCoverPlaceholder). Mutation probe:
rendering the thumbnail regardless of the empty URL turns the new test
red. flutter analyze clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What: `variantKindLabel` now lives once in `core/variant_display.dart`
(the detail page's public copy, the variant dialog's public copy and the
tabs panel's private copy are gone; all four call sites render the same
strings as before). The editor pane header/delete prompt and the editor
reducer's list-pane summary sync (`EditorDoujinLoadedAction`,
`DoujinSavedAction`, `DoujinCreatedAction`) use `core/title_display.dart`'s
`primaryTitle` instead of their own rules.

Behaviour change, deliberately: the editor pane used "first title" and the
reducer used "original, else first" — both now follow the shared
kind-priority rule (original > romaji > translated > alias > first), so the
editor header matches the detail AppBar for the same doujin. Visible only
when no original title exists and the first-listed title is not the
highest-priority kind; the server's own summary mapping (original, else
first) replaces the synced list-pane title on the next list load, as it
already did for `DoujinCreatedAction`'s "first title".

Verified: flutter test on variant_display, title_display, editor_reducer,
editor_page and detail_page suites (49 pass; 6 new). Mutation probes:
reverting the pane header to `titles.first.value` turns "the editor pane
header shows the kind-priority title" red; reverting the reducer's two
sites to `titles.first.value` turns both new reducer tests red. flutter
analyze clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What:
- `createAppEpic`, `createOfflineEpics`, `OfflineDownloadService` and
  `createStore` now take the `OfflineStore` port instead of
  `FilesystemOfflineStore`; only `main.dart` names the implementation.
  Chosen over deleting the port because every consumer already used
  port methods only (the one concrete-only member, static `joinUnder`,
  is called from the offline page without a store handle), so the change
  is a type swap plus imports, and it restores the intended layering
  (presentation/middleware → domain port, not data implementation).
- `StorageKeys` in `core/constants.dart` was a dead duplicate of the
  live class in `data/secure_storage.dart`; removed.
- `DownloadStatus.done` was never assigned (the reducer's done-path moves
  the item into the library and drops it from the queue), so the enum
  value and the queue row's unreachable arm are gone; the switch stays
  exhaustive without a wildcard.

Verified: flutter test on the offline suites (epics, reader, reducer,
page, download service, store) plus detail and reader pages (125 pass;
1 new). Mutation probe: reverting `createOfflineEpics`'s parameter to
`FilesystemOfflineStore` makes offline_epics_test fail to compile
("argument type 'OfflineStore' can't be assigned"); the reader test's
`_buildStore` pins `createAppEpic` the same way. flutter analyze clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What:
- The reader restores the image cache ceiling on dispose only while its
  own raise is still in effect (`maximumSizeBytes` equals the value it
  set). A ceiling something else set meanwhile — an overlapping reader
  instance, a later adjustment — is left alone instead of being clamped
  back to the pre-raise value (#67 follow-up).
- `insertEntitySorted` compares `primaryName` by Unicode code point
  (new `core/code_point_compare.dart`) instead of `String.compareTo`
  (UTF-16 code units). Code-point order is UTF-8 byte order, i.e. the
  SQLite BINARY collation the server's entity lists (`ORDER BY Name` /
  `DisplayName`, no NOCASE) return, so a created or renamed entity lands
  where the next reload would put it even when its name has an emoji
  (#74 follow-up). Natural/case-insensitive sort was considered and
  rejected: the backend does not sort that way, and matching it is the
  point.

Verified: flutter test on code_point_compare, entity_reducer and
reader_page suites (36 pass; 4 new). Mutation probes: dropping the
"still ours" comparison turns "leaves a ceiling someone else set
meanwhile alone on dispose" red; reverting the comparator to
`String.compareTo` turns "EntityCreatedAction sorts by code point like
the server" red. flutter analyze clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix: list-pane title mirrors the server; sticky finished keeps its page count
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 3m34s
bdfda65c2c
- Editor list-pane sync (EditorDoujinLoaded / DoujinSaved / DoujinCreated)
  now writes serverSummaryTitle — the backend's MapToSummary rule
  (original kind, else FIRST title) — instead of the kind-priority
  primaryTitle, so a doujin without an original title no longer changes
  its list title on mere selection and flips back on the next list load.
  primaryTitle stays the rule for display surfaces (pane header, delete
  prompt, detail AppBar, reader).
- Reading progress: while `finished` is sticky the entry keeps
  max(existing, recorded) pageCount, so an offline snapshot recorded after
  finishing (smaller count) followed by an online open (full count) is no
  longer read as growth that clears `finished`; only growth beyond the
  finished count un-sticks.
- The manifest-relative join moves to the Flutter-free
  core/offline_paths.dart (joinUnder: null base or null relative → null).
  FilesystemOfflineStore.joinPath delegates to it, its static joinUnder
  is gone, and the offline page uses the core helper — presentation no
  longer imports the store implementation; only main.dart names it.
- Reader dispose: the imageCache-restore comment no longer claims to cover
  an overlapping reader; it guards against a value something else set
  meanwhile. No logic change.

Verified: scripts/verify.sh app (analyze 0 issues, 704 tests passed).
Directional probes, each red on flip and green after a byte-identical
restore: reducer sites switched back to primaryTitle (2 red),
serverSummaryTitle collapsed to primaryTitle (1 red), sticky pageCount
reverted to action.pageCount (1 red), joinUnder null cases returning
non-null (2 red).

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% (67 of 67)
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 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.2% (375 of 411)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (13 of 13)
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/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/duplicates_epics.dart 100.0% (23 of 23)
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/duplicates_actions.dart 50.0% (2 of 4)
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 100.0% (24 of 24)
lib/presentation/state/reducers/duplicates_reducer.dart 100.0% (21 of 21)
lib/presentation/state/reducers/editor_reducer.dart 93.1% (95 of 102)
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% (18 of 18)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (20 of 20)
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/core/variant_display.dart 100.0% (5 of 5)
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 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 26.1% (96 of 368)
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.3% (138 of 139)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/duplicates/duplicates_page.dart 100.0% (112 of 112)
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 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: 79.1% (7264 of 9185)

<!-- 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% (67 of 67) | | 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 | 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.2% (375 of 411) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (13 of 13) | | 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/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/duplicates_epics.dart | 100.0% (23 of 23) | | 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/duplicates_actions.dart | 50.0% (2 of 4) | | 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 | 100.0% (24 of 24) | | lib/presentation/state/reducers/duplicates_reducer.dart | 100.0% (21 of 21) | | lib/presentation/state/reducers/editor_reducer.dart | 93.1% (95 of 102) | | 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% (18 of 18) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (20 of 20) | | 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/core/variant_display.dart | 100.0% (5 of 5) | | 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 | 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 | 26.1% (96 of 368) | | 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.3% (138 of 139) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/duplicates/duplicates_page.dart | 100.0% (112 of 112) | | 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 | 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: 79.1% (7264 of 9185)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh~? Five independently droppable fixes, and every single one of them carries its own directional test and its own probe history? You even caught your own first version diverging from the server's summary rule and fixed it in bdfda65 before I could pounce~ ♡ I read the whole 1,416-line diff twice, both title rules side by side with the backend's MapToSummary, and every sibling that ever whispered FilesystemOfflineStore. My knife came out three times and found nowhere to land. How rude~

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The un-stick rule is airtight. grew only fires beyond the finished count because sticky keeps max(existing.pageCount, action.pageCount) — so the offline-shrink-then-online-restore sequence (8→6→8) can't masquerade as growth. The reducer tests walk exactly that sequence, plus re-finish and independence. I mutation-probed both halves: dropping !grew → 2 tests red; dropping the max() keep → 1 test red. Neither is a tautology~
  • serverSummaryTitle mirrors the backend exactly. I read DoujinEndpoints.cs:307 — Original ?? FirstOrDefault() — and the app helper is its faithful reflection, while display surfaces keep kind-priority primaryTitle. The one fixture where the rules disagree (translated-first vs romaji) is pinned from both directions in title_display_test.dart. Swapping the reducer to primaryTitle goes red. Delicious~
  • The reader cache guard restores only while maximumSizeBytes == _raisedImageCacheMaxBytesTo — a value someone else set meanwhile is theirs. Removed the guard → exactly the new dispose test red. The comment explains the why beautifully.
  • compareCodePoints — code-point order is UTF-8 byte order is SQLite BINARY order, and the doc comment says precisely that. The emoji-vs-fullwidth-A fixture proves it diverges from compareTo where it matters, and entity_reducer_test pins it at the reducer level too. Nested-reader loop is clean and prefix-correct.
  • Port-typing sweep is complete, not cosmetic. createAppEpic, createOfflineEpics, OfflineDownloadService, createStore all take OfflineStore; epics call only port members; main.dart is the only lib/ file naming FilesystemOfflineStore. The port test pins it at compile time. StorageKeys in constants.dart was a corpse (the live one lives in secure_storage.dart) — buried. DownloadStatus.done was unreachable — gone, with the queue-row arm and the stale test fixture honestly updated. No half-deletes anywhere.
  • One join, one label. joinServerUrl replaces both _normalizeUrl clones; variantKindLabel exists exactly once with a test enumerating every arm; the library placeholder branch (imageUrl.isNotEmpty) has its own widget test.
  • flutter analyze: No issues found (85s). All 13 touched suites pass locally (139 tests, run serially after the sandbox choked on parallel test workers — environment, not you). Clone left pristine at bdfda65.

💡 Little ideas (non-blocking)~

  1. offline_epics_test.dart:96 — the port-typing pin builds a real FilesystemOfflineStore(null). A five-line _FakeOfflineStore implements OfflineStore would make the pin purely about the port, no implementation involved. Truly optional — the null-base store degrades to no-op anyway.

Fufu~ five commits, five probe-verified guards, zero leftovers. Merge it before I change my mind about letting you have this one~ ♡


Automated review by Jibril · 2026-09-06
CI/CD: absent for head SHA bdfda65 · Local checks: analyze 0 issues · 13 touched suites 139/139 pass · 5 mutation probes red→green

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh~? Five independently droppable fixes, and every single one of them carries its own directional test *and* its own probe history? You even caught your own first version diverging from the server's summary rule and fixed it in `bdfda65` before I could pounce~ ♡ I read the whole 1,416-line diff twice, both title rules side by side with the backend's `MapToSummary`, and every sibling that ever whispered `FilesystemOfflineStore`. My knife came out three times and found nowhere to land. How *rude~* ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The un-stick rule is airtight.** `grew` only fires beyond the *finished* count because sticky keeps `max(existing.pageCount, action.pageCount)` — so the offline-shrink-then-online-restore sequence (8→6→8) can't masquerade as growth. The reducer tests walk exactly that sequence, plus re-finish and independence. I mutation-probed both halves: dropping `!grew` → 2 tests red; dropping the `max()` keep → 1 test red. Neither is a tautology~ - **`serverSummaryTitle` mirrors the backend *exactly*.** I read `DoujinEndpoints.cs:307` — `Original ?? FirstOrDefault()` — and the app helper is its faithful reflection, while display surfaces keep kind-priority `primaryTitle`. The one fixture where the rules disagree (translated-first vs romaji) is pinned from *both* directions in `title_display_test.dart`. Swapping the reducer to `primaryTitle` goes red. Delicious~ - **The reader cache guard** restores only while `maximumSizeBytes == _raisedImageCacheMaxBytesTo` — a value someone else set meanwhile is *theirs*. Removed the guard → exactly the new dispose test red. The comment explains the why beautifully. - **`compareCodePoints`** — code-point order is UTF-8 byte order is SQLite BINARY order, and the doc comment says precisely that. The emoji-vs-fullwidth-A fixture proves it diverges from `compareTo` where it matters, and `entity_reducer_test` pins it at the reducer level too. Nested-reader loop is clean and prefix-correct. - **Port-typing sweep is complete, not cosmetic.** `createAppEpic`, `createOfflineEpics`, `OfflineDownloadService`, `createStore` all take `OfflineStore`; epics call only port members; `main.dart` is the only `lib/` file naming `FilesystemOfflineStore`. The port test pins it at compile time. `StorageKeys` in `constants.dart` was a corpse (the live one lives in `secure_storage.dart`) — buried. `DownloadStatus.done` was unreachable — gone, with the queue-row arm and the stale test fixture honestly updated. No half-deletes anywhere. - **One join, one label.** `joinServerUrl` replaces both `_normalizeUrl` clones; `variantKindLabel` exists exactly once with a test enumerating every arm; the library placeholder branch (`imageUrl.isNotEmpty`) has its own widget test. - `flutter analyze`: **No issues found** (85s). All 13 touched suites pass locally (139 tests, run serially after the sandbox choked on parallel test workers — environment, not you). Clone left pristine at `bdfda65`. #### 💡 Little ideas (non-blocking)~ 1. **offline_epics_test.dart:96** — the port-typing pin builds a real `FilesystemOfflineStore(null)`. A five-line `_FakeOfflineStore implements OfflineStore` would make the pin purely about the port, no implementation involved. Truly optional — the null-base store degrades to no-op anyway. Fufu~ five commits, five probe-verified guards, zero leftovers. Merge it before I change my mind about letting you have this one~ ♡ --- *Automated review by Jibril · 2026-09-06* *CI/CD: absent for head SHA bdfda65 · Local checks: analyze 0 issues · 13 touched suites 139/139 pass · 5 mutation probes red→green*
bjoern merged commit 4431c67672 into main 2026-09-06 10:44:29 +02:00
bjoern deleted branch fix/app-small-fixes 2026-09-06 10:44:29 +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!93
No description provided.