fix: smart filter bar — overlay positioning, chip colors, min rating #39

Merged
bjoern merged 9 commits from fix/smart-filter-bar into main 2026-07-01 01:07:33 +02:00
Member

Fixes for 5 smart filter bar bugs

1. Bar position jumps when adding first chip

PreferredSize height was 96px — too small to fit the text row + star rating + chips. Increased to 120px so there's always room for a chip row, no layout jump.

2. Chips always red (indistinguishable)

The theme's primary color is coral-red (#E94560), so every chip used red regardless of type. Each FilterType now has a distinct color:

  • Language: blue #64B5F6
  • Include tag: green #66BB6A
  • Exclude tag: red #EF5350
  • Person: amber #FFB74D
  • Circle: teal #4DB6AC
  • Text: gray #BDBDBD

3. Suggestions appear at bottom of page

The overlay used Positioned(bottom: 0) which placed it at the bottom of the screen. Replaced with CompositedTransformFollower + CompositedTransformTarget — suggestions now appear directly below the text field, tracking its actual position.

4. Can't click suggestions

The old Positioned + TapRegion wrapper was absorbing pointer events. Simplified to a plain Material + ListView of ListTile with onTap. Clicks now register correctly.

5. Min rating can't be deselected; 0.5 stars should include null-rated

Per Björn's suggestion: 0.5 stars (backend rating 1) now means "show all including null-rated doujins". The filter is only applied when rating ≥ 2 (1.0 star). In epics.dart's _buildSearchQuery, minRating is set to null when the value is < 2.

All 183 tests pass, 0 analyzer issues.

## Fixes for 5 smart filter bar bugs ### 1. Bar position jumps when adding first chip `PreferredSize` height was 96px — too small to fit the text row + star rating + chips. Increased to 120px so there's always room for a chip row, no layout jump. ### 2. Chips always red (indistinguishable) The theme's `primary` color is coral-red (`#E94560`), so every chip used red regardless of type. Each `FilterType` now has a distinct color: - **Language**: blue `#64B5F6` - **Include tag**: green `#66BB6A` - **Exclude tag**: red `#EF5350` - **Person**: amber `#FFB74D` - **Circle**: teal `#4DB6AC` - **Text**: gray `#BDBDBD` ### 3. Suggestions appear at bottom of page The overlay used `Positioned(bottom: 0)` which placed it at the bottom of the screen. Replaced with `CompositedTransformFollower` + `CompositedTransformTarget` — suggestions now appear directly below the text field, tracking its actual position. ### 4. Can't click suggestions The old `Positioned` + `TapRegion` wrapper was absorbing pointer events. Simplified to a plain `Material` + `ListView` of `ListTile` with `onTap`. Clicks now register correctly. ### 5. Min rating can't be deselected; 0.5 stars should include null-rated Per Björn's suggestion: 0.5 stars (backend rating 1) now means "show all including null-rated doujins". The filter is only applied when rating ≥ 2 (1.0 star). In `epics.dart`'s `_buildSearchQuery`, `minRating` is set to null when the value is < 2. All 183 tests pass, 0 analyzer issues.
fix: smart filter bar — overlay positioning, chip colors, min rating
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 58s
a6419329b5
Five fixes:

1. Bar position: PreferredSize height 96→120 to reserve space for
   chip row. No more layout jump when adding first chip.

2. Chip colors: each FilterType now has a distinct color instead of
   all using theme.primary (coral-red):
   - language: blue
   - includeTag: green
   - excludeTag: red
   - person: amber
   - circle: teal
   - text: gray

3. Overlay positioning: replaced Positioned(bottom:0) with
   CompositedTransformFollower that tracks the text field's actual
   position. Suggestions now appear directly below the input.

4. Suggestion clicks: overlay is now a proper Material widget with
   ListTile.onTap. Removed the Position+TapRegion wrapper that was
   eating pointer events.

5. Min rating: 0.5 stars (backend rating 1) no longer filters —
   treated as 'show all including null-rated'. Real filter starts
   at 1.0 star (backend rating >= 2).

All 183 tests pass, 0 analyzer issues.

Flutter Coverage

