From 2f5260ac841328d70e7be48c80fdb5463f2b38ae Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Mon, 19 Jan 2026 17:54:58 +0100 Subject: [PATCH] introduce smart bang selector --- .../search/presentation/screens/search.dart | 149 +++---- .../presentation/widgets/bang_chips.dart | 121 ------ .../full_search_suggestions.dart | 40 +- .../widgets/smart_bang_selector.dart | 370 ++++++++++++++++++ 4 files changed, 422 insertions(+), 258 deletions(-) delete mode 100644 app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart create mode 100644 app/lib/features/geckoview/features/search/presentation/widgets/smart_bang_selector.dart diff --git a/app/lib/features/geckoview/features/search/presentation/screens/search.dart b/app/lib/features/geckoview/features/search/presentation/screens/search.dart index 703094d8..90bbddcf 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -24,11 +24,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:nullability/nullability.dart'; import 'package:weblibre/core/design/app_colors.dart'; import 'package:weblibre/core/routing/routes.dart'; -import 'package:weblibre/extensions/uri.dart'; -import 'package:weblibre/features/bangs/data/models/bang_data.dart'; import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; import 'package:weblibre/features/bangs/domain/providers/search.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; @@ -46,8 +43,6 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/on_listenable_change_selector.dart'; import 'package:weblibre/presentation/hooks/sampled_value_notifier.dart'; -import 'package:weblibre/presentation/widgets/selectable_chips.dart'; -import 'package:weblibre/presentation/widgets/url_icon.dart'; import 'package:weblibre/utils/text_field_line_count.dart'; import 'package:weblibre/utils/uri_parser.dart' as uri_parser; @@ -152,28 +147,30 @@ class SearchScreen extends HookConsumerWidget { return null; }, []); - useEffect(() { - void measureHeight() { + Future measureHeightWithRetry() async { + const delays = [25, 50, 75, 100]; + + for (final delay in delays) { + await Future.delayed(Duration(milliseconds: delay)); final measuredHeight = getTextFieldHeight(textFieldKey); if (measuredHeight != null) { preferredHeight.value = measuredHeight; + return; } } + } + useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) { - measureHeight(); + measureHeightWithRetry(); // ignore: discarded_futures }); return null; }, [textFieldKey, isEditMode]); useOnListenableChange(isEditMode ? searchTextController : null, () { - final measuredHeight = getTextFieldHeight(textFieldKey); - - if (measuredHeight != null && preferredHeight.value != measuredHeight) { - preferredHeight.value = measuredHeight; - } + measureHeightWithRetry(); // ignore: discarded_futures }); useOnAppLifecycleStateChange((previous, current) { @@ -195,22 +192,25 @@ class SearchScreen extends HookConsumerWidget { defaultSearchBangDataProvider.select((value) => value.value), ); - final selectedBang = useState(null); - final activeBang = selectedBang.value ?? defaultSearchBang; - final showBangIcon = useState(false); + // Watch both selection providers - only one should be set at a time + // due to mutual exclusion in SmartBangSelector + final siteSelectedBang = isEditMode + ? ref.watch(selectedBangDataProvider(domain: existingTabState.url.host)) + : null; + final globalSelectedBang = ref.watch(selectedBangDataProvider()); - ref.listen( - selectedBangDataProvider( - domain: isEditMode ? existingTabState.url.host : null, - ), - (previous, next) { - if (previous != next) { - showBangIcon.value = true; - } + // The active selection is whichever one is set (site takes priority if both somehow set) + final selectedBang = siteSelectedBang ?? globalSelectedBang; + final activeBang = selectedBang ?? defaultSearchBang; + final showBangIcon = selectedBang != null; - selectedBang.value = next; - }, - ); + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + measureHeightWithRetry(); // ignore: discarded_futures + }); + + return null; + }, [showBangIcon]); Future submitSearch(String query) async { if (activeBang != null && (formKey.currentState?.validate() == true)) { @@ -263,12 +263,9 @@ class SearchScreen extends HookConsumerWidget { floating: true, pinned: true, automaticallyImplyLeading: false, + toolbarHeight: isEditMode ? 0 : kToolbarHeight, title: isEditMode - ? _SiteBangsSelector( - tabId: tabId!, - domain: existingTabState.url.host, - searchTextController: searchTextController, - ) + ? null : Align( child: Focus( canRequestFocus: false, @@ -325,7 +322,7 @@ class SearchScreen extends HookConsumerWidget { padding: const EdgeInsets.only(left: 16.0), child: SearchField( textFieldKey: textFieldKey, - showBangIcon: showBangIcon.value, + showBangIcon: showBangIcon, textEditingController: searchTextController, focusNode: searchFocusNode, maxLines: isEditMode ? 3 : 1, @@ -342,14 +339,20 @@ class SearchScreen extends HookConsumerWidget { ); if (newUrl == null) { + // Read from both providers - use site if set, otherwise global + final siteBang = isEditMode + ? ref.read( + selectedBangDataProvider( + domain: existingTabState.url.host, + ), + ) + : null; + final globalBang = ref.read( + selectedBangDataProvider(), + ); final bang = - ref.read( - selectedBangDataProvider( - domain: isEditMode - ? existingTabState.url.host - : null, - ), - ) ?? + siteBang ?? + globalBang ?? await ref.read( defaultSearchBangDataProvider.future, ); @@ -418,6 +421,7 @@ class SearchScreen extends HookConsumerWidget { searchTextController: searchTextController, activeBang: activeBang, submitSearch: submitSearch, + domain: isEditMode ? existingTabState.url.host : null, ), TabSearch(searchTextListenable: sampledSearchText), FeedSearch(searchTextNotifier: sampledSearchText), @@ -434,68 +438,3 @@ class SearchScreen extends HookConsumerWidget { ); } } - -class _SiteBangsSelector extends HookConsumerWidget { - final String tabId; - final String domain; - final TextEditingController searchTextController; - - const _SiteBangsSelector({ - required this.tabId, - required this.domain, - required this.searchTextController, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedBang = ref.watch(selectedBangDataProvider(domain: domain)); - final availableBangs = ref.watch( - bangListProvider( - domain: domain, - orderMostFrequentFirst: true, - ).select((value) => value.value ?? const []), - ); - - if (availableBangs.isEmpty) { - return const SizedBox.shrink(); - } - - return SizedBox( - height: 48, - width: double.maxFinite, - child: SelectableChips( - itemId: (bang) => bang.trigger, - itemAvatar: (bang) => UrlIcon([bang.getDefaultUrl()], iconSize: 20), - itemLabel: (bang) => Text(bang.websiteName), - itemTooltip: (bang) => bang.trigger, - availableItems: availableBangs, - selectedItem: selectedBang, - onSelected: (bang) { - if (selectedBang == null) { - final hasSupportedScheme = - uri_parser - .tryParseUrl(searchTextController.text) - .mapNotNull((uri) => uri.hasSupportedScheme) ?? - false; - - if (hasSupportedScheme) { - searchTextController.clear(); - } - } - - ref - .read(selectedBangTriggerProvider(domain: domain).notifier) - .setTrigger(bang.toKey()); - }, - onDeleted: (bang) { - if (ref.read(selectedBangTriggerProvider(domain: domain)) == - bang.toKey()) { - ref - .read(selectedBangTriggerProvider(domain: domain).notifier) - .clearTrigger(); - } - }, - ), - ); - } -} diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart b/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart deleted file mode 100644 index bc947fd4..00000000 --- a/app/lib/features/geckoview/features/search/presentation/widgets/bang_chips.dart +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2024-2025 Fabian Freund. - * - * This file is part of WebLibre - * (see https://weblibre.eu). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:nullability/nullability.dart'; -import 'package:weblibre/core/routing/routes.dart'; -import 'package:weblibre/features/bangs/data/models/bang_data.dart'; -import 'package:weblibre/features/bangs/data/models/bang_key.dart'; -import 'package:weblibre/features/bangs/domain/providers/search.dart'; -import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; -import 'package:weblibre/presentation/widgets/selectable_chips.dart'; -import 'package:weblibre/presentation/widgets/url_icon.dart'; - -class BangChips extends HookConsumerWidget { - final bool displayMenu; - - final BangData? activeBang; - final void Function(BangData)? onSelected; - final void Function(BangData)? onDeleted; - - final TextEditingController? searchTextController; - - const BangChips({ - required this.activeBang, - required this.onSelected, - required this.onDeleted, - this.searchTextController, - this.displayMenu = true, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final availableBangs = ref.watch(seamlessBangProvider); - - useOnListenableChange(searchTextController, () { - ref - .read(seamlessBangProvider.notifier) - .search(searchTextController!.text); - }); - - return availableBangs.when( - skipLoadingOnReload: true, - data: (availableBangs) { - return SizedBox( - height: 48, - child: Row( - children: [ - if (activeBang != null || availableBangs.isNotEmpty) - Expanded( - child: SelectableChips( - itemId: (bang) => bang.trigger, - itemAvatar: (bang) => - UrlIcon([bang.getDefaultUrl()], iconSize: 20), - itemLabel: (bang) => Text(bang.websiteName), - itemTooltip: (bang) => bang.trigger, - availableItems: availableBangs, - selectedItem: activeBang, - onSelected: onSelected, - onDeleted: onDeleted, - ), - ) - else - Visibility( - visible: displayMenu, - child: Expanded( - child: Text( - "Press '>' to search Bangs.", - style: TextStyle( - color: Theme.of(context).hintColor, - fontStyle: FontStyle.italic, - ), - ), - ), - ), - if (displayMenu) - IconButton( - onPressed: () async { - final searchText = searchTextController?.text.trim(); - - final trigger = await BangSearchRoute( - searchText: (searchText.isEmpty) - ? BangSearchRoute.emptySearchText - : searchText!, - ).push(context); - - if (trigger != null) { - ref - .read(selectedBangTriggerProvider().notifier) - .setTrigger(trigger); - } - }, - icon: const Icon(Icons.chevron_right), - ), - ], - ), - ); - }, - error: (error, stackTrace) => const SizedBox.shrink(), - loading: () => const SizedBox(height: 48, width: double.infinity), - ); - } -} diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart index aab17cc7..de2cd559 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/full_search_suggestions.dart @@ -27,20 +27,23 @@ import 'package:sliver_tools/sliver_tools.dart'; import 'package:weblibre/features/bangs/data/models/bang_data.dart'; import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; import 'package:weblibre/features/bangs/domain/repositories/data.dart'; -import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/search/domain/providers/search_suggestions.dart'; -import 'package:weblibre/features/geckoview/features/search/presentation/dialogs/reset_bang_dialog.dart'; -import 'package:weblibre/features/geckoview/features/search/presentation/widgets/bang_chips.dart'; +import 'package:weblibre/features/geckoview/features/search/presentation/widgets/smart_bang_selector.dart'; class FullSearchTermSuggestions extends HookConsumerWidget { final TextEditingController searchTextController; final Future Function(String query) submitSearch; final BangData? activeBang; + /// The domain to scope site-specific bangs to. + /// When null, only global bangs are shown (new tab mode). + final String? domain; + const FullSearchTermSuggestions({ required this.searchTextController, required this.submitSearch, required this.activeBang, + this.domain, super.key, }); @@ -152,35 +155,8 @@ class FullSearchTermSuggestions extends HookConsumerWidget { 'Search Provider', style: Theme.of(context).textTheme.labelSmall, ), - BangChips( - key: ValueKey(activeBang), - activeBang: activeBang, - onSelected: (bang) { - searchTextController.clear(); - - ref - .read(selectedBangTriggerProvider().notifier) - .setTrigger(bang.toKey()); - }, - onDeleted: (bang) async { - if (ref.read(selectedBangTriggerProvider()) == - bang.toKey()) { - ref - .read(selectedBangTriggerProvider().notifier) - .clearTrigger(); - } else { - final dialogResult = await showResetBangDialog( - context, - triggerName: bang.trigger, - ); - - if (dialogResult == true) { - await ref - .read(bangDataRepositoryProvider.notifier) - .resetFrequency(bang.trigger); - } - } - }, + SmartBangSelector( + domain: domain, searchTextController: searchTextController, ), ], diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/smart_bang_selector.dart b/app/lib/features/geckoview/features/search/presentation/widgets/smart_bang_selector.dart new file mode 100644 index 00000000..8d8de9c9 --- /dev/null +++ b/app/lib/features/geckoview/features/search/presentation/widgets/smart_bang_selector.dart @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2024-2025 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:nullability/nullability.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/extensions/uri.dart'; +import 'package:weblibre/features/bangs/data/models/bang_data.dart'; +import 'package:weblibre/features/bangs/data/models/bang_key.dart'; +import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; +import 'package:weblibre/features/bangs/domain/providers/search.dart'; +import 'package:weblibre/features/bangs/domain/repositories/data.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; +import 'package:weblibre/features/geckoview/features/search/presentation/dialogs/reset_bang_dialog.dart'; +import 'package:weblibre/presentation/widgets/selectable_chips.dart'; +import 'package:weblibre/presentation/widgets/url_icon.dart'; +import 'package:weblibre/utils/uri_parser.dart' as uri_parser; + +/// A unified bang selector widget that displays site-specific and/or global +/// search bangs with a tabbed interface when in edit mode. +/// +/// In edit mode (domain != null): +/// - Shows "Site" and "All" tabs when site-specific bangs exist +/// - "Site" tab displays only domain-specific bangs +/// - "All" tab displays global bangs (same as new tab mode) +/// - Selecting in one tab clears selection in the other (mutual exclusion) +/// +/// In new tab mode (domain == null): +/// - Shows global bangs without tabs +class SmartBangSelector extends HookConsumerWidget { + /// The domain to scope site-specific bangs to. + /// When null, only global bangs are shown (new tab mode). + final String? domain; + + /// Controller for the search text field. + /// Used for text clearing on selection and for seamless bang search. + final TextEditingController searchTextController; + + /// Whether to display the menu button to open bang search. + final bool displayMenu; + + const SmartBangSelector({ + required this.domain, + required this.searchTextController, + this.displayMenu = true, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isEditMode = domain != null; + + // Watch site-specific bangs (only in edit mode) + final siteBangs = isEditMode + ? ref.watch( + bangListProvider( + domain: domain, + orderMostFrequentFirst: true, + ).select((value) => value.value ?? const []), + ) + : const []; + + // Watch global bangs via seamless provider (supports search) + // When search text is empty: shows frequentBangListProvider + // When search text is not empty: shows search results + final searchBangs = ref.watch( + seamlessBangProvider.select((v) => v.value ?? const []), + ); + + // Also watch frequent bangs as fallback when search returns empty + final frequentBangs = ref.watch( + frequentBangListProvider.select((v) => v.value ?? const []), + ); + + // Use search results if available, otherwise fall back to frequent bangs + final globalBangs = searchBangs.isNotEmpty ? searchBangs : frequentBangs; + + // Trigger search when text changes + useOnListenableChange(searchTextController, () { + ref.read(seamlessBangProvider.notifier).search(searchTextController.text); + }); + + // Determine if we should show tabs + final showTabs = isEditMode && siteBangs.isNotEmpty; + + // Watch both selection providers to determine the active bang + final siteSelectedBang = isEditMode + ? ref.watch(selectedBangDataProvider(domain: domain)) + : null; + final globalSelectedBang = ref.watch(selectedBangDataProvider()); + + // The active bang is whichever one is set (only one should be set at a time) + final activeBang = siteSelectedBang ?? globalSelectedBang; + + if (showTabs) { + return _TabbedBangSelector( + domain: domain!, + siteBangs: siteBangs, + globalBangs: globalBangs, + activeBang: activeBang, + isSiteSelected: siteSelectedBang != null, + searchTextController: searchTextController, + displayMenu: displayMenu, + ); + } + + // No tabs - show global bangs directly + return _BangChipsList( + domain: domain, + siteDomain: null, // No mutual exclusion needed without tabs + bangs: globalBangs, + selectedBang: activeBang, + searchTextController: searchTextController, + displayMenu: displayMenu, + ); + } +} + +/// Tabbed bang selector with "Site" and "All" tabs. +class _TabbedBangSelector extends HookConsumerWidget { + final String domain; + final List siteBangs; + final List globalBangs; + final BangData? activeBang; + final bool isSiteSelected; + final TextEditingController searchTextController; + final bool displayMenu; + + const _TabbedBangSelector({ + required this.domain, + required this.siteBangs, + required this.globalBangs, + required this.activeBang, + required this.isSiteSelected, + required this.searchTextController, + required this.displayMenu, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabController = useTabController(initialLength: 2); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Tab bar - full width + SizedBox( + height: 32, + width: double.infinity, + child: TabBar( + controller: tabController, + dividerColor: Colors.transparent, + tabs: const [ + Tab(text: 'Site'), + Tab(text: 'All'), + ], + ), + ), + // Tab content + SizedBox( + height: 48, + child: TabBarView( + controller: tabController, + children: [ + // Site tab - uses domain-scoped provider, clears global on select + _BangChipsList( + domain: domain, + siteDomain: domain, // Pass for mutual exclusion + bangs: siteBangs, + selectedBang: isSiteSelected ? activeBang : null, + searchTextController: searchTextController, + displayMenu: displayMenu, + ), + // All tab - uses global provider, clears site on select + _BangChipsList( + domain: null, + siteDomain: domain, // Pass for mutual exclusion + bangs: globalBangs, + selectedBang: !isSiteSelected ? activeBang : null, + searchTextController: searchTextController, + displayMenu: displayMenu, + ), + ], + ), + ), + ], + ); + } +} + +/// The actual chip list with selection handling. +class _BangChipsList extends HookConsumerWidget { + /// The domain for this list's selection provider. + /// null = global provider, non-null = domain-scoped provider. + final String? domain; + + /// The site domain for mutual exclusion. + /// When selecting, the OTHER provider (site vs global) will be cleared. + /// null = no mutual exclusion (single mode without tabs). + final String? siteDomain; + + final List bangs; + final BangData? selectedBang; + final TextEditingController searchTextController; + final bool displayMenu; + + const _BangChipsList({ + required this.domain, + required this.siteDomain, + required this.bangs, + required this.selectedBang, + required this.searchTextController, + required this.displayMenu, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasContent = selectedBang != null || bangs.isNotEmpty; + + return SizedBox( + height: 48, + child: Row( + children: [ + if (hasContent) + Expanded( + child: SelectableChips( + itemId: (bang) => bang.trigger, + itemAvatar: (bang) => + UrlIcon([bang.getDefaultUrl()], iconSize: 20), + itemLabel: (bang) => Text(bang.websiteName), + itemTooltip: (bang) => bang.trigger, + availableItems: bangs, + selectedItem: selectedBang, + onSelected: (bang) => _handleSelection(context, ref, bang), + onDeleted: (bang) => _handleDeletion(context, ref, bang), + ), + ) + else if (displayMenu) + Expanded( + child: Text( + "Press '>' to search Bangs.", + style: TextStyle( + color: Theme.of(context).hintColor, + fontStyle: FontStyle.italic, + ), + ), + ) + else + const Spacer(), + if (displayMenu) + IconButton( + onPressed: () => _openBangSearch(context, ref), + icon: const Icon(Icons.chevron_right), + ), + ], + ), + ); + } + + void _handleSelection(BuildContext context, WidgetRef ref, BangData bang) { + // Text clearing logic: + // - Site tab (domain != null): only clear if text is a valid URL + // - All tab (domain == null): always clear (same as original BangChips) + if (domain == null) { + // All tab / new tab mode - always clear + searchTextController.clear(); + } else { + // Site tab - only clear if text is a valid URL + final hasSupportedScheme = uri_parser + .tryParseUrl(searchTextController.text) + .mapNotNull((uri) => uri.hasSupportedScheme) ?? + false; + + if (hasSupportedScheme) { + searchTextController.clear(); + } + } + + // Clear the OTHER provider for mutual exclusion + if (siteDomain != null) { + if (domain == null) { + // We're in All tab (global), clear Site selection + ref + .read(selectedBangTriggerProvider(domain: siteDomain).notifier) + .clearTrigger(); + } else { + // We're in Site tab, clear All (global) selection + ref.read(selectedBangTriggerProvider().notifier).clearTrigger(); + } + } + + // Set selection using this list's provider scope + ref + .read(selectedBangTriggerProvider(domain: domain).notifier) + .setTrigger(bang.toKey()); + } + + Future _handleDeletion( + BuildContext context, + WidgetRef ref, + BangData bang, + ) async { + final currentSelection = + ref.read(selectedBangTriggerProvider(domain: domain)); + + if (currentSelection == bang.toKey()) { + // Clear selection if the deleted bang is currently selected + ref + .read(selectedBangTriggerProvider(domain: domain).notifier) + .clearTrigger(); + } else { + // Show reset frequency dialog for non-selected bangs + final dialogResult = await showResetBangDialog( + context, + triggerName: bang.trigger, + ); + + if (dialogResult == true) { + await ref + .read(bangDataRepositoryProvider.notifier) + .resetFrequency(bang.trigger); + } + } + } + + Future _openBangSearch(BuildContext context, WidgetRef ref) async { + final searchText = searchTextController.text.trim(); + + final trigger = await BangSearchRoute( + searchText: searchText.isEmpty + ? BangSearchRoute.emptySearchText + : searchText, + ).push(context); + + if (trigger != null) { + // Clear the OTHER provider for mutual exclusion + if (siteDomain != null) { + if (domain == null) { + ref + .read(selectedBangTriggerProvider(domain: siteDomain).notifier) + .clearTrigger(); + } else { + ref.read(selectedBangTriggerProvider().notifier).clearTrigger(); + } + } + + ref + .read(selectedBangTriggerProvider(domain: domain).notifier) + .setTrigger(trigger); + } + } +}