improve search flow

This commit is contained in:
Fabian Freund
2026-06-02 12:53:29 +02:00
parent 84ae5e71dd
commit 661e3c29d9
4 changed files with 97 additions and 22 deletions
@@ -23,7 +23,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/design/app_colors.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/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
@@ -844,16 +843,27 @@ class SingleGridTabPreview extends HookConsumerWidget {
depth: depth, depth: depth,
onTap: () async { onTap: () async {
if (tabId != activeTabId) { 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 //Close first to avoid rebuilds
onClose(); onClose();
await ref.read(tabRepositoryProvider.notifier).selectTab(tabId); 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 { } else {
onClose(); onClose();
} }
@@ -989,16 +999,27 @@ class SingleListTabPreview extends HookConsumerWidget {
depth: depth, depth: depth,
onTap: () async { onTap: () async {
if (tabId != activeTabId) { 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 //Close first to avoid rebuilds
onClose(); onClose();
await ref.read(tabRepositoryProvider.notifier).selectTab(tabId); 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 { } else {
onClose(); onClose();
} }
@@ -17,6 +17,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@@ -268,6 +269,28 @@ class TabViewHeader extends HookConsumerWidget {
return null; return null;
}, [tabsReorderable, canManualReorder]); }, [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 { useOnListenableChange(searchTextController, () async {
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) { if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
await ref await ref
@@ -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/uri_breadcrumb.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart';
import 'package:weblibre/utils/text_highlight.dart'; import 'package:weblibre/utils/text_highlight.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class TabSearch extends HookConsumerWidget { class TabSearch extends HookConsumerWidget {
static const _matchPrefix = '***'; static const _matchPrefix = '***';
@@ -247,14 +248,23 @@ class TabSearch extends HookConsumerWidget {
await ref await ref
.read(tabRepositoryProvider.notifier) .read(tabRepositoryProvider.notifier)
.selectTab(result.id); .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)) == ref.read(findInPageControllerProvider(result.id)) ==
FindInPageState.hidden()) { FindInPageState.hidden() &&
await ref context.mounted) {
.read( final findController = ref.read(
findInPageControllerProvider(result.id).notifier, findInPageControllerProvider(result.id).notifier,
) );
.findAll(text: result.sourceSearchQuery!); ui_helper.showFindInPageSuggestion(
context,
query: query,
onFind: () => findController.findAll(text: query),
);
} }
if (context.mounted) { if (context.mounted) {
+21
View File
@@ -101,6 +101,27 @@ void showInfoMessage(
ScaffoldMessenger.of(context).showSnackBar(snackBar); 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( void showOpenedTabsFromAnotherDeviceMessage(
BuildContext context, BuildContext context,
int openedTabs, { int openedTabs, {