diff --git a/app/lib/features/bangs/presentation/widgets/site_search.dart b/app/lib/features/bangs/presentation/widgets/site_search.dart index 3d820664..c11b994e 100644 --- a/app/lib/features/bangs/presentation/widgets/site_search.dart +++ b/app/lib/features/bangs/presentation/widgets/site_search.dart @@ -45,7 +45,6 @@ class SiteSearch extends HookConsumerWidget { final List availableBangs; final TextEditingController? controller; final bool searchInNewTab; - final Widget? label; const SiteSearch({ required this.domain, @@ -53,7 +52,6 @@ class SiteSearch extends HookConsumerWidget { super.key, this.controller, this.searchInNewTab = false, - this.label, }); @override @@ -119,6 +117,7 @@ class SiteSearch extends HookConsumerWidget { return Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Visibility( visible: availableBangs.isNotEmpty, @@ -169,7 +168,9 @@ class SiteSearch extends HookConsumerWidget { maxLines: null, activeBang: selectedBang, showSuggestions: true, - label: label, + label: (selectedBang != null) + ? const Text('Search') + : const Text('Address / Search'), onTap: () { if (!searchFocusNode.hasFocus) { // Select all text when the field is tapped @@ -188,6 +189,7 @@ class SiteSearch extends HookConsumerWidget { searchTextController: searchTextController, activeBang: selectedBang, submitSearch: submitSearch, + limit: 25, ), ], ); diff --git a/app/lib/features/geckoview/features/browser/domain/providers.dart b/app/lib/features/geckoview/features/browser/domain/providers.dart index bb1cc726..5b254c50 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.dart @@ -289,7 +289,7 @@ EquatableValue> seamlessFilteredTabEntities( } @Riverpod() -EquatableValue> seamlessFilteredTabPreviews( +EquatableValue> filteredTabPreviews( Ref ref, TabSearchPartition searchPartition, ContainerFilter containerFilter, @@ -307,21 +307,7 @@ EquatableValue> seamlessFilteredTabPreviews( ); if (tabSearchResults == null) { - return EquatableValue( - availableTabStates.value.values - .map( - (state) => TabPreview( - id: state.id, - title: state.title, - icon: state.icon, - url: state.url, - highlightedUrl: null, - content: null, - sourceSearchQuery: null, - ), - ) - .toList(), - ); + return EquatableValue([]); } return EquatableValue( @@ -330,6 +316,7 @@ EquatableValue> seamlessFilteredTabPreviews( .map((tab) { return TabPreview( id: tab.id, + containerId: tab.containerId, title: tab.title ?? availableTabStates.value[tab.id]!.title, icon: null, url: tab.cleanUrl ?? availableTabStates.value[tab.id]!.url, diff --git a/app/lib/features/geckoview/features/browser/domain/providers.g.dart b/app/lib/features/geckoview/features/browser/domain/providers.g.dart index 1778cbfd..ebba51df 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.g.dart @@ -640,11 +640,10 @@ final class SeamlessFilteredTabEntitiesFamily extends $Family String toString() => r'seamlessFilteredTabEntitiesProvider'; } -@ProviderFor(seamlessFilteredTabPreviews) -const seamlessFilteredTabPreviewsProvider = - SeamlessFilteredTabPreviewsFamily._(); +@ProviderFor(filteredTabPreviews) +const filteredTabPreviewsProvider = FilteredTabPreviewsFamily._(); -final class SeamlessFilteredTabPreviewsProvider +final class FilteredTabPreviewsProvider extends $FunctionalProvider< EquatableValue>, @@ -652,23 +651,23 @@ final class SeamlessFilteredTabPreviewsProvider EquatableValue> > with $Provider>> { - const SeamlessFilteredTabPreviewsProvider._({ - required SeamlessFilteredTabPreviewsFamily super.from, + const FilteredTabPreviewsProvider._({ + required FilteredTabPreviewsFamily super.from, required (TabSearchPartition, ContainerFilter) super.argument, }) : super( retry: null, - name: r'seamlessFilteredTabPreviewsProvider', + name: r'filteredTabPreviewsProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override - String debugGetCreateSourceHash() => _$seamlessFilteredTabPreviewsHash(); + String debugGetCreateSourceHash() => _$filteredTabPreviewsHash(); @override String toString() { - return r'seamlessFilteredTabPreviewsProvider' + return r'filteredTabPreviewsProvider' '' '$argument'; } @@ -682,7 +681,7 @@ final class SeamlessFilteredTabPreviewsProvider @override EquatableValue> create(Ref ref) { final argument = this.argument as (TabSearchPartition, ContainerFilter); - return seamlessFilteredTabPreviews(ref, argument.$1, argument.$2); + return filteredTabPreviews(ref, argument.$1, argument.$2); } /// {@macro riverpod.override_with_value} @@ -697,8 +696,7 @@ final class SeamlessFilteredTabPreviewsProvider @override bool operator ==(Object other) { - return other is SeamlessFilteredTabPreviewsProvider && - other.argument == argument; + return other is FilteredTabPreviewsProvider && other.argument == argument; } @override @@ -707,32 +705,32 @@ final class SeamlessFilteredTabPreviewsProvider } } -String _$seamlessFilteredTabPreviewsHash() => - r'ee1d441ca5f5822e201de8fc0b264ea7b8db0776'; +String _$filteredTabPreviewsHash() => + r'f2958369b4270f4c8165526c0ffad22f41ec0491'; -final class SeamlessFilteredTabPreviewsFamily extends $Family +final class FilteredTabPreviewsFamily extends $Family with $FunctionalFamilyOverride< EquatableValue>, (TabSearchPartition, ContainerFilter) > { - const SeamlessFilteredTabPreviewsFamily._() + const FilteredTabPreviewsFamily._() : super( retry: null, - name: r'seamlessFilteredTabPreviewsProvider', + name: r'filteredTabPreviewsProvider', dependencies: null, $allTransitiveDependencies: null, isAutoDispose: true, ); - SeamlessFilteredTabPreviewsProvider call( + FilteredTabPreviewsProvider call( TabSearchPartition searchPartition, ContainerFilter containerFilter, - ) => SeamlessFilteredTabPreviewsProvider._( + ) => FilteredTabPreviewsProvider._( argument: (searchPartition, containerFilter), from: this, ); @override - String toString() => r'seamlessFilteredTabPreviewsProvider'; + String toString() => r'filteredTabPreviewsProvider'; } 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 dc39e079..f37aaa62 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 @@ -203,7 +203,6 @@ class ViewTabSheetWidget extends HookConsumerWidget { domain: initialTabState.url.host, availableBangs: availableBangs, controller: searchTextController, - label: const Text('Address'), ), ), const Divider(), diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart index 2026527e..8343a562 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart @@ -266,7 +266,7 @@ class ListTabPreview extends HookConsumerWidget { decoration: BoxDecoration( color: tabState.isPrivate ? const Color(0xFF25003E) : null, border: isActive ? Border.all(color: colorScheme.primary) : null, - // borderRadius: const BorderRadius.all(Radius.circular(16.0)), + borderRadius: const BorderRadius.all(Radius.circular(4.0)), ), child: Row( children: [ diff --git a/app/lib/features/geckoview/features/search/domain/entities/tab_preview.dart b/app/lib/features/geckoview/features/search/domain/entities/tab_preview.dart index 79ee3c49..745197b1 100644 --- a/app/lib/features/geckoview/features/search/domain/entities/tab_preview.dart +++ b/app/lib/features/geckoview/features/search/domain/entities/tab_preview.dart @@ -22,6 +22,7 @@ import 'package:weblibre/domain/entities/equatable_image.dart'; class TabPreview with FastEquatable { final String id; + final String? containerId; final String title; final EquatableImage? icon; @@ -35,6 +36,7 @@ class TabPreview with FastEquatable { TabPreview({ required this.id, + required this.containerId, required this.title, required this.icon, required this.url, @@ -46,6 +48,7 @@ class TabPreview with FastEquatable { @override List get hashParameters => [ id, + containerId, title, icon, url, 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 56b8fce4..96a400ea 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; @@ -125,142 +126,152 @@ class SearchScreen extends HookConsumerWidget { } return Scaffold( - body: Form( - key: formKey, - child: CustomScrollView( - slivers: [ - SliverAppBar( - 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), + body: SafeArea( + child: Form( + key: formKey, + child: FadingScroll( + builder: (context, controller) { + return CustomScrollView( + controller: controller, + slivers: [ + SliverAppBar( + 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), + ), + ], + 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: const Color(0x648000D7), + ), + TabType.child => + (currentTabTabType == TabType.private) + ? SegmentedButton.styleFrom( + selectedBackgroundColor: const Color( + 0x648000D7, + ), + ) + : 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: const Color(0x648000D7), ), - TabType.child => - (currentTabTabType == TabType.private) - ? SegmentedButton.styleFrom( - selectedBackgroundColor: const Color( - 0x648000D7, - ), - ) - : null, - }, - ), - ), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: SearchField( - showBangIcon: showBangIcon.value, - textEditingController: searchTextController, - focusNode: searchFocusNode, - autofocus: true, - label: const Text('Address / Search'), - onSubmitted: (value) async { - if (value.isNotEmpty) { - var newUrl = uri_parser.tryParseUrl( - value, - eagerParsing: true, - ); - - if (newUrl == null) { - final bang = - ref.read(selectedBangDataProvider()) ?? - await ref.read( - defaultSearchBangDataProvider.future, + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: SearchField( + showBangIcon: showBangIcon.value, + textEditingController: searchTextController, + focusNode: searchFocusNode, + autofocus: true, + label: const Text('Address / Search'), + onSubmitted: (value) async { + if (value.isNotEmpty) { + var newUrl = uri_parser.tryParseUrl( + value, + eagerParsing: true, ); - if (bang != null) { - newUrl = bang.getTemplateUrl(value); + if (newUrl == null) { + final bang = + ref.read(selectedBangDataProvider()) ?? + await ref.read( + defaultSearchBangDataProvider.future, + ); - if (!privateTabMode) { - await ref - .read(bangSearchProvider.notifier) - .triggerBangSearch(bang, value); + if (bang != null) { + newUrl = bang.getTemplateUrl(value); + + if (!privateTabMode) { + await ref + .read(bangSearchProvider.notifier) + .triggerBangSearch(bang, value); + } + } + } + + if (newUrl != null) { + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: newUrl, + private: privateTabMode, + parentId: + (selectedTabType.value == + TabType.child) + ? ref.read(selectedTabProvider) + : null, + launchedFromIntent: launchedFromIntent, + ); + + if (context.mounted) { + ref + .read( + bottomSheetControllerProvider.notifier, + ) + .requestDismiss(); + + const BrowserRoute().go(context); + } + } } - } - } - - if (newUrl != null) { - await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: newUrl, - private: privateTabMode, - parentId: - (selectedTabType.value == TabType.child) - ? ref.read(selectedTabProvider) - : null, - launchedFromIntent: launchedFromIntent, - ); - - if (context.mounted) { - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - - const BrowserRoute().go(context); - } - } - } - }, - activeBang: activeBang, - showSuggestions: true, + }, + activeBang: activeBang, + showSuggestions: true, + ), + ), + ), ), - ), - ), - ), - SliverToBoxAdapter( - child: ClipboardFillLink(controller: searchTextController), - ), - const SliverToBoxAdapter(child: Divider()), - FullSearchTermSuggestions( - searchTextController: searchTextController, - activeBang: activeBang, - submitSearch: submitSearch, - ), - TabSearch(searchTextListenable: sampledSearchText), - FeedSearch(searchTextNotifier: sampledSearchText), - HistorySuggestions( - isPrivate: privateTabMode, - searchTextListenable: sampledSearchText, - ), - ], + SliverToBoxAdapter( + child: ClipboardFillLink(controller: searchTextController), + ), + const SliverToBoxAdapter(child: Divider()), + FullSearchTermSuggestions( + searchTextController: searchTextController, + activeBang: activeBang, + submitSearch: submitSearch, + ), + TabSearch(searchTextListenable: sampledSearchText), + FeedSearch(searchTextNotifier: sampledSearchText), + HistorySuggestions( + isPrivate: privateTabMode, + searchTextListenable: sampledSearchText, + ), + ], + ); + }, + ), ), ), ); diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/fixed_search_suggestions.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/fixed_search_suggestions.dart index 5995ba68..d5c98b5e 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/fixed_search_suggestions.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/fixed_search_suggestions.dart @@ -32,13 +32,13 @@ class FixedSearchTermSuggestions extends HookConsumerWidget { final TextEditingController searchTextController; final Future Function(String query) submitSearch; final BangData? activeBang; - final int count; + final int limit; const FixedSearchTermSuggestions({ required this.searchTextController, required this.submitSearch, required this.activeBang, - this.count = 3, + required this.limit, super.key, }); @@ -76,27 +76,28 @@ class FixedSearchTermSuggestions extends HookConsumerWidget { .whereNot( (suggestion) => suggestion == searchTextController.text, ) - .take(count), + .take(limit), ] : []; - return Column( - mainAxisSize: MainAxisSize.min, - children: prioritizedSuggestions - .map( - (suggestion) => ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.search), - title: Text(suggestion), - onLongPress: () { - searchTextController.text = suggestion; - }, - onTap: () async { - await submitSearch(suggestion); - }, - ), - ) - .toList(), + return Wrap( + spacing: 8.0, + children: prioritizedSuggestions.map((query) { + return InkWell( + onLongPress: () { + searchTextController.text = query; + }, + child: InputChip( + // avatar: const Icon(Icons.search), + label: Text(query), + onSelected: (value) async { + if (value) { + await submitSearch(query); + } + }, + ), + ); + }).toList(), ); } } 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 f8b5e125..7afb8e80 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 @@ -61,35 +61,45 @@ class FullSearchTermSuggestions extends HookConsumerWidget { .addQuery(searchTextController.text); }); - final Widget listSliver; + final MultiSliver listSliver; if (!searchTextIsNotEmpty && (searchHistory.value.isNotEmpty)) { final entries = searchHistory.value!; - listSliver = SliverList.builder( - itemCount: entries.length, - itemBuilder: (context, index) { - final query = entries[index].searchQuery; + listSliver = MultiSliver( + children: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Wrap( + spacing: 8.0, + children: entries.map((entry) { + final query = entry.searchQuery; - return ListTile( - leading: const Icon(Icons.history), - title: Text(query), - onLongPress: () { - searchTextController.text = query; - }, - onTap: () async { - await submitSearch(query); - }, - trailing: IconButton( - onPressed: () async { - await ref - .read(bangDataRepositoryProvider.notifier) - .removeSearchEntry(query); - }, - icon: const Icon(Icons.close), + return InkWell( + onLongPress: () { + searchTextController.text = query; + }, + child: InputChip( + avatar: const Icon(Icons.history), + label: Text(query), + onSelected: (value) async { + if (value) { + await submitSearch(query); + } + }, + onDeleted: () async { + await ref + .read(bangDataRepositoryProvider.notifier) + .removeSearchEntry(query); + }, + ), + ); + }).toList(), + ), ), - ); - }, + ), + ], ); } else { final prioritizedSuggestions = [ @@ -100,22 +110,33 @@ class FullSearchTermSuggestions extends HookConsumerWidget { ), ]; - listSliver = SliverList.builder( - itemCount: prioritizedSuggestions.length, - itemBuilder: (context, index) { - final suggestion = prioritizedSuggestions[index]; - - return ListTile( - leading: const Icon(Icons.search), - title: Text(suggestion), - onLongPress: () { - searchTextController.text = suggestion; - }, - onTap: () async { - await submitSearch(suggestion); - }, - ); - }, + listSliver = MultiSliver( + children: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Wrap( + spacing: 8.0, + children: prioritizedSuggestions.map((query) { + return InkWell( + onLongPress: () { + searchTextController.text = query; + }, + child: InputChip( + // avatar: const Icon(Icons.search), + label: Text(query), + onSelected: (value) async { + if (value) { + await submitSearch(query); + } + }, + ), + ); + }).toList(), + ), + ), + ), + ], ); } @@ -167,17 +188,20 @@ class FullSearchTermSuggestions extends HookConsumerWidget { ), ), SliverToBoxAdapter( - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 150), - child: FadingScroll( - fadingSize: 25, - builder: (context, controller) { - return CustomScrollView( - shrinkWrap: true, - controller: controller, - slivers: [listSliver], - ); - }, + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 150), + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return CustomScrollView( + shrinkWrap: true, + controller: controller, + slivers: [listSliver], + ); + }, + ), ), ), ), diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart index aa561f91..fcfc796b 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart @@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/utils/image_helper.dart'; import 'package:weblibre/presentation/hooks/cached_future.dart'; import 'package:weblibre/presentation/hooks/listenable_callback.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; +import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; class HistorySuggestions extends HookConsumerWidget { final bool isPrivate; @@ -82,6 +83,7 @@ class HistorySuggestions extends HookConsumerWidget { itemCount: historySuggestions.length, itemBuilder: (context, index) { final suggestion = historySuggestions[index]; + final uri = suggestion.description.mapNotNull(Uri.tryParse); return HookBuilder( key: ValueKey(suggestion.id), @@ -107,30 +109,27 @@ class HistorySuggestions extends HookConsumerWidget { title: suggestion.title.mapNotNull( (title) => Text(title), ), - subtitle: suggestion.description.mapNotNull( - (description) => Text( - description, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), + subtitle: + uri.mapNotNull((uri) => UriBreadcrumb(uri: uri)) ?? + suggestion.description.mapNotNull( + (description) => Text( + description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), onTap: () async { - if (suggestion.description != null) { - if (Uri.tryParse(suggestion.description!) - case final Uri url) { - await ref - .read(tabRepositoryProvider.notifier) - .addTab(url: url, private: isPrivate); + if (uri != null) { + await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: uri, private: isPrivate); - if (context.mounted) { - ref - .read( - bottomSheetControllerProvider.notifier, - ) - .requestDismiss(); + if (context.mounted) { + ref + .read(bottomSheetControllerProvider.notifier) + .requestDismiss(); - const BrowserRoute().go(context); - } + const BrowserRoute().go(context); } } }, diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart index 9f7dab4b..7581f7ff 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart @@ -17,6 +17,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -33,6 +37,7 @@ import 'package:weblibre/features/geckoview/features/find_in_page/presentation/c import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart'; import 'package:weblibre/presentation/hooks/listenable_callback.dart'; @@ -53,15 +58,53 @@ class TabSearch extends HookConsumerWidget { ref.read(selectedContainerDataProvider.select((value) => value.value)), ); - final tabs = ref - .watch( - seamlessFilteredTabPreviewsProvider( - TabSearchPartition.search, - // ignore: provider_parameters - ContainerFilterById(containerId: selectedContainer.value?.id), - ), - ) - .value; + final tabSearchResults = ref.watch( + filteredTabPreviewsProvider( + TabSearchPartition.search, + // ignore: provider_parameters + ContainerFilterDisabled(), + ), + ); + + final containerIdsWithResults = useMemoized( + () => EquatableValue( + tabSearchResults.value + .map((result) => result.containerId) + .fold( + {}, + (map, id) => + map..update(id, (value) => ++value, ifAbsent: () => 1), + ), + ), + [tabSearchResults], + ); + + final filteredTabs = useMemoized( + () => tabSearchResults.value + .where((result) => result.containerId == selectedContainer.value?.id) + .toList(), + [tabSearchResults, selectedContainer.value?.id], + ); + + unawaited( + useValueChanged(containerIdsWithResults, (_, _) async { + if (containerIdsWithResults.value.isNotEmpty && + !containerIdsWithResults.value.containsKey( + selectedContainer.value?.id, + )) { + selectedContainer.value = await ref + .read(containerRepositoryProvider.notifier) + .getAllContainersWithCount() + .then((containers) { + return containers.firstWhereOrNull( + (container) => + !container.metadata.authSettings.authenticationRequired && + containerIdsWithResults.value.containsKey(container.id), + ); + }); + } + }), + ); useListenableCallback(searchTextListenable, () async { if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.search))) { @@ -79,6 +122,10 @@ class TabSearch extends HookConsumerWidget { } }); + if (tabSearchResults.value.isEmpty) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + return MultiSliver( children: [ const SliverToBoxAdapter(child: Divider()), @@ -92,6 +139,9 @@ class TabSearch extends HookConsumerWidget { ContainerChips( displayMenu: false, selectedContainer: selectedContainer.value, + showUnassignedChip: containerIdsWithResults.value.containsKey( + null, + ), onSelected: (container) async { if (container != null) { if (await ref @@ -106,7 +156,10 @@ class TabSearch extends HookConsumerWidget { onDeleted: (container) { selectedContainer.value = null; }, - containerFilter: (container) => (container.tabCount ?? 0) > 0, + containerFilter: (container) => + containerIdsWithResults.value.containsKey(container.id), + containerBadgeCount: (container) => + containerIdsWithResults.value[container?.id] ?? 0, searchTextListenable: searchTextListenable, ), ], @@ -114,10 +167,11 @@ class TabSearch extends HookConsumerWidget { ), ), SliverList.builder( - itemCount: tabs.length, + itemCount: filteredTabs.length, itemBuilder: (context, index) { - final result = tabs[index]; + final result = filteredTabs[index]; + final titleHasMatch = result.title.contains(_matchPrefix); final urlHasMatch = result.highlightedUrl?.contains(_matchPrefix) ?? false; final bodyHasMatch = @@ -142,11 +196,13 @@ class TabSearch extends HookConsumerWidget { ), ), ), - subtitle: (bodyHasMatch || urlHasMatch) + subtitle: (bodyHasMatch || (urlHasMatch && !titleHasMatch)) ? MarkdownBody( - data: bodyHasMatch - ? result.content! - : result.highlightedUrl!, + data: + (bodyHasMatch + ? result.content! + : result.highlightedUrl!) + .replaceAll(RegExp(r'\s+'), ' '), styleSheet: MarkdownStyleSheet( p: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart index 17f807a2..f38526e7 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart @@ -38,10 +38,12 @@ import 'package:weblibre/presentation/widgets/selectable_chips.dart'; class ContainerChips extends HookConsumerWidget { final bool displayMenu; + final bool showUnassignedChip; final bool showGroupSuggestions; final ContainerData? selectedContainer; final bool Function(ContainerDataWithCount)? containerFilter; + final int Function(ContainerDataWithCount?)? containerBadgeCount; final void Function(ContainerDataWithCount?)? onSelected; final void Function(ContainerDataWithCount)? onDeleted; final void Function(ContainerDataWithCount)? onLongPress; @@ -54,8 +56,10 @@ class ContainerChips extends HookConsumerWidget { required this.onDeleted, this.onLongPress, this.containerFilter, + this.containerBadgeCount, this.searchTextListenable, this.displayMenu = true, + this.showUnassignedChip = true, this.showGroupSuggestions = false, }); @@ -97,7 +101,9 @@ class ContainerChips extends HookConsumerWidget { container.color.withValues(alpha: 0.33), itemLabel: (container) => ContainerTitle(container: container), - itemBadgeCount: (container) => container.tabCount, + itemBadgeCount: (container) => + containerBadgeCount?.call(container) ?? + container.tabCount, itemWrap: (child, container) { return TabDragContainerTarget( container: container, @@ -105,36 +111,39 @@ class ContainerChips extends HookConsumerWidget { ); }, prefixListItems: [ - TabDragContainerTarget( - container: null, - child: Consumer( - builder: (context, ref, child) { - final tabCount = ref.watch( - containerTabCountProvider( - // ignore: provider_parameters - ContainerFilterById(containerId: null), - ).select((value) => value.value ?? 0), - ); + if (showUnassignedChip) + TabDragContainerTarget( + container: null, + child: Consumer( + builder: (context, ref, child) { + final int tabCount = + containerBadgeCount?.call(null) ?? + ref.watch( + containerTabCountProvider( + // ignore: provider_parameters + ContainerFilterById(containerId: null), + ).select((value) => value.value ?? 0), + ); - return FilterChip( - avatar: const Icon(MdiIcons.folderHidden), - labelPadding: (tabCount > 0) - ? null - : const EdgeInsets.only(right: 2.0), - label: (tabCount > 0) - ? Text(tabCount.toString()) - : const SizedBox.shrink(), - selected: selectedContainer == null, - showCheckmark: false, - onSelected: (value) { - if (value) { - onSelected?.call(null); - } - }, - ); - }, + return FilterChip( + avatar: const Icon(MdiIcons.folderHidden), + labelPadding: (tabCount > 0) + ? null + : const EdgeInsets.only(right: 2.0), + label: (tabCount > 0) + ? Text(tabCount.toString()) + : const SizedBox.shrink(), + selected: selectedContainer == null, + showCheckmark: false, + onSelected: (value) { + if (value) { + onSelected?.call(null); + } + }, + ); + }, + ), ), - ), if (showGroupSuggestions) Consumer( builder: (context, ref, child) {