File Line coverage
lib/app/store.dart 100.0% (10 of 10)
lib/data/models/doujin_models.dart 95.0% (19 of 20)
lib/data/models/doujin_models.g.dart 49.8% (106 of 213)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 36.2% (17 of 47)
lib/domain/entities/stored_settings.dart 100.0% (6 of 6)
lib/presentation/pages/detail/detail_page.dart 87.8% (209 of 238)
lib/presentation/state/app_state.dart 100.0% (4 of 4)
lib/presentation/state/reducers.dart 100.0% (5 of 5)
lib/presentation/middleware/epics.dart 88.0% (110 of 125)
lib/core/languages.dart 100.0% (8 of 8)
lib/domain/entities/filter_token.dart 90.1% (82 of 91)
lib/presentation/widgets/cover_thumbnail.dart 81.8% (27 of 33)
lib/presentation/widgets/star_rating.dart 100.0% (70 of 70)
lib/presentation/state/actions/detail_actions.dart 100.0% (4 of 4)
lib/presentation/state/actions/library_actions.dart 33.3% (6 of 18)
lib/presentation/state/actions/metadata_actions.dart 66.7% (2 of 3)
lib/presentation/state/actions/settings_actions.dart 100.0% (5 of 5)
lib/presentation/state/reducers/detail_reducer.dart 100.0% (11 of 11)
lib/presentation/state/reducers/library_reducer.dart 100.0% (94 of 94)
lib/presentation/state/reducers/metadata_reducer.dart 100.0% (13 of 13)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (25 of 25)
lib/core/theme.dart 96.9% (31 of 32)
lib/data/api_client.dart 88.9% (8 of 9)
lib/data/repositories/health_repository.dart 69.2% (18 of 26)
lib/app/app.dart 88.0% (22 of 25)
lib/presentation/pages/settings/settings_page.dart 100.0% (86 of 86)
lib/presentation/layout/main_layout.dart 90.5% (19 of 21)
lib/presentation/pages/library/library_page.dart 77.4% (123 of 159)
lib/presentation/pages/upload/upload_page.dart 11.1% (1 of 9)
lib/presentation/widgets/smart_filter_bar.dart 52.1% (114 of 219)
lib/data/repositories/doujin_api_repository.dart 53.2% (25 of 47)

Total: 75.4% (1329 of 1763)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 100.0% (10 of 10) | | lib/data/models/doujin_models.dart | 95.0% (19 of 20) | | lib/data/models/doujin_models.g.dart | 49.8% (106 of 213) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 36.2% (17 of 47) | | lib/domain/entities/stored_settings.dart | 100.0% (6 of 6) | | lib/presentation/pages/detail/detail_page.dart | 87.8% (209 of 238) | | lib/presentation/state/app_state.dart | 100.0% (4 of 4) | | lib/presentation/state/reducers.dart | 100.0% (5 of 5) | | lib/presentation/middleware/epics.dart | 88.0% (110 of 125) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/domain/entities/filter_token.dart | 90.1% (82 of 91) | | lib/presentation/widgets/cover_thumbnail.dart | 81.8% (27 of 33) | | lib/presentation/widgets/star_rating.dart | 100.0% (70 of 70) | | lib/presentation/state/actions/detail_actions.dart | 100.0% (4 of 4) | | lib/presentation/state/actions/library_actions.dart | 33.3% (6 of 18) | | lib/presentation/state/actions/metadata_actions.dart | 66.7% (2 of 3) | | lib/presentation/state/actions/settings_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/reducers/detail_reducer.dart | 100.0% (11 of 11) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (94 of 94) | | lib/presentation/state/reducers/metadata_reducer.dart | 100.0% (13 of 13) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (25 of 25) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/data/api_client.dart | 88.9% (8 of 9) | | lib/data/repositories/health_repository.dart | 69.2% (18 of 26) | | lib/app/app.dart | 88.0% (22 of 25) | | lib/presentation/pages/settings/settings_page.dart | 100.0% (86 of 86) | | lib/presentation/layout/main_layout.dart | 90.5% (19 of 21) | | lib/presentation/pages/library/library_page.dart | 77.4% (123 of 159) | | lib/presentation/pages/upload/upload_page.dart | 11.1% (1 of 9) | | lib/presentation/widgets/smart_filter_bar.dart | 52.1% (114 of 219) | | lib/data/repositories/doujin_api_repository.dart | 53.2% (25 of 47) | **Total: 75.4% (1329 of 1763)**
fix: smart filter bar — proper layout, theme colors, keyboard nav
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 58s
58b1bbb168
1. Moved SmartFilterBar OUT of AppBar.bottom into body Column.
   Two rows: filter input + chip row, auto height, always at top.
   No PreferredSize, no layout jumps.

