diff --git a/app/lib/features/geckoview/domain/entities/states/find_result.dart b/app/lib/features/geckoview/domain/entities/states/find_result.dart index 8fa63760..8cb52a28 100644 --- a/app/lib/features/geckoview/domain/entities/states/find_result.dart +++ b/app/lib/features/geckoview/domain/entities/states/find_result.dart @@ -1,17 +1,23 @@ import 'package:fast_equatable/fast_equatable.dart'; class FindResultState with FastEquatable { + final String? lastSearchText; + final int activeMatchOrdinal; final int numberOfMatches; final bool isDoneCounting; + bool get hasMatches => numberOfMatches > 0; + FindResultState({ + required this.lastSearchText, required this.activeMatchOrdinal, required this.numberOfMatches, required this.isDoneCounting, }); factory FindResultState.$default() => FindResultState( + lastSearchText: null, activeMatchOrdinal: -1, numberOfMatches: 0, isDoneCounting: false, @@ -19,6 +25,7 @@ class FindResultState with FastEquatable { @override List get hashParameters => [ + lastSearchText, activeMatchOrdinal, numberOfMatches, isDoneCounting, diff --git a/app/lib/features/geckoview/domain/providers/tab_state.dart b/app/lib/features/geckoview/domain/providers/tab_state.dart index b505941f..ac30ac75 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -9,9 +9,11 @@ import 'package:lensai/features/geckoview/domain/entities/states/security.dart'; import 'package:lensai/features/geckoview/domain/entities/states/tab.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; +import 'package:lensai/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart'; import 'package:lensai/features/geckoview/utils/image_helper.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:rxdart/rxdart.dart'; part 'tab_state.g.dart'; @@ -105,19 +107,24 @@ class TabStates extends _$TabStates { void _onFindResultsChange(FindResultsEvent event) { final FindResultsEvent(:tabId, :results) = event; + final current = state[tabId] ?? TabState.$default(tabId); if (results.isNotEmpty) { final result = results.last; - final current = state[tabId] ?? TabState.$default(tabId); state = {...state} ..[tabId] = current.copyWith.findResultState( FindResultState( + lastSearchText: ref.read(findInPageRepositoryProvider(tabId)), activeMatchOrdinal: result.activeMatchOrdinal, numberOfMatches: result.numberOfMatches, isDoneCounting: result.isDoneCounting, ), ); + } else if (current.findResultState.hasMatches) { + state = { + ...state, + }..[tabId] = current.copyWith.findResultState(FindResultState.$default()); } } @@ -144,9 +151,11 @@ class TabStates extends _$TabStates { eventService.readerableEvents.listen((event) { _onReaderableStateChange(event); }), - eventService.findResultsEvent.listen((event) { - _onFindResultsChange(event); - }), + eventService.findResultsEvent + .debounceTime(const Duration(milliseconds: 25)) + .listen((event) { + _onFindResultsChange(event); + }), ]; ref.listen(fireImmediately: true, engineReadyStateProvider, ( diff --git a/app/lib/features/geckoview/domain/providers/tab_state.g.dart b/app/lib/features/geckoview/domain/providers/tab_state.g.dart index af7b62f4..3a1721ba 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -160,7 +160,7 @@ final selectedTabStateProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef SelectedTabStateRef = AutoDisposeProviderRef; -String _$tabStatesHash() => r'3a906aec8c4e650de8d5d2461ff45a99b8a0a3b9'; +String _$tabStatesHash() => r'f0b7b4fbfab633695b3e7e7457a6f84bc6e81b88'; /// See also [TabStates]. @ProviderFor(TabStates) diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/domain/entities/find_in_page_state.dart b/app/lib/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart similarity index 50% rename from app/lib/features/geckoview/features/find_in_page/presentation/domain/entities/find_in_page_state.dart rename to app/lib/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart index 502efb3b..983c8da9 100644 --- a/app/lib/features/geckoview/features/find_in_page/presentation/domain/entities/find_in_page_state.dart +++ b/app/lib/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart @@ -6,12 +6,12 @@ part 'find_in_page_state.g.dart'; @CopyWith() class FindInPageState with FastEquatable { final bool visible; - final String? searchText; + final String? lastSearchText; - FindInPageState({required this.visible, required this.searchText}); + FindInPageState({required this.visible, required this.lastSearchText}); - FindInPageState.hidden() : visible = false, searchText = null; + FindInPageState.hidden() : visible = false, lastSearchText = null; @override - List get hashParameters => [visible, searchText]; + List get hashParameters => [visible, lastSearchText]; } diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/domain/entities/find_in_page_state.g.dart b/app/lib/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.g.dart similarity index 83% rename from app/lib/features/geckoview/features/find_in_page/presentation/domain/entities/find_in_page_state.g.dart rename to app/lib/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.g.dart index 7382041f..20cbcd0c 100644 --- a/app/lib/features/geckoview/features/find_in_page/presentation/domain/entities/find_in_page_state.g.dart +++ b/app/lib/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.g.dart @@ -9,7 +9,7 @@ part of 'find_in_page_state.dart'; abstract class _$FindInPageStateCWProxy { FindInPageState visible(bool visible); - FindInPageState searchText(String? searchText); + FindInPageState lastSearchText(String? lastSearchText); /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FindInPageState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -17,7 +17,7 @@ abstract class _$FindInPageStateCWProxy { /// ```dart /// FindInPageState(...).copyWith(id: 12, name: "My name") /// ```` - FindInPageState call({bool visible, String? searchText}); + FindInPageState call({bool visible, String? lastSearchText}); } /// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfFindInPageState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfFindInPageState.copyWith.fieldName(...)` @@ -30,8 +30,8 @@ class _$FindInPageStateCWProxyImpl implements _$FindInPageStateCWProxy { FindInPageState visible(bool visible) => this(visible: visible); @override - FindInPageState searchText(String? searchText) => - this(searchText: searchText); + FindInPageState lastSearchText(String? lastSearchText) => + this(lastSearchText: lastSearchText); @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FindInPageState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -42,7 +42,7 @@ class _$FindInPageStateCWProxyImpl implements _$FindInPageStateCWProxy { /// ```` FindInPageState call({ Object? visible = const $CopyWithPlaceholder(), - Object? searchText = const $CopyWithPlaceholder(), + Object? lastSearchText = const $CopyWithPlaceholder(), }) { return FindInPageState( visible: @@ -50,11 +50,11 @@ class _$FindInPageStateCWProxyImpl implements _$FindInPageStateCWProxy { ? _value.visible // ignore: cast_nullable_to_non_nullable : visible as bool, - searchText: - searchText == const $CopyWithPlaceholder() - ? _value.searchText + lastSearchText: + lastSearchText == const $CopyWithPlaceholder() + ? _value.lastSearchText // ignore: cast_nullable_to_non_nullable - : searchText as String?, + : lastSearchText as String?, ); } } diff --git a/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart new file mode 100644 index 00000000..15eef5d8 --- /dev/null +++ b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart @@ -0,0 +1,37 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'find_in_page.g.dart'; + +@Riverpod(keepAlive: true) +class FindInPageRepository extends _$FindInPageRepository + implements GeckoFindInPageService { + @override + String? build(String? tabId) { + return null; + } + + @override + Future clearMatches() { + final service = GeckoFindInPageService(tabId: tabId); + + state = null; + + return service.clearMatches(); + } + + @override + Future findAll(String text) { + final service = GeckoFindInPageService(tabId: tabId); + + state = text; + + return service.findAll(text); + } + + @override + Future findNext(bool forward) { + final service = GeckoFindInPageService(tabId: tabId); + return service.findNext(forward); + } +} diff --git a/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart new file mode 100644 index 00000000..1303846e --- /dev/null +++ b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart @@ -0,0 +1,163 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'find_in_page.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$findInPageRepositoryHash() => + r'c289f136c27a22978f15668a13d7044eb39c3355'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$FindInPageRepository extends BuildlessNotifier { + late final String? tabId; + + String? build(String? tabId); +} + +/// See also [FindInPageRepository]. +@ProviderFor(FindInPageRepository) +const findInPageRepositoryProvider = FindInPageRepositoryFamily(); + +/// See also [FindInPageRepository]. +class FindInPageRepositoryFamily extends Family { + /// See also [FindInPageRepository]. + const FindInPageRepositoryFamily(); + + /// See also [FindInPageRepository]. + FindInPageRepositoryProvider call(String? tabId) { + return FindInPageRepositoryProvider(tabId); + } + + @override + FindInPageRepositoryProvider getProviderOverride( + covariant FindInPageRepositoryProvider provider, + ) { + return call(provider.tabId); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'findInPageRepositoryProvider'; +} + +/// See also [FindInPageRepository]. +class FindInPageRepositoryProvider + extends NotifierProviderImpl { + /// See also [FindInPageRepository]. + FindInPageRepositoryProvider(String? tabId) + : this._internal( + () => FindInPageRepository()..tabId = tabId, + from: findInPageRepositoryProvider, + name: r'findInPageRepositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$findInPageRepositoryHash, + dependencies: FindInPageRepositoryFamily._dependencies, + allTransitiveDependencies: + FindInPageRepositoryFamily._allTransitiveDependencies, + tabId: tabId, + ); + + FindInPageRepositoryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.tabId, + }) : super.internal(); + + final String? tabId; + + @override + String? runNotifierBuild(covariant FindInPageRepository notifier) { + return notifier.build(tabId); + } + + @override + Override overrideWith(FindInPageRepository Function() create) { + return ProviderOverride( + origin: this, + override: FindInPageRepositoryProvider._internal( + () => create()..tabId = tabId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + tabId: tabId, + ), + ); + } + + @override + NotifierProviderElement createElement() { + return _FindInPageRepositoryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is FindInPageRepositoryProvider && other.tabId == tabId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, tabId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin FindInPageRepositoryRef on NotifierProviderRef { + /// The parameter `tabId` of this provider. + String? get tabId; +} + +class _FindInPageRepositoryProviderElement + extends NotifierProviderElement + with FindInPageRepositoryRef { + _FindInPageRepositoryProviderElement(super.provider); + + @override + String? get tabId => (origin as FindInPageRepositoryProvider).tabId; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart index da8d2008..9eb22fb4 100644 --- a/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart +++ b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart @@ -1,8 +1,8 @@ -import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; -import 'package:lensai/features/geckoview/features/find_in_page/presentation/domain/entities/find_in_page_state.dart'; +import 'package:lensai/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart'; +import 'package:lensai/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'find_in_page.g.dart'; @@ -13,24 +13,23 @@ class FindInPageController extends _$FindInPageController { state = state.copyWith.visible(true); } - void hide() { + Future hide() async { + await clearMatches(); state = FindInPageState.hidden(); } Future findAll({required String text}) { - final service = GeckoFindInPageService( - tabId: ref.read(selectedTabProvider), - ); + final tabId = ref.read(selectedTabProvider); + final service = ref.read(findInPageRepositoryProvider(tabId).notifier); - state = FindInPageState(visible: true, searchText: text); + state = FindInPageState(visible: true, lastSearchText: text); return service.findAll(text); } Future findNext({bool forward = true}) { - final service = GeckoFindInPageService( - tabId: ref.read(selectedTabProvider), - ); + final tabId = ref.read(selectedTabProvider); + final service = ref.read(findInPageRepositoryProvider(tabId).notifier); state = state.copyWith.visible(true); @@ -38,9 +37,8 @@ class FindInPageController extends _$FindInPageController { } Future clearMatches() { - final service = GeckoFindInPageService( - tabId: ref.read(selectedTabProvider), - ); + final tabId = ref.read(selectedTabProvider); + final service = ref.read(findInPageRepositoryProvider(tabId).notifier); return service.clearMatches(); } @@ -48,15 +46,15 @@ class FindInPageController extends _$FindInPageController { @override FindInPageState build() { ref.listen(selectedTabStateProvider, (previous, next) async { - if (state.visible && state.searchText.isNotEmpty) { - if (previous != null) { + if (state.visible && state.lastSearchText.isNotEmpty) { + if (previous != null && next != null) { final loadingOrReloading = - previous.isLoading == true && next?.isLoading == false; - final tabSwitch = previous.id != next?.id; + previous.isLoading == true && next.isLoading == false; + final tabSwitchWithoutResults = + previous.id != next.id && !next.findResultState.hasMatches; - if (loadingOrReloading || tabSwitch) { - await clearMatches(); - await findAll(text: state.searchText!); + if (loadingOrReloading || tabSwitchWithoutResults) { + await findAll(text: state.lastSearchText!); } } } diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.g.dart b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.g.dart index a4b637c4..063c720c 100644 --- a/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.g.dart +++ b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.g.dart @@ -7,7 +7,7 @@ part of 'find_in_page.dart'; // ************************************************************************** String _$findInPageControllerHash() => - r'bb8584767aa8ad93e3bccd69d1012daaa8c5e9ea'; + r'b75a1edb4c25246bf2a5f5c9a807d6151b23856e'; /// See also [FindInPageController]. @ProviderFor(FindInPageController) diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart b/app/lib/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart index b0b8ba27..d545303f 100644 --- a/app/lib/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart +++ b/app/lib/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart @@ -12,14 +12,17 @@ class FindInPageWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final findInPageState = ref.watch(findInPageControllerProvider); + final searchResult = ref.watch( + selectedTabStateProvider.select((state) => state?.findResultState), + ); final textController = useTextEditingController( - text: findInPageState.searchText, - keys: [findInPageState.searchText], + text: searchResult?.lastSearchText ?? findInPageState.lastSearchText, + keys: [searchResult?.lastSearchText, findInPageState.lastSearchText], ); return Visibility( - visible: findInPageState.visible, + visible: findInPageState.visible || searchResult?.hasMatches == true, child: Padding( padding: padding, child: Material( @@ -48,51 +51,35 @@ class FindInPageWidget extends HookConsumerWidget { }, ), ), - HookConsumer( - builder: (context, ref, child) { - final searchResult = ref.watch( - selectedTabStateProvider.select( - (state) => state?.findResultState, - ), - ); - - if (searchResult != null) { - if (searchResult.numberOfMatches == 0) { - return const Text('Not found'); - } - - return Text( - '${searchResult.activeMatchOrdinal + 1} of ${searchResult.numberOfMatches}', - ); - } - - return const SizedBox.shrink(); - }, + Text( + (searchResult != null && searchResult.hasMatches) + ? '${searchResult.activeMatchOrdinal + 1} of ${searchResult.numberOfMatches}' + : 'Not found', ), IconButton( icon: const Icon(Icons.arrow_upward), onPressed: () async { - await ref - .read(findInPageControllerProvider.notifier) - .findNext(forward: false); + if (searchResult?.hasMatches ?? false) { + await ref + .read(findInPageControllerProvider.notifier) + .findNext(forward: false); + } }, ), IconButton( icon: const Icon(Icons.arrow_downward), onPressed: () async { - await ref - .read(findInPageControllerProvider.notifier) - .findNext(); + if (searchResult?.hasMatches ?? false) { + await ref + .read(findInPageControllerProvider.notifier) + .findNext(); + } }, ), IconButton( icon: const Icon(Icons.clear), onPressed: () async { - ref.read(findInPageControllerProvider.notifier).hide(); - - await ref - .read(findInPageControllerProvider.notifier) - .clearMatches(); + await ref.read(findInPageControllerProvider.notifier).hide(); textController.clear(); }, ),