From 661e3c29d9771c69addfc29ee05667d335495122 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 2 Jun 2026 12:53:29 +0200 Subject: [PATCH] improve search flow --- .../widgets/tab_view/tab_preview.dart | 51 +++++++++++++------ .../widgets/tab_view/tab_view_header.dart | 23 +++++++++ .../widgets/search_modules/tab_search.dart | 24 ++++++--- apps/weblibre/lib/utils/ui_helper.dart | 21 ++++++++ 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart index 88da8b8b..528fc39e 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart @@ -23,7 +23,6 @@ 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/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; @@ -844,16 +843,27 @@ class SingleGridTabPreview extends HookConsumerWidget { depth: depth, onTap: () async { if (tabId != activeTabId) { + // Offer to locate the match within the page instead of opening + // Find in Page unprompted (see #421). Shown before onClose so it + // surfaces on the root messenger and survives the tray closing. + final query = sourceSearchQuery; + if (query != null && + query.isNotEmpty && + ref.read(findInPageControllerProvider(tabId)) == + FindInPageState.hidden()) { + final findController = ref.read( + findInPageControllerProvider(tabId).notifier, + ); + ui_helper.showFindInPageSuggestion( + context, + query: query, + onFind: () => findController.findAll(text: query), + ); + } + //Close first to avoid rebuilds onClose(); await ref.read(tabRepositoryProvider.notifier).selectTab(tabId); - if (sourceSearchQuery.isNotEmpty && - ref.read(findInPageControllerProvider(tabId)) == - FindInPageState.hidden()) { - await ref - .read(findInPageControllerProvider(tabId).notifier) - .findAll(text: sourceSearchQuery!); - } } else { onClose(); } @@ -989,16 +999,27 @@ class SingleListTabPreview extends HookConsumerWidget { depth: depth, onTap: () async { if (tabId != activeTabId) { + // Offer to locate the match within the page instead of opening + // Find in Page unprompted (see #421). Shown before onClose so it + // surfaces on the root messenger and survives the tray closing. + final query = sourceSearchQuery; + if (query != null && + query.isNotEmpty && + ref.read(findInPageControllerProvider(tabId)) == + FindInPageState.hidden()) { + final findController = ref.read( + findInPageControllerProvider(tabId).notifier, + ); + ui_helper.showFindInPageSuggestion( + context, + query: query, + onFind: () => findController.findAll(text: query), + ); + } + //Close first to avoid rebuilds onClose(); await ref.read(tabRepositoryProvider.notifier).selectTab(tabId); - if (sourceSearchQuery.isNotEmpty && - ref.read(findInPageControllerProvider(tabId)) == - FindInPageState.hidden()) { - await ref - .read(findInPageControllerProvider(tabId).notifier) - .findAll(text: sourceSearchQuery!); - } } else { onClose(); } diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart index fa990e28..066ee7b1 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.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 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; @@ -268,6 +269,28 @@ class TabViewHeader extends HookConsumerWidget { return null; }, [tabsReorderable, canManualReorder]); + // Keep the in-place tab filter in lockstep with the search field. The + // preview query lives in a provider whose lifetime is independent of this + // header and of [searchMode], so it can outlive the search UI and leave the + // tab list filtered with no visible search box (#421). Whenever we are not + // searching, drop any lingering query so all tabs are shown again. + useEffect(() { + if (!searchMode.value && + ref.exists( + tabSearchRepositoryProvider(TabSearchPartition.preview), + )) { + unawaited( + ref + .read( + tabSearchRepositoryProvider(TabSearchPartition.preview).notifier, + ) + .addQuery(''), + ); + } + + return null; + }, [searchMode.value]); + useOnListenableChange(searchTextController, () async { if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) { await ref diff --git a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart index f7919985..72e09172 100644 --- a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart +++ b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart @@ -45,6 +45,7 @@ import 'package:weblibre/presentation/widgets/safe_raw_image.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; import 'package:weblibre/utils/text_highlight.dart'; +import 'package:weblibre/utils/ui_helper.dart' as ui_helper; class TabSearch extends HookConsumerWidget { static const _matchPrefix = '***'; @@ -247,14 +248,23 @@ class TabSearch extends HookConsumerWidget { await ref .read(tabRepositoryProvider.notifier) .selectTab(result.id); - if (result.sourceSearchQuery.isNotEmpty && + + // Offer to locate the match within the page instead of + // opening Find in Page unprompted (see #421). + final query = result.sourceSearchQuery; + if (query != null && + query.isNotEmpty && ref.read(findInPageControllerProvider(result.id)) == - FindInPageState.hidden()) { - await ref - .read( - findInPageControllerProvider(result.id).notifier, - ) - .findAll(text: result.sourceSearchQuery!); + FindInPageState.hidden() && + context.mounted) { + final findController = ref.read( + findInPageControllerProvider(result.id).notifier, + ); + ui_helper.showFindInPageSuggestion( + context, + query: query, + onFind: () => findController.findAll(text: query), + ); } if (context.mounted) { diff --git a/apps/weblibre/lib/utils/ui_helper.dart b/apps/weblibre/lib/utils/ui_helper.dart index 854319fe..a40d1855 100644 --- a/apps/weblibre/lib/utils/ui_helper.dart +++ b/apps/weblibre/lib/utils/ui_helper.dart @@ -101,6 +101,27 @@ void showInfoMessage( ScaffoldMessenger.of(context).showSnackBar(snackBar); } +void showFindInPageSuggestion( + BuildContext context, { + required String query, + required VoidCallback onFind, + Duration duration = const Duration(seconds: 5), + bool persist = false, +}) { + final snackBar = _createFloatingSnackBar( + content: Text( + 'Find "${_truncateForSnackBar(query)}" on this page?', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + action: SnackBarAction(label: 'Find', onPressed: onFind), + duration: duration, + persist: persist, + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); +} + void showOpenedTabsFromAnotherDeviceMessage( BuildContext context, int openedTabs, {