From 9ddf1daea9e016195a9305c50114dc2e8dc49cb5 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 22 Jan 2026 10:02:40 +0100 Subject: [PATCH] respect edit mode in history search --- .../presentation/widgets/site_search.dart | 235 ------------------ .../search/presentation/screens/search.dart | 30 ++- .../search_modules/history_suggestions.dart | 25 +- 3 files changed, 33 insertions(+), 257 deletions(-) delete mode 100644 app/lib/features/bangs/presentation/widgets/site_search.dart diff --git a/app/lib/features/bangs/presentation/widgets/site_search.dart b/app/lib/features/bangs/presentation/widgets/site_search.dart deleted file mode 100644 index 6511ac34..00000000 --- a/app/lib/features/bangs/presentation/widgets/site_search.dart +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (c) 2024-2025 Fabian Freund. - * - * This file is part of WebLibre - * (see https://weblibre.eu). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * 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:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:nullability/nullability.dart'; -import 'package:weblibre/core/routing/routes.dart'; -import 'package:weblibre/extensions/uri.dart'; -import 'package:weblibre/features/bangs/data/models/bang_data.dart'; -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/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'; -import 'package:weblibre/features/geckoview/features/search/domain/providers/search_suggestions.dart'; -import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_field.dart'; -import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/fixed_search_suggestions.dart'; -import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; -import 'package:weblibre/presentation/hooks/on_listenable_change_selector.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 SiteSearch extends HookConsumerWidget { - final String domain; - final List availableBangs; - final TextEditingController? controller; - final bool searchInNewTab; - - const SiteSearch({ - required this.domain, - required this.availableBangs, - super.key, - this.controller, - this.searchInNewTab = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final searchTextController = controller ?? useTextEditingController(); - final searchFocusNode = useFocusNode(); - - useOnListenableChangeSelector( - searchFocusNode, - () => searchFocusNode.hasFocus, - () { - if (searchFocusNode.hasFocus) { - // Select all text when the field is focused - searchTextController.selection = TextSelection( - baseOffset: 0, - extentOffset: searchTextController.text.length, - ); - } - }, - ); - - //Request initial focus in a way our useOnListenableChangeSelector is triggered - useEffect(() { - //Wait for first frame then request focus - unawaited( - Future.delayed(const Duration(milliseconds: 1000 ~/ 60)).whenComplete( - () { - WidgetsBinding.instance.addPostFrameCallback((_) { - searchFocusNode.requestFocus(); - }); - }, - ), - ); - - return null; - }, []); - - useOnAppLifecycleStateChange((previous, current) { - switch (current) { - case AppLifecycleState.detached: - case AppLifecycleState.inactive: - case AppLifecycleState.hidden: - case AppLifecycleState.paused: - //Fixes issue with disappearing keyboard after resume (even we request focus) - searchFocusNode.unfocus(); - case AppLifecycleState.resumed: - WidgetsBinding.instance.addPostFrameCallback((_) { - searchFocusNode.requestFocus(); - }); - } - }); - - useOnListenableChange(searchTextController, () { - if (ref.exists(searchSuggestionsProvider())) { - ref - .read(searchSuggestionsProvider().notifier) - .addQuery(searchTextController.text); - } - }); - - final selectedBang = ref.watch(selectedBangDataProvider(domain: domain)); - final defaultSearchBang = ref.watch( - defaultSearchBangDataProvider.select((value) => value.value), - ); - - Future submitSearch(String value) async { - if (value.isNotEmpty) { - final bang = selectedBang ?? defaultSearchBang; - - final newUrl = uri_parser.tryParseUrl(value, eagerParsing: true); - - if (newUrl != null) { - await ref - .read(tabSessionProvider(tabId: null).notifier) - .loadUrl(url: newUrl); - - ref.read(bottomSheetControllerProvider.notifier).requestDismiss(); - } else if (bang != null) { - final isPrivate = - ref.read(selectedTabStateProvider)?.isPrivate ?? - ref - .read(generalSettingsWithDefaultsProvider) - .defaultCreateTabType == - TabType.private; - - final searchUri = bang.getTemplateUrl(value); - - if (!isPrivate) { - await ref - .read(bangSearchProvider.notifier) - .triggerBangSearch(bang, value); - } - - if (searchInNewTab) { - await ref - .read(tabRepositoryProvider.notifier) - .addTab(url: searchUri, private: isPrivate, selectTab: true); - } else { - await ref - .read(tabSessionProvider(tabId: null).notifier) - .loadUrl(url: searchUri); - } - - ref.read(bottomSheetControllerProvider.notifier).requestDismiss(); - } - } - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Visibility( - visible: availableBangs.isNotEmpty, - child: 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) { - if (selectedBang == null) { - final hasSupportedScheme = - uri_parser - .tryParseUrl(searchTextController.text) - .mapNotNull((uri) => uri.hasSupportedScheme) ?? - false; - - if (hasSupportedScheme) { - searchTextController.clear(); - } - } - - 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(); - } - }, - ), - ), - ), - SearchField( - textEditingController: searchTextController, - focusNode: searchFocusNode, - maxLines: null, - activeBang: selectedBang, - showSuggestions: true, - label: (selectedBang != null) - ? const Text('Search') - : const Text('Address / Search'), - unfocusOnTapOutside: false, - onSubmitted: (value) async { - await submitSearch(value); - }, - ), - FixedSearchTermSuggestions( - searchTextController: searchTextController, - activeBang: selectedBang, - submitSearch: submitSearch, - limit: 25, - ), - ], - ); - } -} 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 5bb4ea5a..4e111e78 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -426,8 +426,36 @@ class SearchScreen extends HookConsumerWidget { TabSearch(searchTextListenable: sampledSearchText), FeedSearch(searchTextNotifier: sampledSearchText), HistorySuggestions( - isPrivate: privateTabMode, searchTextListenable: sampledSearchText, + onUriSelected: (uri) async { + if (isEditMode) { + // Load into existing tab + await ref + .read(tabSessionProvider(tabId: tabId).notifier) + .loadUrl(url: uri); + } else { + // Create new tab + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: uri, + private: privateTabMode, + parentId: (selectedTabType.value == TabType.child) + ? ref.read(selectedTabProvider) + : null, + launchedFromIntent: launchedFromIntent, + selectTab: true, + ); + } + + if (context.mounted) { + ref + .read(bottomSheetControllerProvider.notifier) + .requestDismiss(); + + const BrowserRoute().go(context); + } + }, ), ], ); 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 8a73b0fd..b6c45e98 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 @@ -25,9 +25,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:sliver_tools/sliver_tools.dart'; -import 'package:weblibre/core/routing/routes.dart'; -import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; -import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/search/domain/providers/engine_suggestions.dart'; import 'package:weblibre/features/geckoview/utils/image_helper.dart'; import 'package:weblibre/presentation/hooks/cached_future.dart'; @@ -35,12 +32,12 @@ import 'package:weblibre/presentation/widgets/failure_widget.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; class HistorySuggestions extends HookConsumerWidget { - final bool isPrivate; final ValueListenable searchTextListenable; + final void Function(Uri uri) onUriSelected; const HistorySuggestions({ super.key, - required this.isPrivate, + required this.onUriSelected, required this.searchTextListenable, }); @@ -121,23 +118,9 @@ class HistorySuggestions extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), ), - onTap: () async { + onTap: () { if (uri != null) { - await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: uri, - private: isPrivate, - selectTab: true, - ); - - if (context.mounted) { - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); - - const BrowserRoute().go(context); - } + onUriSelected(uri); } }, );