fix: smart filter bar — overlay positioning, chip colors, min rating #39
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/smart-filter-bar"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes for 5 smart filter bar bugs
1. Bar position jumps when adding first chip
PreferredSizeheight 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
primarycolor is coral-red (#E94560), so every chip used red regardless of type. EachFilterTypenow has a distinct color:#64B5F6#66BB6A#EF5350#FFB74D#4DB6AC#BDBDBD3. Suggestions appear at bottom of page
The overlay used
Positioned(bottom: 0)which placed it at the bottom of the screen. Replaced withCompositedTransformFollower+CompositedTransformTarget— suggestions now appear directly below the text field, tracking its actual position.4. Can't click suggestions
The old
Positioned+TapRegionwrapper was absorbing pointer events. Simplified to a plainMaterial+ListViewofListTilewithonTap. 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,minRatingis set to null when the value is < 2.All 183 tests pass, 0 analyzer issues.
Flutter Coverage
Total: 75.4% (1329 of 1763)
Proper fix pushed. Here's what changed from the first attempt:
#1 Layout: Removed
SmartFilterBarfromAppBar.bottomentirely. It's now in the body as aColumn— filter bar on top with auto height, grid below inExpanded. NoPreferredSize, 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 useGalleryColors.accent(amber-gold), exclude usesGalleryColors.error(red), text usesGalleryColors.textSecondary.#4 Suggestions:
InkWellwith properonTapfor mouse clicks. Keyboard navigation added:#5 Rating triggers search:
UpdateMinRatingActionis now in the_filterTokensChangedEpicdebounced trigger list. Changing the min rating fires a new search after 300ms debounce.All four issues fixed:
#1 Arrow keys: The TextField is now wrapped in a
Focuswidget with a separate_keyboardFocusNode. Arrow Up/Down are intercepted at the Focus level and returnKeyEventResult.handled— the TextField never sees them, so the cursor stays put while the selection moves.#2 Tab inserts:
Focus.onKeyEventhandles Tab by returningKeyEventResult.handled, preventing the default focus traversal. Tab now inserts the highlighted suggestion.#3 Negative suggestions:
buildSuggestionshandles the-prefix. It strips the prefix, searches on the remaining text, and returns results asexcludeTagtokens with "Exclude tag:" labels. Languages/people/circles are only offered for positive queries.#4 Click adds: The overlay is now stateless — no
KeyboardListener, no separateFocusNode, noStoreProviderinside it.onSelectcallback runs in the parent's widget context whereStoreProvideris available. Clicks work because there's nothing intercepting the tap anymore.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 callsOverlayEntry.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 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),-kreturned nothing.Fix: the
-prefix now searches ALL types (languages, tags, people, circles). Whatever is selected getsnegative: trueon theFilterToken, so it renders as a red exclude chip. Also added a genericnegativeflag toFilterTokenso any type can be negative, not justexcludeTag.Now
-k→ shows "−Language: korean → ko", "−Tag: ..." (if any match), etc.Fixed. The chip color was determined by
token.typeonly — a negative language token (type: language, negative: true) still got the amber-gold accent color instead of red.Both
_colorForTypemethods (chip widget + suggestion overlay) now checktoken.isNegativefirst, returningGalleryColors.error(red) before falling through to the type-based color switch.Fixed. Language suggestions now:
ko+korean)noneexcluded entirelyLanguage: Korean,Language: Japanese,Language: Unknown→: typingde→Language: de → GermanFixed.
noneis back in suggestions. Addedtextlessas an alias fornoneso:textless→Language: Textlessnone→Language: none → Textlessunknown→Language: UnknownAll languages treated equally now — no special skipping.
🔮 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+CompositedTransformTargetapproach for the suggestion overlay is the correct Flutter way to anchor a floating UI — much better than the oldPositioned(bottom: 0)hack. And the negative-flag onFilterTokenis 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)~
[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 toKeyEventResult.ignored, which lets the TextField'sonSubmittedfire_commitTypedText(). That's the right behavior — but it relies on the_overlayEntry == nullguard happening before theKeyDownEventcheck. This works, just confirming the ordering is deliberate and correct. Fufu~ ♡[smart_filter_bar.dart —
_keyboardFocusNodelifecycle] — TheFocuswidget wrapping the TextField uses_keyboardFocusNodefor key interception whilewidget.focusNodehandles actual text focus. This dual-focus-node pattern is correct —descendantsAreFocusable: trueensures the TextField still receives focus. One tiny thing:_keyboardFocusNodeis disposed indispose()(✓) but never explicitly unfocused. In practice this doesn't matter because disposal handles it, but some Flutter lints prefer an explicit_keyboardFocusNode.unfocus()indispose(). Not a real issue — just a style note.[filter_token.dart —
buildSuggestionslanguage dedup logic] — The proper-name-vs-alias deduplication is clever (longest key = proper name, everything else = alias), but it's O(n) overknownLanguageson every keystroke. For the current15-entry language map this is trivially fast. But if♪knownLanguagesever grows large, consider caching thelangProper/langAliasesmaps as a static computed-once structure. Premature optimization for now — just a note for the future[filter_token.dart —
FilterTokenequality andnegativefield] — Addingnegativeto==andhashCodeis 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! TheisNegativegetter (negative || type == FilterType.excludeTag) correctly preserves backward compat with the old exclude-tag path. ♡[epics.dart —
minRatingnullification] —(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.toBackendRatingmaps 0.5 stars → backend rating 1, and the check>= 2means 1.0 star (backend 2) is the first filtering threshold. This is consistent with Björn's spec. Verified correct! ♪[filter_token.dart —
textlessalias fornone] — Adding'textless': 'none'toknownLanguagesis a clean solution — it's just another alias mapping, no special-casing needed inbuildSuggestions. The fact that typingnoneshowsLanguage: none → Textlesswhile typingtextlessshowsLanguage: Textlessis a natural consequence of the alias-resolution logic. Elegant! ♡[library_page.dart — layout restructure] — Moving
SmartFilterBarfromAppBar.bottominto the bodyColumnwithExpandedfor the grid is the right call.AppBar.bottomwithPreferredSizewas fighting the auto-height filter bar. Now the filter bar sizes naturally and the grid fills the rest. Clean fix. ♪[smart_filter_bar.dart —
GalleryColorsusage] — 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 policyStatic scan: clean (Dart only; no secrets, shell injection, eval/exec, pickle, or SQL injection in added lines)