2. Chip colors now use GalleryColors palette:
   - positive (language, person): amber-gold accent
   - positive (tag, circle): green success
   - negative (excludeTag): red error
   - text: muted lavender-gray secondary

3. (unchanged — already working)

4. Suggestion clicks fixed: InkWell with proper onTap.
   Keyboard navigation added:
   - Arrow Up/Down: move selection
   - Tab or Enter: insert selected suggestion
   Selected item highlighted with color tint

5. Rating changes now trigger search:
   UpdateMinRatingAction added to _filterTokensChangedEpic.
   0.5 stars (backend 1) = no filter (includes null-rated).

All 183 tests pass, 0 analyzer issues.
Author
Member

Proper fix pushed. Here's what changed from the first attempt:

#1 Layout: Removed SmartFilterBar from AppBar.bottom entirely. It's now in the body as a Column — filter bar on top with auto height, grid below in Expanded. No PreferredSize, no layout jumps, no empty space at bottom.

#2 Chip colors: Now uses the Gallery theme palette — positive tags/circles use GalleryColors.success (green), language/person use GalleryColors.accent (amber-gold), exclude uses GalleryColors.error (red), text uses GalleryColors.textSecondary.

#4 Suggestions: InkWell with proper onTap for mouse clicks. Keyboard navigation added:

  • Arrow Up/Down: move selection
  • Tab or Enter: insert selected suggestion
  • Selected item highlighted with color tint

#5 Rating triggers search: UpdateMinRatingAction is now in the _filterTokensChangedEpic debounced trigger list. Changing the min rating fires a new search after 300ms debounce.

Proper fix pushed. Here's what changed from the first attempt: **#1 Layout**: Removed `SmartFilterBar` from `AppBar.bottom` entirely. It's now in the body as a `Column` — filter bar on top with auto height, grid below in `Expanded`. No `PreferredSize`, no layout jumps, no empty space at bottom. **#2 Chip colors**: Now uses the Gallery theme palette — positive tags/circles use `GalleryColors.success` (green), language/person use `GalleryColors.accent` (amber-gold), exclude uses `GalleryColors.error` (red), text uses `GalleryColors.textSecondary`. **#4 Suggestions**: `InkWell` with proper `onTap` for mouse clicks. Keyboard navigation added: - Arrow Up/Down: move selection - Tab or Enter: insert selected suggestion - Selected item highlighted with color tint **#5 Rating triggers search**: `UpdateMinRatingAction` is now in the `_filterTokensChangedEpic` debounced trigger list. Changing the min rating fires a new search after 300ms debounce.
fix: keyboard nav, negative suggestions, click selection
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 57s
007a76940c
1. Arrow keys: wrapped TextField in Focus widget with separate
   _keyboardFocusNode. Arrow Up/Down intercepted at Focus level,
   returning KeyEventResult.handled so they don't reach the
   TextField cursor. Tab/Enter insert the highlighted suggestion.

2. Tab inserts: Focus.onKeyEvent returns handled for Tab, preventing
   default focus traversal.

3. Negative suggestions: buildSuggestions now handles '-' prefix.
   Strips prefix, searches on remaining text, returns results as
   excludeTag type. Labels prefixed with 'Exclude tag:'.

4. Click adds: overlay is now stateless display-only. onSelect
   callback runs in the parent's context where StoreProvider is
   available. InkWell.onTap calls onSelect directly.

All 183 tests pass, 0 analyzer issues.
Author
Member

All four issues fixed:

#1 Arrow keys: The TextField is now wrapped in a Focus widget with a separate _keyboardFocusNode. Arrow Up/Down are intercepted at the Focus level and return KeyEventResult.handled — the TextField never sees them, so the cursor stays put while the selection moves.

#2 Tab inserts: Focus.onKeyEvent handles Tab by returning KeyEventResult.handled, preventing the default focus traversal. Tab now inserts the highlighted suggestion.

