tab edit initial

This commit is contained in:
Fabian Freund
2026-01-19 12:31:26 +01:00
parent 1dc609f4e4
commit 629c6da667
8 changed files with 306 additions and 243 deletions
+7
View File
@@ -136,10 +136,16 @@ class SearchRoute extends GoRouteData with $SearchRoute {
final bool launchedFromIntent; 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({ const SearchRoute({
required this.tabType, required this.tabType,
this.searchText = SearchRoute.emptySearchText, this.searchText = SearchRoute.emptySearchText,
this.launchedFromIntent = false, this.launchedFromIntent = false,
this.tabId,
}); });
@override @override
@@ -150,6 +156,7 @@ class SearchRoute extends GoRouteData with $SearchRoute {
? null ? null
: searchText, : searchText,
launchedFromIntent: launchedFromIntent, launchedFromIntent: launchedFromIntent,
tabId: tabId,
); );
} }
} }
+2
View File
@@ -637,6 +637,7 @@ mixin $SearchRoute on GoRouteData {
_$boolConverter, _$boolConverter,
) ?? ) ??
false, false,
tabId: state.uri.queryParameters['tab-id'],
); );
SearchRoute get _self => this as SearchRoute; SearchRoute get _self => this as SearchRoute;
@@ -647,6 +648,7 @@ mixin $SearchRoute on GoRouteData {
queryParams: { queryParams: {
if (_self.launchedFromIntent != false) if (_self.launchedFromIntent != false)
'launched-from-intent': _self.launchedFromIntent.toString(), 'launched-from-intent': _self.launchedFromIntent.toString(),
if (_self.tabId != null) 'tab-id': _self.tabId,
}, },
); );
@@ -27,10 +27,10 @@ final class ViewTabsSheet extends Sheet {
List<Object?> get hashParameters => [null]; List<Object?> get hashParameters => [null];
} }
final class EditUrlSheet extends Sheet { final class SiteSettingsSheet extends Sheet {
final TabState tabState; final TabState tabState;
EditUrlSheet({required this.tabState}); SiteSettingsSheet({required this.tabState});
@override @override
List<Object?> get hashParameters => [tabState]; List<Object?> get hashParameters => [tabState];
@@ -591,7 +591,7 @@ class _SheetContainer extends HookConsumerWidget {
onNotification: dismissOnThreshold, onNotification: dismissOnThreshold,
child: _ViewTabsSheet(maxChildSize: stableMaxChildSize), child: _ViewTabsSheet(maxChildSize: stableMaxChildSize),
), ),
final EditUrlSheet parameter => final SiteSettingsSheet parameter =>
NotificationListener<DraggableScrollableNotification>( NotificationListener<DraggableScrollableNotification>(
onNotification: dismissOnThreshold, onNotification: dismissOnThreshold,
child: _ViewUrlSheet( child: _ViewUrlSheet(
@@ -24,7 +24,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:text_scroll/text_scroll.dart'; import 'package:text_scroll/text_scroll.dart';
import 'package:weblibre/core/design/app_colors.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/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/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
@@ -67,66 +70,91 @@ class AppBarTitle extends HookConsumerWidget {
return Row( return Row(
children: [ 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), const SizedBox(width: 8),
// Title/URL tap → opens search screen
Expanded( Expanded(
child: Column( child: GestureDetector(
crossAxisAlignment: CrossAxisAlignment.start, onTap: () async {
mainAxisSize: MainAxisSize.min, // Don't pre-fill for internal URLs
children: [ final searchText = tabState.url.scheme == 'about'
Skeletonizer( ? ''
enabled: tabState.title.isEmpty, : tabState.url.toString();
child: Skeleton.replace(
replacement: const Padding( await SearchRoute(
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1), tabId: tabState.id,
child: Bone.text(), searchText: searchText.isEmpty
), ? SearchRoute.emptySearchText
child: TextScroll( : searchText,
key: ValueKey(tabState.title), tabType: tabState.isPrivate ? TabType.private : TabType.regular,
tabState.title, ).push(context);
style: theme.textTheme.bodyLarge?.copyWith( },
color: theme.colorScheme.onSurface, 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, child: TextScroll(
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), key: ValueKey(tabState.title),
delayBefore: const Duration(milliseconds: 500), tabState.title,
pauseBetween: const Duration(milliseconds: 5000), style: theme.textTheme.bodyLarge?.copyWith(
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, 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,
),
),
),
],
),
],
),
), ),
), ),
], ],
@@ -218,20 +218,7 @@ class BrowserTabBar extends HookConsumerWidget {
final toolbarHeight = useMemoized(() => getToolbarHeight()); final toolbarHeight = useMemoized(() => getToolbarHeight());
return GestureDetector( return GestureDetector(
onTap: () { // Tap handling moved to AppBarTitle for split icon/title behavior
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));
}
},
onHorizontalDragStart: (details) { onHorizontalDragStart: (details) {
dragStartPosition.value = details.globalPosition; dragStartPosition.value = details.globalPosition;
}, },
@@ -22,8 +22,7 @@ import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/bangs/presentation/widgets/site_search.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/tab.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/certificate_tile.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
@@ -64,64 +63,6 @@ class ViewTabSheetWidget extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final headerKey = useMemoized(() => GlobalKey()); final headerKey = useMemoized(() => GlobalKey());
final textFieldKey = useMemoized(() => GlobalKey());
final scrolledTo = useRef(0.0);
final searchTextController = useTextEditingController(
text: initialTabState.url.toString(),
);
Future<void> 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); final bottomInsets = useRef(0.0);
useEffect( useEffect(
@@ -164,21 +105,23 @@ class ViewTabSheetWidget extends HookConsumerWidget {
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0), padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
if (draggableScrollableController.size > 0.85) { // Dismiss sheet and open search screen with tab context
await draggableScrollableController.animateTo( onClose();
(scrolledTo.value > 0.0)
? scrolledTo.value // Don't pre-fill for internal URLs
: initialHeight, final searchText = initialTabState.url.scheme == 'about'
duration: const Duration(milliseconds: 150), ? ''
curve: Curves.easeInOut, : initialTabState.url.toString();
);
} else { await SearchRoute(
await draggableScrollableController.animateTo( tabId: initialTabState.id,
1.0, searchText: searchText.isEmpty
duration: const Duration(milliseconds: 150), ? SearchRoute.emptySearchText
curve: Curves.easeInOut, : searchText,
); tabType: initialTabState.isPrivate
} ? TabType.private
: TabType.regular,
).push(context);
}, },
child: WebsiteTitleTile(initialTabState), child: WebsiteTitleTile(initialTabState),
), ),
@@ -199,20 +142,8 @@ class ViewTabSheetWidget extends HookConsumerWidget {
controller: controller, controller: controller,
physics: const ClampingScrollPhysicsWithoutImplicit(), physics: const ClampingScrollPhysicsWithoutImplicit(),
children: [ 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 // Tracking Protection Section
TrackingProtectionSection( TrackingProtectionSection(tabId: initialTabState.id),
tabId: initialTabState.id,
),
const Divider(), const Divider(),
// Permissions Section // Permissions Section
PermissionsSection( PermissionsSection(
@@ -221,9 +152,7 @@ class ViewTabSheetWidget extends HookConsumerWidget {
), ),
const Divider(), const Divider(),
// Clear Site Data Section // Clear Site Data Section
ClearSiteDataSection( ClearSiteDataSection(url: initialTabState.url),
url: initialTabState.url,
),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
], ],
); );
@@ -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/bangs/domain/providers/search.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.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/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/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/providers.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/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/sampled_value_notifier.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; import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
class SearchScreen extends HookConsumerWidget { class SearchScreen extends HookConsumerWidget {
@@ -47,10 +50,15 @@ class SearchScreen extends HookConsumerWidget {
final TabType tabType; final TabType tabType;
final bool launchedFromIntent; 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({ const SearchScreen({
required this.initialSearchText, required this.initialSearchText,
required this.tabType, required this.tabType,
this.launchedFromIntent = false, this.launchedFromIntent = false,
this.tabId,
}); });
@override @override
@@ -67,11 +75,23 @@ class SearchScreen extends HookConsumerWidget {
final selectedTabType = useState(tabType); final selectedTabType = useState(tabType);
final currentTabTabType = ref.watch(selectedTabTypeProvider); final currentTabTabType = ref.watch(selectedTabTypeProvider);
final privateTabMode = switch (selectedTabType.value) { // When editing an existing tab, get its state
TabType.regular => false, // If tab no longer exists (null), fall back to new tab mode
TabType.private => true, final existingTabState = tabId != null
TabType.child => currentTabTabType == TabType.private, ? 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( final searchTextController = useTextEditingController(
text: initialSearchText, text: initialSearchText,
@@ -105,13 +125,16 @@ class SearchScreen extends HookConsumerWidget {
final activeBang = selectedBang.value ?? defaultSearchBang; final activeBang = selectedBang.value ?? defaultSearchBang;
final showBangIcon = useState(false); final showBangIcon = useState(false);
ref.listen(selectedBangDataProvider(), (previous, next) { ref.listen(
if (previous != next) { selectedBangDataProvider(domain: isEditMode ? existingTabState.url.host : null),
showBangIcon.value = true; (previous, next) {
} if (previous != next) {
showBangIcon.value = true;
}
selectedBang.value = next; selectedBang.value = next;
}); },
);
Future<void> submitSearch(String query) async { Future<void> submitSearch(String query) async {
if (activeBang != null && (formKey.currentState?.validate() == true)) { if (activeBang != null && (formKey.currentState?.validate() == true)) {
@@ -123,17 +146,25 @@ class SearchScreen extends HookConsumerWidget {
.triggerBangSearch(activeBang, query); .triggerBangSearch(activeBang, query);
} }
await ref if (isEditMode) {
.read(tabRepositoryProvider.notifier) // Load into existing tab
.addTab( await ref
url: searchUri, .read(tabSessionProvider(tabId: tabId).notifier)
private: privateTabMode, .loadUrl(url: searchUri);
parentId: (selectedTabType.value == TabType.child) } else {
? ref.read(selectedTabProvider) // Create new tab
: null, await ref
launchedFromIntent: launchedFromIntent, .read(tabRepositoryProvider.notifier)
selectTab: true, .addTab(
); url: searchUri,
private: privateTabMode,
parentId: (selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
selectTab: true,
);
}
if (context.mounted) { if (context.mounted) {
ref.read(bottomSheetControllerProvider.notifier).requestDismiss(); ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
@@ -156,54 +187,70 @@ class SearchScreen extends HookConsumerWidget {
floating: true, floating: true,
pinned: true, pinned: true,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: Align( title: isEditMode
child: Focus( ? Column(
canRequestFocus: false, mainAxisSize: MainAxisSize.min,
child: SegmentedButton( children: [
showSelectedIcon: false, _SiteBangsSelector(
segments: [ tabId: tabId!,
const ButtonSegment( domain: existingTabState.url.host,
value: TabType.regular, ),
label: Text('Regular'), Text(
icon: Icon(MdiIcons.tab), 'Editing current tab',
), style: Theme.of(context).textTheme.bodySmall?.copyWith(
const ButtonSegment( color: Theme.of(context).colorScheme.onSurfaceVariant,
value: TabType.private, ),
label: Text('Private'), ),
icon: Icon(MdiIcons.tabUnselected), ],
), )
if (createChildTabsOption) : Align(
const ButtonSegment( child: Focus(
value: TabType.child, canRequestFocus: false,
label: Text('Child'), child: SegmentedButton(
icon: Icon(MdiIcons.fileTree), 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( bottom: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),
child: Padding( child: Padding(
@@ -223,7 +270,9 @@ class SearchScreen extends HookConsumerWidget {
if (newUrl == null) { if (newUrl == null) {
final bang = final bang =
ref.read(selectedBangDataProvider()) ?? ref.read(selectedBangDataProvider(
domain: isEditMode ? existingTabState.url.host : null,
)) ??
await ref.read( await ref.read(
defaultSearchBangDataProvider.future, defaultSearchBangDataProvider.future,
); );
@@ -240,19 +289,27 @@ class SearchScreen extends HookConsumerWidget {
} }
if (newUrl != null) { if (newUrl != null) {
await ref if (isEditMode) {
.read(tabRepositoryProvider.notifier) // Load into existing tab
.addTab( await ref
url: newUrl, .read(tabSessionProvider(tabId: tabId).notifier)
private: privateTabMode, .loadUrl(url: newUrl);
parentId: } else {
(selectedTabType.value == // Create new tab
TabType.child) await ref
? ref.read(selectedTabProvider) .read(tabRepositoryProvider.notifier)
: null, .addTab(
launchedFromIntent: launchedFromIntent, url: newUrl,
selectTab: true, private: privateTabMode,
); parentId:
(selectedTabType.value ==
TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
selectTab: true,
);
}
if (context.mounted) { if (context.mounted) {
ref 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();
}
},
),
);
}
}