diff --git a/app/lib/features/bangs/domain/repositories/sync.g.dart b/app/lib/features/bangs/domain/repositories/sync.g.dart index f57dcf73..a9cc707f 100644 --- a/app/lib/features/bangs/domain/repositories/sync.g.dart +++ b/app/lib/features/bangs/domain/repositories/sync.g.dart @@ -7,7 +7,7 @@ part of 'sync.dart'; // ************************************************************************** String _$bangSyncRepositoryHash() => - r'1e6341c8daf25aaee61d158f94445564d4450a9e'; + r'86933b05d01629c67ed0443df91e991c40136423'; /// See also [BangSyncRepository]. @ProviderFor(BangSyncRepository) diff --git a/app/lib/features/content_block/domain/repositories/sync.g.dart b/app/lib/features/content_block/domain/repositories/sync.g.dart index 9abc4e0e..57e0c97d 100644 --- a/app/lib/features/content_block/domain/repositories/sync.g.dart +++ b/app/lib/features/content_block/domain/repositories/sync.g.dart @@ -7,7 +7,7 @@ part of 'sync.dart'; // ************************************************************************** String _$hostSyncRepositoryHash() => - r'22b89d3687c415441f99caa4d77dabaeb4969534'; + r'5eed0c51e1b1e4dd1241634e297aff283eef01c5'; /// See also [HostSyncRepository]. @ProviderFor(HostSyncRepository) diff --git a/app/lib/features/search_browser/domain/providers.dart b/app/lib/features/search_browser/domain/providers.dart index 230d8e3f..6773bc03 100644 --- a/app/lib/features/search_browser/domain/providers.dart +++ b/app/lib/features/search_browser/domain/providers.dart @@ -125,3 +125,15 @@ class ActiveChatModel extends _$ActiveChatModel { return ChatModel.gpt4o; } } + +@Riverpod(keepAlive: true) +class ShowFindInPage extends _$ShowFindInPage { + void update(bool show) { + state = show; + } + + @override + bool build() { + return false; + } +} diff --git a/app/lib/features/search_browser/domain/providers.g.dart b/app/lib/features/search_browser/domain/providers.g.dart index f898357b..0c2619d3 100644 --- a/app/lib/features/search_browser/domain/providers.g.dart +++ b/app/lib/features/search_browser/domain/providers.g.dart @@ -396,5 +396,20 @@ final activeChatModelProvider = ); typedef _$ActiveChatModel = Notifier; +String _$showFindInPageHash() => r'7ee5703f7d0c7e8a8dd6b0849d7b1e0a41fe24d9'; + +/// See also [ShowFindInPage]. +@ProviderFor(ShowFindInPage) +final showFindInPageProvider = NotifierProvider.internal( + ShowFindInPage.new, + name: r'showFindInPageProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$showFindInPageHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ShowFindInPage = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/search_browser/presentation/screens/browser.dart b/app/lib/features/search_browser/presentation/screens/browser.dart index 4e67ef13..f267473b 100644 --- a/app/lib/features/search_browser/presentation/screens/browser.dart +++ b/app/lib/features/search_browser/presentation/screens/browser.dart @@ -434,6 +434,14 @@ class KagiScreen extends HookConsumerWidget { child: const Text('Share'), ), const Divider(), + MenuItemButton( + onPressed: () async { + ref.read(showFindInPageProvider.notifier).update(true); + }, + leadingIcon: const Icon(Icons.search), + child: const Text('Find in page'), + ), + const Divider(), MenuItemButton( onPressed: () async { await activeWebView?.currentController?.reload(); diff --git a/app/lib/features/web_view/presentation/widgets/web_view.dart b/app/lib/features/web_view/presentation/widgets/web_view.dart index 7827b991..8aaa935a 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view.dart @@ -216,6 +216,28 @@ class _WebViewState extends ConsumerState { ); final webViewProgress = useValueNotifier(100); + final inPageSearchResult = + useValueNotifier<({int activeMatchOrdinal, int numberOfMatches})?>( + null, + ); + + final findInteractionController = useMemoized( + () => FindInteractionController( + onFindResultReceived: ( + controller, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting, + ) { + if (isDoneCounting) { + inPageSearchResult.value = ( + activeMatchOrdinal: activeMatchOrdinal, + numberOfMatches: numberOfMatches, + ); + } + }, + ), + ); useOnAppLifecycleStateChange((previous, current) async { switch (current) { @@ -242,6 +264,7 @@ class _WebViewState extends ConsumerState { InAppWebView( initialUrlRequest: URLRequest(url: WebUri.uri(widget.page.value.url)), initialSettings: initialSettings, + findInteractionController: findInteractionController, contextMenu: ContextMenu( menuItems: [ ContextMenuItem( @@ -524,6 +547,99 @@ class _WebViewState extends ConsumerState { }, ), ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: HookConsumer( + builder: (context, ref, child) { + final showFindInPage = ref.watch(showFindInPageProvider); + + final textController = useTextEditingController(); + final progress = useValueListenable(webViewProgress); + + return Visibility( + visible: showFindInPage, + child: Padding( + padding: + EdgeInsets.only(bottom: (progress < 100) ? 4.0 : 0.0), + child: Material( + child: Row( + children: [ + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: textController, + autofocus: true, + autocorrect: false, + decoration: const InputDecoration.collapsed( + hintText: 'Find in page', + ), + keyboardType: TextInputType.text, + onSubmitted: (value) async { + if (value == '') { + inPageSearchResult.value = null; + await findInteractionController.clearMatches(); + } else { + await findInteractionController.findAll( + find: value, + ); + } + }, + ), + ), + HookConsumer( + builder: (context, ref, child) { + final searchResult = + useValueListenable(inPageSearchResult); + + if (searchResult != null) { + if (searchResult.numberOfMatches == 0) { + return const Text('Not found'); + } + + return Text( + '${searchResult.activeMatchOrdinal + 1} of ${searchResult.numberOfMatches}', + ); + } + + return const SizedBox.shrink(); + }, + ), + IconButton( + icon: const Icon(Icons.arrow_upward), + onPressed: () async { + await findInteractionController.findNext( + forward: false, + ); + }, + ), + IconButton( + icon: const Icon(Icons.arrow_downward), + onPressed: () async { + await findInteractionController.findNext(); + }, + ), + IconButton( + icon: const Icon(Icons.clear), + onPressed: () async { + ref + .read(showFindInPageProvider.notifier) + .update(false); + + inPageSearchResult.value = null; + await findInteractionController.clearMatches(); + textController.clear(); + }, + ), + ], + ), + ), + ), + ); + }, + ), + ), ], ); }