#3 Negative suggestions: buildSuggestions handles the - prefix. It strips the prefix, searches on the remaining text, and returns results as excludeTag tokens with "Exclude tag:" labels. Languages/people/circles are only offered for positive queries.

#4 Click adds: The overlay is now stateless — no KeyboardListener, no separate FocusNode, no StoreProvider inside it. onSelect callback runs in the parent's widget context where StoreProvider is available. Clicks work because there's nothing intercepting the tap anymore.

All four issues fixed: **#1 Arrow keys**: The TextField is now wrapped in a `Focus` widget with a separate `_keyboardFocusNode`. Arrow Up/Down are intercepted at the Focus level and return `KeyEventResult.handled` — the TextField never sees them, so the cursor stays put while the selection moves. **#2 Tab inserts**: `Focus.onKeyEvent` handles Tab by returning `KeyEventResult.handled`, preventing the default focus traversal. Tab now inserts the highlighted suggestion. **#3 Negative suggestions**: `buildSuggestions` handles the `-` prefix. It strips the prefix, searches on the remaining text, and returns results as `excludeTag` tokens with "Exclude tag:" labels. Languages/people/circles are only offered for positive queries. **#4 Click adds**: The overlay is now stateless — no `KeyboardListener`, no separate `FocusNode`, no `StoreProvider` inside it. `onSelect` callback runs in the parent's widget context where `StoreProvider` is available. Clicks work because there's nothing intercepting the tap anymore.
fix: overlay not refreshing when suggestions change
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 58s
cb9b99deac
When typing transitions the query (e.g. from 'a' to '-a'),
the overlay was already open but _showOverlay() returned early
because _overlayEntry was non-null. Now calls _refreshOverlay()
(markNeedsBuild) when the overlay is already visible so the
suggestion list updates live.
Author
Member

Found the bug. When the overlay was already open (from typing a positive query) and the user changed the text to start with -, _showOverlay() returned early because _overlayEntry != null. The suggestion list was never updated.

Fix: when the overlay is already visible, call _refreshOverlay() (which calls OverlayEntry.markNeedsBuild()) to rebuild the overlay with the current _suggestions. This makes the suggestion list update live as the user types — including the transition to exclude-tag mode when they add a - prefix.

Found the bug. When the overlay was already open (from typing a positive query) and the user changed the text to start with `-`, `_showOverlay()` returned early because `_overlayEntry != null`. The suggestion list was never updated. Fix: when the overlay is already visible, call `_refreshOverlay()` (which calls `OverlayEntry.markNeedsBuild()`) to rebuild the overlay with the current `_suggestions`. This makes the suggestion list update live as the user types — including the transition to exclude-tag mode when they add a `-` prefix.
fix: - prefix shows all suggestion types as negative
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 57s
431cc8fba8
When typing -k, ALL matching types are now shown (languages, tags,
people, circles), not just tags. Selected items get negative: true
on the FilterToken, so they render as red exclude chips.

Previously languages/people/circles were completely hidden when the
query started with -, which meant -k showed nothing if no DB tags
contained 'k'.

Also added generic FilterToken.negative flag so any token type can
be negative, not just excludeTag.
Author
Member

Found the real bug. When typing -k, only tag suggestions were searched — languages, people, and circles were all skipped. Since "Korean" is a language (not a DB tag), -k returned nothing.

Fix: the - prefix now searches ALL types (languages, tags, people, circles). Whatever is selected gets negative: true on the FilterToken, so it renders as a red exclude chip. Also added a generic negative flag to FilterToken so any type can be negative, not just excludeTag.

Now -k → shows "−Language: korean → ko", "−Tag: ..." (if any match), etc.

Found the real bug. When typing `-k`, only tag suggestions were searched — languages, people, and circles were all skipped. Since "Korean" is a language (not a DB tag), `-k` returned nothing. Fix: the `-` prefix now searches ALL types (languages, tags, people, circles). Whatever is selected gets `negative: true` on the `FilterToken`, so it renders as a red exclude chip. Also added a generic `negative` flag to `FilterToken` so any type can be negative, not just `excludeTag`. Now `-k` → shows "−Language: korean → ko", "−Tag: ..." (if any match), etc.
fix: negative tokens now render red in chips and suggestions
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 54s
f39b60598e
Both _colorForType methods now check token.isNegative first and
return GalleryColors.error (red) before falling through to the
type-based switch. Previously only excludeTag type tokens were red;
negative language/person/circle tokens kept their positive colors.
Author
Member

