From 629c6da667d8d3dabeec7d293ee3c1ac70b7656b Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Mon, 19 Jan 2026 12:31:26 +0100 Subject: [PATCH] tab edit initial --- app/lib/core/routing/routes.browser.dart | 7 + app/lib/core/routing/routes.g.dart | 2 + .../browser/domain/entities/sheet.dart | 4 +- .../browser/presentation/screens/browser.dart | 2 +- .../browser_modules/app_bar_title.dart | 134 +++++---- .../browser_modules/bottom_app_bar.dart | 15 +- .../presentation/widgets/sheets/view_tab.dart | 111 ++----- .../search/presentation/screens/search.dart | 274 ++++++++++++------ 8 files changed, 306 insertions(+), 243 deletions(-) diff --git a/app/lib/core/routing/routes.browser.dart b/app/lib/core/routing/routes.browser.dart index b06e42f5..493f53d7 100644 --- a/app/lib/core/routing/routes.browser.dart +++ b/app/lib/core/routing/routes.browser.dart @@ -136,10 +136,16 @@ class SearchRoute extends GoRouteData with $SearchRoute { final bool launchedFromIntent; + /// When provided, the search screen will load URLs into this existing tab + /// instead of creating a new tab. This also changes the UI to show + /// site-specific bangs instead of the tab type selector. + final String? tabId; + const SearchRoute({ required this.tabType, this.searchText = SearchRoute.emptySearchText, this.launchedFromIntent = false, + this.tabId, }); @override @@ -150,6 +156,7 @@ class SearchRoute extends GoRouteData with $SearchRoute { ? null : searchText, launchedFromIntent: launchedFromIntent, + tabId: tabId, ); } } diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index dc4e7dcf..a0627f96 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -637,6 +637,7 @@ mixin $SearchRoute on GoRouteData { _$boolConverter, ) ?? false, + tabId: state.uri.queryParameters['tab-id'], ); SearchRoute get _self => this as SearchRoute; @@ -647,6 +648,7 @@ mixin $SearchRoute on GoRouteData { queryParams: { if (_self.launchedFromIntent != false) 'launched-from-intent': _self.launchedFromIntent.toString(), + if (_self.tabId != null) 'tab-id': _self.tabId, }, ); diff --git a/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart b/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart index 546ab8f0..96af92ed 100644 --- a/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart +++ b/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart @@ -27,10 +27,10 @@ final class ViewTabsSheet extends Sheet { List get hashParameters => [null]; } -final class EditUrlSheet extends Sheet { +final class SiteSettingsSheet extends Sheet { final TabState tabState; - EditUrlSheet({required this.tabState}); + SiteSettingsSheet({required this.tabState}); @override List get hashParameters => [tabState]; diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 464d7d1d..2e3f1898 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -591,7 +591,7 @@ class _SheetContainer extends HookConsumerWidget { onNotification: dismissOnThreshold, child: _ViewTabsSheet(maxChildSize: stableMaxChildSize), ), - final EditUrlSheet parameter => + final SiteSettingsSheet parameter => NotificationListener( onNotification: dismissOnThreshold, child: _ViewUrlSheet( diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart index 177ef760..22d876c7 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart @@ -24,7 +24,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:text_scroll/text_scroll.dart'; import 'package:weblibre/core/design/app_colors.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; @@ -67,66 +70,91 @@ class AppBarTitle extends HookConsumerWidget { return Row( children: [ - TabIcon(tabState: tabState), + // Icon tap → opens site settings sheet + GestureDetector( + onTap: () { + ref + .read(bottomSheetControllerProvider.notifier) + .show(SiteSettingsSheet(tabState: tabState)); + }, + child: TabIcon(tabState: tabState), + ), const SizedBox(width: 8), + // Title/URL tap → opens search screen Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Skeletonizer( - enabled: tabState.title.isEmpty, - child: Skeleton.replace( - replacement: const Padding( - padding: EdgeInsets.only(right: 4, top: 1, bottom: 1), - child: Bone.text(), - ), - child: TextScroll( - key: ValueKey(tabState.title), - tabState.title, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onSurface, + child: GestureDetector( + onTap: () async { + // Don't pre-fill for internal URLs + final searchText = tabState.url.scheme == 'about' + ? '' + : tabState.url.toString(); + + await SearchRoute( + tabId: tabState.id, + searchText: searchText.isEmpty + ? SearchRoute.emptySearchText + : searchText, + tabType: tabState.isPrivate ? TabType.private : TabType.regular, + ).push(context); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Skeletonizer( + enabled: tabState.title.isEmpty, + child: Skeleton.replace( + replacement: const Padding( + padding: EdgeInsets.only(right: 4, top: 1, bottom: 1), + child: Bone.text(), ), - // mode: TextScrollMode.bouncing, - velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), - delayBefore: const Duration(milliseconds: 500), - pauseBetween: const Duration(milliseconds: 5000), - fadedBorder: true, - fadeBorderSide: FadeBorderSide.right, - fadedBorderWidth: 0.05, - intervalSpaces: 4, - numberOfReps: 2, - ), - ), - ), - Row( - children: [ - if (isTabTuneledAsync.hasValue && - isTabTuneledAsync.value == true) ...[ - const Icon(MdiIcons.tunnelOutline, size: 14), - const SizedBox(width: 4), - ], - icon, - const SizedBox(width: 4), - if (tabState.isPrivate) ...[ - Icon( - MdiIcons.dominoMask, - color: appColors.privateTabPurple, - size: 14, - ), - const SizedBox(width: 4), - ], - Expanded( - child: UriBreadcrumb( - uri: tabState.url, - style: theme.textTheme.bodyMedium?.copyWith( + child: TextScroll( + key: ValueKey(tabState.title), + tabState.title, + style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, ), + // mode: TextScrollMode.bouncing, + velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), + delayBefore: const Duration(milliseconds: 500), + pauseBetween: const Duration(milliseconds: 5000), + fadedBorder: true, + fadeBorderSide: FadeBorderSide.right, + fadedBorderWidth: 0.05, + intervalSpaces: 4, + numberOfReps: 2, ), ), - ], - ), - ], + ), + Row( + children: [ + if (isTabTuneledAsync.hasValue && + isTabTuneledAsync.value == true) ...[ + const Icon(MdiIcons.tunnelOutline, size: 14), + const SizedBox(width: 4), + ], + icon, + const SizedBox(width: 4), + if (tabState.isPrivate) ...[ + Icon( + MdiIcons.dominoMask, + color: appColors.privateTabPurple, + size: 14, + ), + const SizedBox(width: 4), + ], + Expanded( + child: UriBreadcrumb( + uri: tabState.url, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ), ), ), ], diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index b57e0542..8653c6e8 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -218,20 +218,7 @@ class BrowserTabBar extends HookConsumerWidget { final toolbarHeight = useMemoized(() => getToolbarHeight()); return GestureDetector( - onTap: () { - if (displayedSheet case EditUrlSheet()) { - ref.read(bottomSheetControllerProvider.notifier).requestDismiss(); - return; - } - - final tabState = ref.read(selectedTabStateProvider); - - if (tabState != null) { - ref - .read(bottomSheetControllerProvider.notifier) - .show(EditUrlSheet(tabState: tabState)); - } - }, + // Tap handling moved to AppBarTitle for split icon/title behavior onHorizontalDragStart: (details) { dragStartPosition.value = details.globalPosition; }, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart index b86347bb..321b011a 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart @@ -22,8 +22,7 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; -import 'package:weblibre/features/bangs/presentation/widgets/site_search.dart'; +import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; @@ -64,64 +63,6 @@ class ViewTabSheetWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final headerKey = useMemoized(() => GlobalKey()); - final textFieldKey = useMemoized(() => GlobalKey()); - - final scrolledTo = useRef(0.0); - - final searchTextController = useTextEditingController( - text: initialTabState.url.toString(), - ); - - Future scroll() async { - if (!context.mounted) return; - - final header = headerKey.currentContext?.findRenderObject(); - final text = textFieldKey.currentContext?.findRenderObject(); - - if (header case final RenderBox headerBox) { - if (text case final RenderBox textBox) { - final totalHeight = - headerBox.size.height + - textBox.size.height + - bottomAppBarHeight + - MediaQuery.of(context).viewInsets.bottom; - - final relative = (totalHeight / MediaQuery.of(context).size.height) - .clamp(0.0, 1.0); - - if (draggableScrollableController.size < relative && - relative > scrolledTo.value) { - await draggableScrollableController.animateTo( - relative, - duration: const Duration(milliseconds: 150), - curve: Curves.easeInOut, - ); - scrolledTo.value = relative; - } - } - } - } - - useOnListenableChange(searchTextController, () { - WidgetsBinding.instance.addPostFrameCallback((_) async { - await scroll(); - }); - }); - - final availableBangs = ref.watch( - bangListProvider( - domain: initialTabState.url.host, - orderMostFrequentFirst: true, - ).select((value) => value.value ?? const []), - ); - - useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) async { - await scroll(); - }); - - return null; - }); final bottomInsets = useRef(0.0); useEffect( @@ -164,21 +105,23 @@ class ViewTabSheetWidget extends HookConsumerWidget { padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0), child: GestureDetector( onTap: () async { - if (draggableScrollableController.size > 0.85) { - await draggableScrollableController.animateTo( - (scrolledTo.value > 0.0) - ? scrolledTo.value - : initialHeight, - duration: const Duration(milliseconds: 150), - curve: Curves.easeInOut, - ); - } else { - await draggableScrollableController.animateTo( - 1.0, - duration: const Duration(milliseconds: 150), - curve: Curves.easeInOut, - ); - } + // Dismiss sheet and open search screen with tab context + onClose(); + + // Don't pre-fill for internal URLs + final searchText = initialTabState.url.scheme == 'about' + ? '' + : initialTabState.url.toString(); + + await SearchRoute( + tabId: initialTabState.id, + searchText: searchText.isEmpty + ? SearchRoute.emptySearchText + : searchText, + tabType: initialTabState.isPrivate + ? TabType.private + : TabType.regular, + ).push(context); }, child: WebsiteTitleTile(initialTabState), ), @@ -199,20 +142,8 @@ class ViewTabSheetWidget extends HookConsumerWidget { controller: controller, physics: const ClampingScrollPhysicsWithoutImplicit(), children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SiteSearch( - key: textFieldKey, - domain: initialTabState.url.host, - availableBangs: availableBangs, - controller: searchTextController, - ), - ), - const Divider(), // Tracking Protection Section - TrackingProtectionSection( - tabId: initialTabState.id, - ), + TrackingProtectionSection(tabId: initialTabState.id), const Divider(), // Permissions Section PermissionsSection( @@ -221,9 +152,7 @@ class ViewTabSheetWidget extends HookConsumerWidget { ), const Divider(), // Clear Site Data Section - ClearSiteDataSection( - url: initialTabState.url, - ), + ClearSiteDataSection(url: initialTabState.url), const SizedBox(height: 16.0), ], ); 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 6a934608..02743e6d 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -29,6 +29,7 @@ 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'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; @@ -40,6 +41,8 @@ import 'package:weblibre/features/geckoview/features/search/presentation/widgets import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.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/uri_parser.dart' as uri_parser; class SearchScreen extends HookConsumerWidget { @@ -47,10 +50,15 @@ class SearchScreen extends HookConsumerWidget { final TabType tabType; final bool launchedFromIntent; + /// When provided, URLs will be loaded into this existing tab. + /// When null, a new tab will be created. + final String? tabId; + const SearchScreen({ required this.initialSearchText, required this.tabType, this.launchedFromIntent = false, + this.tabId, }); @override @@ -67,11 +75,23 @@ class SearchScreen extends HookConsumerWidget { final selectedTabType = useState(tabType); final currentTabTabType = ref.watch(selectedTabTypeProvider); - final privateTabMode = switch (selectedTabType.value) { - TabType.regular => false, - TabType.private => true, - TabType.child => currentTabTabType == TabType.private, - }; + // When editing an existing tab, get its state + // If tab no longer exists (null), fall back to new tab mode + final existingTabState = tabId != null + ? ref.watch(tabStateProvider(tabId)) + : null; + + // Determine if we're in edit mode (tabId provided AND tab still exists) + final isEditMode = tabId != null && existingTabState != null; + + // Derive private mode from existing tab or from selector + final privateTabMode = isEditMode + ? existingTabState.isPrivate + : switch (selectedTabType.value) { + TabType.regular => false, + TabType.private => true, + TabType.child => currentTabTabType == TabType.private, + }; final searchTextController = useTextEditingController( text: initialSearchText, @@ -105,13 +125,16 @@ class SearchScreen extends HookConsumerWidget { final activeBang = selectedBang.value ?? defaultSearchBang; final showBangIcon = useState(false); - ref.listen(selectedBangDataProvider(), (previous, next) { - if (previous != next) { - showBangIcon.value = true; - } + ref.listen( + selectedBangDataProvider(domain: isEditMode ? existingTabState.url.host : null), + (previous, next) { + if (previous != next) { + showBangIcon.value = true; + } - selectedBang.value = next; - }); + selectedBang.value = next; + }, + ); Future submitSearch(String query) async { if (activeBang != null && (formKey.currentState?.validate() == true)) { @@ -123,17 +146,25 @@ class SearchScreen extends HookConsumerWidget { .triggerBangSearch(activeBang, query); } - await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: searchUri, - private: privateTabMode, - parentId: (selectedTabType.value == TabType.child) - ? ref.read(selectedTabProvider) - : null, - launchedFromIntent: launchedFromIntent, - selectTab: true, - ); + if (isEditMode) { + // Load into existing tab + await ref + .read(tabSessionProvider(tabId: tabId).notifier) + .loadUrl(url: searchUri); + } else { + // Create new tab + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: searchUri, + private: privateTabMode, + parentId: (selectedTabType.value == TabType.child) + ? ref.read(selectedTabProvider) + : null, + launchedFromIntent: launchedFromIntent, + selectTab: true, + ); + } if (context.mounted) { ref.read(bottomSheetControllerProvider.notifier).requestDismiss(); @@ -156,54 +187,70 @@ class SearchScreen extends HookConsumerWidget { floating: true, pinned: true, automaticallyImplyLeading: false, - title: Align( - child: Focus( - canRequestFocus: false, - child: SegmentedButton( - showSelectedIcon: false, - segments: [ - const ButtonSegment( - value: TabType.regular, - label: Text('Regular'), - icon: Icon(MdiIcons.tab), - ), - const ButtonSegment( - value: TabType.private, - label: Text('Private'), - icon: Icon(MdiIcons.tabUnselected), - ), - if (createChildTabsOption) - const ButtonSegment( - value: TabType.child, - label: Text('Child'), - icon: Icon(MdiIcons.fileTree), + title: isEditMode + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + _SiteBangsSelector( + tabId: tabId!, + domain: existingTabState.url.host, + ), + Text( + 'Editing current tab', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ) + : Align( + child: Focus( + canRequestFocus: false, + child: SegmentedButton( + showSelectedIcon: false, + segments: [ + const ButtonSegment( + value: TabType.regular, + label: Text('Regular'), + icon: Icon(MdiIcons.tab), + ), + const ButtonSegment( + value: TabType.private, + label: Text('Private'), + icon: Icon(MdiIcons.tabUnselected), + ), + if (createChildTabsOption) + const ButtonSegment( + value: TabType.child, + label: Text('Child'), + icon: Icon(MdiIcons.fileTree), + ), + ], + selected: {selectedTabType.value}, + onSelectionChanged: (value) { + selectedTabType.value = value.first; + // Restore focus to search field after segment change + WidgetsBinding.instance.addPostFrameCallback((_) { + searchFocusNode.requestFocus(); + }); + }, + style: switch (selectedTabType.value) { + TabType.regular => null, + TabType.private => SegmentedButton.styleFrom( + selectedBackgroundColor: + appColors.privateSelectionOverlay, + ), + TabType.child => + (currentTabTabType == TabType.private) + ? SegmentedButton.styleFrom( + selectedBackgroundColor: + appColors.privateSelectionOverlay, + ) + : null, + }, ), - ], - selected: {selectedTabType.value}, - onSelectionChanged: (value) { - selectedTabType.value = value.first; - // Restore focus to search field after segment change - WidgetsBinding.instance.addPostFrameCallback((_) { - searchFocusNode.requestFocus(); - }); - }, - style: switch (selectedTabType.value) { - TabType.regular => null, - TabType.private => SegmentedButton.styleFrom( - selectedBackgroundColor: - appColors.privateSelectionOverlay, ), - TabType.child => - (currentTabTabType == TabType.private) - ? SegmentedButton.styleFrom( - selectedBackgroundColor: - appColors.privateSelectionOverlay, - ) - : null, - }, - ), - ), - ), + ), bottom: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), child: Padding( @@ -223,7 +270,9 @@ class SearchScreen extends HookConsumerWidget { if (newUrl == null) { final bang = - ref.read(selectedBangDataProvider()) ?? + ref.read(selectedBangDataProvider( + domain: isEditMode ? existingTabState.url.host : null, + )) ?? await ref.read( defaultSearchBangDataProvider.future, ); @@ -240,19 +289,27 @@ class SearchScreen extends HookConsumerWidget { } if (newUrl != null) { - await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: newUrl, - private: privateTabMode, - parentId: - (selectedTabType.value == - TabType.child) - ? ref.read(selectedTabProvider) - : null, - launchedFromIntent: launchedFromIntent, - selectTab: true, - ); + if (isEditMode) { + // Load into existing tab + await ref + .read(tabSessionProvider(tabId: tabId).notifier) + .loadUrl(url: newUrl); + } else { + // Create new tab + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: newUrl, + private: privateTabMode, + parentId: + (selectedTabType.value == + TabType.child) + ? ref.read(selectedTabProvider) + : null, + launchedFromIntent: launchedFromIntent, + selectTab: true, + ); + } if (context.mounted) { ref @@ -296,3 +353,56 @@ class SearchScreen extends HookConsumerWidget { ); } } + +class _SiteBangsSelector extends HookConsumerWidget { + final String tabId; + final String domain; + + const _SiteBangsSelector({ + required this.tabId, + required this.domain, + }); + + @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) { + 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(); + } + }, + ), + ); + } +}