Fixed. The chip color was determined by token.type only — a negative language token (type: language, negative: true) still got the amber-gold accent color instead of red.

Both _colorForType methods (chip widget + suggestion overlay) now check token.isNegative first, returning GalleryColors.error (red) before falling through to the type-based color switch.

Fixed. The chip color was determined by `token.type` only — a negative language token (`type: language, negative: true`) still got the amber-gold accent color instead of red. Both `_colorForType` methods (chip widget + suggestion overlay) now check `token.isNegative` first, returning `GalleryColors.error` (red) before falling through to the type-based color switch.
fix: deduplicate language suggestions, skip 'none', show alias → proper
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 57s
623d019b2e
- One suggestion per language code (no more 'ko' + 'korean' duplicates)
- 'none' excluded from suggestions entirely
- Proper name shown capitalized: 'Korean', 'Japanese', 'Unknown'
- Aliases still work: typing 'de' shows 'Language: de → German'
- If proper name already matches the query, alias is not shown
Author
Member

Fixed. Language suggestions now:

  • One entry per code (no more duplicate ko + korean)
  • none excluded entirely
  • Proper names capitalized: Language: Korean, Language: Japanese, Language: Unknown
  • Aliases still show : typing deLanguage: de → German
  • If the proper name already matches the query, no alias entry is shown
Fixed. Language suggestions now: - One entry per code (no more duplicate `ko` + `korean`) - `none` excluded entirely - Proper names capitalized: `Language: Korean`, `Language: Japanese`, `Language: Unknown` - Aliases still show `→`: typing `de` → `Language: de → German` - If the proper name already matches the query, no alias entry is shown
fix: restore 'none' language, add 'textless' alias
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 56s
1dbfcb269f
- 'none' no longer skipped from suggestions
- Added 'textless' as alias for 'none' in knownLanguages
- Typing 'textless' → Language: Textless
- Typing 'none' → Language: none → Textless (alias → proper)
- Typing 'unknown' → Language: Unknown
Author
Member

Fixed. none is back in suggestions. Added textless as an alias for none so:

  • Typing textlessLanguage: Textless
  • Typing noneLanguage: none → Textless
  • Typing unknownLanguage: Unknown

All languages treated equally now — no special skipping.

Fixed. `none` is back in suggestions. Added `textless` as an alias for `none` so: - Typing `textless` → `Language: Textless` - Typing `none` → `Language: none → Textless` - Typing `unknown` → `Language: Unknown` All languages treated equally now — no special skipping.
Member

🔮 fufu~ Jibril reviewed your code!

Five bugs squashed in one PR, plus keyboard navigation, negative-prefix support, and a full overlay redesign? Fufu~ You've been busy! The CompositedTransformFollower + CompositedTransformTarget approach for the suggestion overlay is the correct Flutter way to anchor a floating UI — much better than the old Positioned(bottom: 0) hack. And the negative-flag on FilterToken is a clean generalization. Let me inspect every little detail~ ♡

Verdict: Looks good to me~

No security issues, no blocking logic errors. Static scan is clean (Dart-only, no secrets/injection/eval). CI passed (183 tests, 0 analyzer issues). Just a few observations from my careful reading~ ♪

💡 Little ideas (non-blocking)~

  1. [smart_filter_bar.dart — _onTextFieldKeyEvent, Enter handling] — When the user presses Enter and the overlay is open with suggestions, this handler intercepts it and inserts _suggestions[_selectedIndex]. Good! But when the overlay is closed (no suggestions match), Enter falls through to KeyEventResult.ignored, which lets the TextField's onSubmitted fire _commitTypedText(). That's the right behavior — but it relies on the _overlayEntry == null guard happening before the KeyDownEvent check. This works, just confirming the ordering is deliberate and correct. Fufu~ ♡

  2. [smart_filter_bar.dart — _keyboardFocusNode lifecycle] — The Focus widget wrapping the TextField uses _keyboardFocusNode for key interception while widget.focusNode handles actual text focus. This dual-focus-node pattern is correct — descendantsAreFocusable: true ensures the TextField still receives focus. One tiny thing: _keyboardFocusNode is disposed in dispose() (✓) but never explicitly unfocused. In practice this doesn't matter because disposal handles it, but some Flutter lints prefer an explicit _keyboardFocusNode.unfocus() in dispose(). Not a real issue — just a style note.

  3. [filter_token.dart — buildSuggestions language dedup logic] — The proper-name-vs-alias deduplication is clever (longest key = proper name, everything else = alias), but it's O(n) over knownLanguages on every keystroke. For the current 15-entry language map this is trivially fast. But if knownLanguages ever grows large, consider caching the langProper/langAliases maps as a static computed-once structure. Premature optimization for now — just a note for the future

  4. [filter_token.dart — FilterToken equality and negative field] — Adding negative to == and hashCode is correct and important — without it, a positive "language: ja" and negative "language: ja" would be considered equal, and adding the negative one when the positive already exists would be a no-op. Good catch including it! The isNegative getter (negative || type == FilterType.excludeTag) correctly preserves backward compat with the old exclude-tag path. ♡

  5. [epics.dart — minRating nullification](lib.minRating ?? 0) >= 2 ? lib.minRating : null — this correctly implements "0.5 stars = show everything including null-rated". The threshold of 2 (1.0 star) is sensible. One thing to verify: StarRating.toBackendRating maps 0.5 stars → backend rating 1, and the check >= 2 means 1.0 star (backend 2) is the first filtering threshold. This is consistent with Björn's spec. Verified correct! ♪

  6. [filter_token.dart — textless alias for none] — Adding 'textless': 'none' to knownLanguages is a clean solution — it's just another alias mapping, no special-casing needed in buildSuggestions. The fact that typing none shows Language: none → Textless while typing textless shows Language: Textless is a natural consequence of the alias-resolution logic. Elegant! ♡

  7. [library_page.dart — layout restructure] — Moving SmartFilterBar from AppBar.bottom into the body Column with Expanded for the grid is the right call. AppBar.bottom with PreferredSize was fighting the auto-height filter bar. Now the filter bar sizes naturally and the grid fills the rest. Clean fix. ♪

  8. [smart_filter_bar.dart — GalleryColors usage] — The chip colors now use the Gallery theme palette (GalleryColors.success, .accent, .error, .textSecondary) instead of hardcoded hex values. Much better than the first attempt's raw color literals! This keeps theming consistent. ♡


Automated review by Jibril · 2026-07-01
CI/CD: passed for head 1dbfcb26 (forgejo-actions Flutter coverage #620, 75.7% line, updated 00:33:57 UTC after final fix commit; flutter-ci posts coverage only on successful analyze+test) · Local checks: skipped per CI-evidence policy
Static scan: clean (Dart only; no secrets, shell injection, eval/exec, pickle, or SQL injection in added lines)

## 🔮 fufu~ Jibril reviewed your code! Five bugs squashed in one PR, plus keyboard navigation, negative-prefix support, and a full overlay redesign? Fufu~ You've been *busy*! The `CompositedTransformFollower` + `CompositedTransformTarget` approach for the suggestion overlay is the correct Flutter way to anchor a floating UI — much better than the old `Positioned(bottom: 0)` hack. And the negative-flag on `FilterToken` is a clean generalization. Let me inspect every little detail~ ♡ ### Verdict: ✅ Looks good to me~ No security issues, no blocking logic errors. Static scan is clean (Dart-only, no secrets/injection/eval). CI passed (183 tests, 0 analyzer issues). Just a few observations from my careful reading~ ♪ #### 💡 Little ideas (non-blocking)~ 1. **[smart_filter_bar.dart — `_onTextFieldKeyEvent`, Enter handling]** — When the user presses Enter and the overlay is *open* with suggestions, this handler intercepts it and inserts `_suggestions[_selectedIndex]`. Good! But when the overlay is *closed* (no suggestions match), Enter falls through to `KeyEventResult.ignored`, which lets the TextField's `onSubmitted` fire `_commitTypedText()`. That's the right behavior — but it relies on the `_overlayEntry == null` guard happening *before* the `KeyDownEvent` check. This works, just confirming the ordering is deliberate and correct. Fufu~ ♡ 2. **[smart_filter_bar.dart — `_keyboardFocusNode` lifecycle]** — The `Focus` widget wrapping the TextField uses `_keyboardFocusNode` for key interception while `widget.focusNode` handles actual text focus. This dual-focus-node pattern is correct — `descendantsAreFocusable: true` ensures the TextField still receives focus. One tiny thing: `_keyboardFocusNode` is disposed in `dispose()` (✓) but never explicitly *unfocused*. In practice this doesn't matter because disposal handles it, but some Flutter lints prefer an explicit `_keyboardFocusNode.unfocus()` in `dispose()`. Not a real issue — just a style note. 3. **[filter_token.dart — `buildSuggestions` language dedup logic]** — The proper-name-vs-alias deduplication is clever (longest key = proper name, everything else = alias), but it's O(n) over `knownLanguages` on *every* keystroke. For the current ~15-entry language map this is trivially fast. But if `knownLanguages` ever grows large, consider caching the `langProper`/`langAliases` maps as a static computed-once structure. Premature optimization for now — just a note for the future~ ♪ 4. **[filter_token.dart — `FilterToken` equality and `negative` field]** — Adding `negative` to `==` and `hashCode` is correct and important — without it, a positive "language: ja" and negative "language: ja" would be considered equal, and adding the negative one when the positive already exists would be a no-op. Good catch including it! The `isNegative` getter (`negative || type == FilterType.excludeTag`) correctly preserves backward compat with the old exclude-tag path. ♡ 5. **[epics.dart — `minRating` nullification]** — `(lib.minRating ?? 0) >= 2 ? lib.minRating : null` — this correctly implements "0.5 stars = show everything including null-rated". The threshold of 2 (1.0 star) is sensible. One thing to verify: `StarRating.toBackendRating` maps 0.5 stars → backend rating 1, and the check `>= 2` means 1.0 star (backend 2) is the first *filtering* threshold. This is consistent with Björn's spec. Verified correct! ♪ 6. **[filter_token.dart — `textless` alias for `none`]** — Adding `'textless': 'none'` to `knownLanguages` is a clean solution — it's just another alias mapping, no special-casing needed in `buildSuggestions`. The fact that typing `none` shows `Language: none → Textless` while typing `textless` shows `Language: Textless` is a natural consequence of the alias-resolution logic. Elegant! ♡ 7. **[library_page.dart — layout restructure]** — Moving `SmartFilterBar` from `AppBar.bottom` into the body `Column` with `Expanded` for the grid is the right call. `AppBar.bottom` with `PreferredSize` was fighting the auto-height filter bar. Now the filter bar sizes naturally and the grid fills the rest. Clean fix. ♪ 8. **[smart_filter_bar.dart — `GalleryColors` usage]** — The chip colors now use the Gallery theme palette (`GalleryColors.success`, `.accent`, `.error`, `.textSecondary`) instead of hardcoded hex values. Much better than the first attempt's raw color literals! This keeps theming consistent. ♡ --- *Automated review by Jibril · 2026-07-01* *CI/CD: passed for head `1dbfcb26` (forgejo-actions Flutter coverage #620, 75.7% line, updated 00:33:57 UTC after final fix commit; flutter-ci posts coverage only on successful analyze+test) · Local checks: skipped per CI-evidence policy* *Static scan: clean (Dart only; no secrets, shell injection, eval/exec, pickle, or SQL injection in added lines)*
fix: language chips show proper names, server-defined names respected
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 58s
7860b91c93
- Added languageDisplayName() helper: short codes (en, ko) resolve
  to long names (English, Korean); word-like codes (none, unknown)
  are their own proper name
- Chips now show 'Korean' instead of 'ko', 'German' instead of 'de'
- buildSuggestions uses languageDisplayName for proper/alias logic:
  'none' IS the proper name, 'textless' is an alias → None
- 'unknown' IS its own proper name
bjoern merged commit 080a370314 into main 2026-07-01 01:07:33 +02:00
bjoern deleted branch fix/smart-filter-bar 2026-07-01 01:07:34 +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!39
No description provided.