diff --git a/app/lib/features/bangs/domain/providers/bangs.dart b/app/lib/features/bangs/domain/providers/bangs.dart index 5d410f94..251b3d7f 100644 --- a/app/lib/features/bangs/domain/providers/bangs.dart +++ b/app/lib/features/bangs/domain/providers/bangs.dart @@ -21,6 +21,12 @@ Stream defaultSearchBangData(Ref ref) { return repository.watchBang(trigger); } +@Riverpod() +Stream bangData(Ref ref, String trigger) { + final repository = ref.watch(bangDataRepositoryProvider.notifier); + return repository.watchBang(trigger); +} + @Riverpod() Stream>> bangCategories(Ref ref) { final repository = ref.watch(bangDataRepositoryProvider.notifier); diff --git a/app/lib/features/bangs/domain/providers/bangs.g.dart b/app/lib/features/bangs/domain/providers/bangs.g.dart index e8226c18..2ae40d73 100644 --- a/app/lib/features/bangs/domain/providers/bangs.g.dart +++ b/app/lib/features/bangs/domain/providers/bangs.g.dart @@ -24,26 +24,7 @@ final defaultSearchBangDataProvider = StreamProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef DefaultSearchBangDataRef = StreamProviderRef; -String _$bangCategoriesHash() => r'947fcfd2dffcc7f585c6ed7379d319f4fe72293a'; - -/// See also [bangCategories]. -@ProviderFor(bangCategories) -final bangCategoriesProvider = - AutoDisposeStreamProvider>>.internal( - bangCategories, - name: r'bangCategoriesProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$bangCategoriesHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef BangCategoriesRef = - AutoDisposeStreamProviderRef>>; -String _$bangListHash() => r'd1e0bb9fa4f523ce516e075c0d149bf7803ebb2b'; +String _$bangDataHash() => r'b10d3a74e19d30fc372b9ecbf7146bccd9fdf83b'; /// Copied from Dart SDK class _SystemHash { @@ -66,6 +47,142 @@ class _SystemHash { } } +/// See also [bangData]. +@ProviderFor(bangData) +const bangDataProvider = BangDataFamily(); + +/// See also [bangData]. +class BangDataFamily extends Family> { + /// See also [bangData]. + const BangDataFamily(); + + /// See also [bangData]. + BangDataProvider call(String trigger) { + return BangDataProvider(trigger); + } + + @override + BangDataProvider getProviderOverride(covariant BangDataProvider provider) { + return call(provider.trigger); + } + + 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'bangDataProvider'; +} + +/// See also [bangData]. +class BangDataProvider extends AutoDisposeStreamProvider { + /// See also [bangData]. + BangDataProvider(String trigger) + : this._internal( + (ref) => bangData(ref as BangDataRef, trigger), + from: bangDataProvider, + name: r'bangDataProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$bangDataHash, + dependencies: BangDataFamily._dependencies, + allTransitiveDependencies: BangDataFamily._allTransitiveDependencies, + trigger: trigger, + ); + + BangDataProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.trigger, + }) : super.internal(); + + final String trigger; + + @override + Override overrideWith( + Stream Function(BangDataRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: BangDataProvider._internal( + (ref) => create(ref as BangDataRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + trigger: trigger, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _BangDataProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is BangDataProvider && other.trigger == trigger; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, trigger.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin BangDataRef on AutoDisposeStreamProviderRef { + /// The parameter `trigger` of this provider. + String get trigger; +} + +class _BangDataProviderElement + extends AutoDisposeStreamProviderElement + with BangDataRef { + _BangDataProviderElement(super.provider); + + @override + String get trigger => (origin as BangDataProvider).trigger; +} + +String _$bangCategoriesHash() => r'947fcfd2dffcc7f585c6ed7379d319f4fe72293a'; + +/// See also [bangCategories]. +@ProviderFor(bangCategories) +final bangCategoriesProvider = + AutoDisposeStreamProvider>>.internal( + bangCategories, + name: r'bangCategoriesProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$bangCategoriesHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef BangCategoriesRef = + AutoDisposeStreamProviderRef>>; +String _$bangListHash() => r'd1e0bb9fa4f523ce516e075c0d149bf7803ebb2b'; + /// See also [bangList]. @ProviderFor(bangList) const bangListProvider = BangListFamily(); diff --git a/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.dart b/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.dart index 4d88bfe3..a12ce545 100644 --- a/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.dart +++ b/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.dart @@ -1,14 +1,37 @@ import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:weblibre/features/geckoview/features/search/domain/repositories/search_suggestions.dart'; +import 'package:weblibre/features/search/domain/autosuggest/brave.dart'; +import 'package:weblibre/features/search/domain/autosuggest/duckduckgo.dart'; import 'package:weblibre/features/search/domain/autosuggest/kagi.dart'; +import 'package:weblibre/features/search/domain/autosuggest/qwant.dart'; import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; part 'search_suggestions.g.dart'; @Riverpod(keepAlive: true) ISearchSuggestionProvider defaultSearchSuggestions(Ref ref) { - return ref.watch(kagiAutosuggestServiceProvider.notifier); + final provider = ref.watch( + generalSettingsRepositoryProvider.select( + (value) => value.defaultSearchSuggestionsProvider, + ), + ); + + return switch (provider) { + SearchSuggestionProviders.brave => ref.watch( + braveAutosuggestServiceProvider.notifier, + ), + SearchSuggestionProviders.ddg => ref.watch( + duckDuckGoAutosuggestServiceProvider.notifier, + ), + SearchSuggestionProviders.kagi => ref.watch( + kagiAutosuggestServiceProvider.notifier, + ), + SearchSuggestionProviders.qwant => ref.watch( + qwantAutosuggestServiceProvider.notifier, + ), + }; } @Riverpod() diff --git a/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.g.dart b/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.g.dart index 63bb0507..b9bf173e 100644 --- a/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.g.dart +++ b/app/lib/features/geckoview/features/search/domain/providers/search_suggestions.g.dart @@ -7,7 +7,7 @@ part of 'search_suggestions.dart'; // ************************************************************************** String _$defaultSearchSuggestionsHash() => - r'56514064baa64acf0a28ec34cfad957dd863538b'; + r'e503757071dd29988cd25e360b56a5a646800d5b'; /// See also [defaultSearchSuggestions]. @ProviderFor(defaultSearchSuggestions) diff --git a/app/lib/features/onboarding/presentation/pages/default_search.dart b/app/lib/features/onboarding/presentation/pages/default_search.dart index 69cdda7a..31649b3f 100644 --- a/app/lib/features/onboarding/presentation/pages/default_search.dart +++ b/app/lib/features/onboarding/presentation/pages/default_search.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; +import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; const defaultBangs = ['ddg', 'brave', 'startpage', 'qwant']; @@ -17,7 +21,14 @@ class DefaultSearchPage extends HookConsumerWidget { final bangs = ref.watch(bangListProvider(triggers: defaultBangs)); - final activeBang = ref.watch(defaultSearchBangDataProvider).valueOrNull; + final activeBang = ref.watch( + defaultSearchBangDataProvider.select((value) => value.valueOrNull), + ); + final activeAutosuggest = ref.watch( + generalSettingsRepositoryProvider.select( + (value) => value.defaultSearchSuggestionsProvider, + ), + ); Future updateSearchProvider(String trigger) async { await ref @@ -37,31 +48,27 @@ class DefaultSearchPage extends HookConsumerWidget { children: [ const SizedBox(height: 24), Center( - child: Text( - 'Default Search Provider', - style: theme.textTheme.headlineMedium, - ), + child: Text('Search', style: theme.textTheme.headlineMedium), ), const SizedBox(height: 24), - SizedBox( - height: 48, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Selected Provider'), - if (activeBang != null) - FilterChip( - showCheckmark: false, - label: Text(activeBang.websiteName), - avatar: UrlIcon([ - activeBang.getTemplateUrl(''), - ], iconSize: 20), - selected: true, - onSelected: (value) {}, - ), - ], - ), + const ListTile( + title: Text('Default Search Provider'), + leading: Icon(MdiIcons.cloudSearch), + contentPadding: EdgeInsets.zero, ), + if (activeBang != null) + Align( + alignment: Alignment.centerLeft, + child: FilterChip( + showCheckmark: false, + label: Text(activeBang.websiteName), + avatar: UrlIcon([ + activeBang.getTemplateUrl(''), + ], iconSize: 20), + selected: true, + onSelected: (value) {}, + ), + ), const Divider(), Wrap( spacing: 8.0, @@ -93,6 +100,41 @@ class DefaultSearchPage extends HookConsumerWidget { ), ], ), + const SizedBox(height: 24), + const ListTile( + title: Text('Default Autocomplete Provider'), + leading: Icon(MdiIcons.weatherCloudyArrowRight), + contentPadding: EdgeInsets.zero, + ), + DropdownMenu( + initialSelection: activeAutosuggest, + inputDecorationTheme: InputDecorationTheme( + prefixIconConstraints: BoxConstraints.tight( + const Size.square(24), + ), + ), + width: double.infinity, + leadingIcon: BangIcon(trigger: activeAutosuggest.relatedBang), + dropdownMenuEntries: SearchSuggestionProviders.values.map(( + provider, + ) { + return DropdownMenuEntry( + value: provider, + label: provider.label, + leadingIcon: BangIcon(trigger: provider.relatedBang), + ); + }).toList(), + onSelected: (value) async { + if (value != null) { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .defaultSearchSuggestionsProvider(value), + ); + } + }, + ), ], ); }, diff --git a/app/lib/features/search/domain/autosuggest/brave.dart b/app/lib/features/search/domain/autosuggest/brave.dart new file mode 100644 index 00000000..4e4d7ae1 --- /dev/null +++ b/app/lib/features/search/domain/autosuggest/brave.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:exceptions/exceptions.dart'; +import 'package:http/http.dart' as http; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/http_error_handler.dart'; +import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; + +part 'brave.g.dart'; + +@Riverpod(keepAlive: true) +class BraveAutosuggestService extends _$BraveAutosuggestService + implements ISearchSuggestionProvider { + static final _baseUrl = Uri.https('search.brave.com', 'api/suggest'); + + late http.Client _client; + + @override + void build() { + _client = http.Client(); + + ref.onDispose(() { + _client.close(); + }); + } + + @override + Future>> getSuggestions(String query) { + return Result.fromAsync(() async { + final response = await _client + .get(_baseUrl.replace(queryParameters: {'q': query})) + .timeout(const Duration(seconds: 5)); + + final results = jsonDecode(utf8.decode(response.bodyBytes)) as List; + return switch (results.last) { + final String result => [result], + final List resultList => resultList.cast(), + _ => [], + }; + }, exceptionHandler: handleHttpError); + } +} diff --git a/app/lib/features/search/domain/autosuggest/brave.g.dart b/app/lib/features/search/domain/autosuggest/brave.g.dart new file mode 100644 index 00000000..070f8b8a --- /dev/null +++ b/app/lib/features/search/domain/autosuggest/brave.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'brave.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$braveAutosuggestServiceHash() => + r'3b2f465dcf4b44b0450975d3909fc7066d171bf2'; + +/// See also [BraveAutosuggestService]. +@ProviderFor(BraveAutosuggestService) +final braveAutosuggestServiceProvider = + NotifierProvider.internal( + BraveAutosuggestService.new, + name: r'braveAutosuggestServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$braveAutosuggestServiceHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$BraveAutosuggestService = 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, deprecated_member_use_from_same_package diff --git a/app/lib/features/search/domain/autosuggest/duckduckgo.dart b/app/lib/features/search/domain/autosuggest/duckduckgo.dart new file mode 100644 index 00000000..6d067726 --- /dev/null +++ b/app/lib/features/search/domain/autosuggest/duckduckgo.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:exceptions/exceptions.dart'; +import 'package:http/http.dart' as http; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/http_error_handler.dart'; +import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; + +part 'duckduckgo.g.dart'; + +@Riverpod(keepAlive: true) +class DuckDuckGoAutosuggestService extends _$DuckDuckGoAutosuggestService + implements ISearchSuggestionProvider { + static final _baseUrl = Uri.https('duckduckgo.com', 'ac/'); + + late http.Client _client; + + @override + void build() { + _client = http.Client(); + + ref.onDispose(() { + _client.close(); + }); + } + + @override + Future>> getSuggestions(String query) { + return Result.fromAsync(() async { + final response = await _client + .get(_baseUrl.replace(queryParameters: {'type': 'list', 'q': query})) + .timeout(const Duration(seconds: 5)); + + final results = jsonDecode(utf8.decode(response.bodyBytes)) as List; + return switch (results.last) { + final String result => [result], + final List resultList => resultList.cast(), + _ => [], + }; + }, exceptionHandler: handleHttpError); + } +} diff --git a/app/lib/features/search/domain/autosuggest/duckduckgo.g.dart b/app/lib/features/search/domain/autosuggest/duckduckgo.g.dart new file mode 100644 index 00000000..f517483e --- /dev/null +++ b/app/lib/features/search/domain/autosuggest/duckduckgo.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'duckduckgo.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$duckDuckGoAutosuggestServiceHash() => + r'1679e6d9bce7cb380b1ee60c768abe2fddb79478'; + +/// See also [DuckDuckGoAutosuggestService]. +@ProviderFor(DuckDuckGoAutosuggestService) +final duckDuckGoAutosuggestServiceProvider = + NotifierProvider.internal( + DuckDuckGoAutosuggestService.new, + name: r'duckDuckGoAutosuggestServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$duckDuckGoAutosuggestServiceHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$DuckDuckGoAutosuggestService = 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, deprecated_member_use_from_same_package diff --git a/app/lib/features/search/domain/autosuggest/qwant.dart b/app/lib/features/search/domain/autosuggest/qwant.dart new file mode 100644 index 00000000..3b4fdf92 --- /dev/null +++ b/app/lib/features/search/domain/autosuggest/qwant.dart @@ -0,0 +1,46 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:convert'; + +import 'package:exceptions/exceptions.dart'; +import 'package:http/http.dart' as http; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/http_error_handler.dart'; +import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; + +part 'qwant.g.dart'; + +@Riverpod(keepAlive: true) +class QwantAutosuggestService extends _$QwantAutosuggestService + implements ISearchSuggestionProvider { + static final _baseUrl = Uri.https('api.qwant.com', 'v3/suggest'); + + late http.Client _client; + + @override + void build() { + _client = http.Client(); + + ref.onDispose(() { + _client.close(); + }); + } + + @override + Future>> getSuggestions(String query) { + return Result.fromAsync(() async { + final response = await _client + .get(_baseUrl.replace(queryParameters: {'q': query})) + .timeout(const Duration(seconds: 5)); + + final results = + jsonDecode(utf8.decode(response.bodyBytes)) as Map; + if (results['status'] != 'success') { + throw Exception('qwant api error'); + } + + final items = results['data']['items'] as List; + return items.map((item) => item['value'] as String).toList(); + }, exceptionHandler: handleHttpError); + } +} diff --git a/app/lib/features/search/domain/autosuggest/qwant.g.dart b/app/lib/features/search/domain/autosuggest/qwant.g.dart new file mode 100644 index 00000000..c81c1bac --- /dev/null +++ b/app/lib/features/search/domain/autosuggest/qwant.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'qwant.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$qwantAutosuggestServiceHash() => + r'a942cd073289d20d43647de5c0f43fb08790045f'; + +/// See also [QwantAutosuggestService]. +@ProviderFor(QwantAutosuggestService) +final qwantAutosuggestServiceProvider = + NotifierProvider.internal( + QwantAutosuggestService.new, + name: r'qwantAutosuggestServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$qwantAutosuggestServiceHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$QwantAutosuggestService = 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, deprecated_member_use_from_same_package diff --git a/app/lib/features/search/domain/entities/abstract/i_search_suggestion_provider.dart b/app/lib/features/search/domain/entities/abstract/i_search_suggestion_provider.dart index 03a8aeeb..15d88cad 100644 --- a/app/lib/features/search/domain/entities/abstract/i_search_suggestion_provider.dart +++ b/app/lib/features/search/domain/entities/abstract/i_search_suggestion_provider.dart @@ -1,5 +1,17 @@ import 'package:exceptions/exceptions.dart'; +enum SearchSuggestionProviders { + brave('Brave', 'brave'), + ddg('DuckDuckGo', 'ddg'), + kagi('Kagi', 'kagi'), + qwant('Qwant', 'qwant'); + + final String label; + final String relatedBang; + + const SearchSuggestionProviders(this.label, this.relatedBang); +} + abstract interface class ISearchSuggestionProvider { Future>> getSuggestions(String query); } diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index b994c8e8..022e8682 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -3,7 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; +import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart'; import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart'; import 'package:weblibre/features/settings/presentation/widgets/default_search_selector.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; @@ -93,6 +95,65 @@ class GeneralSettingsScreen extends HookConsumerWidget { ], ), ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Default Autocomplete Provider'), + leading: Icon(MdiIcons.weatherCloudyArrowRight), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: const EdgeInsets.only(left: 40), + child: DropdownMenu( + initialSelection: + generalSettings.defaultSearchSuggestionsProvider, + inputDecorationTheme: InputDecorationTheme( + prefixIconConstraints: BoxConstraints.tight( + const Size.square(24), + ), + ), + width: double.infinity, + leadingIcon: BangIcon( + trigger: generalSettings + .defaultSearchSuggestionsProvider + .relatedBang, + ), + dropdownMenuEntries: SearchSuggestionProviders.values + .map((provider) { + return DropdownMenuEntry( + value: provider, + label: provider.label, + leadingIcon: BangIcon( + trigger: provider.relatedBang, + ), + ); + }) + .toList(), + onSelected: (value) async { + if (value != null) { + await ref + .read( + saveGeneralSettingsControllerProvider + .notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .defaultSearchSuggestionsProvider(value), + ); + } + }, + ), + ), + ], + ), + ), SwitchListTile.adaptive( title: const Text('Enable Reader Mode'), subtitle: const Text( diff --git a/app/lib/features/settings/presentation/widgets/bang_icon.dart b/app/lib/features/settings/presentation/widgets/bang_icon.dart new file mode 100644 index 00000000..1b19aae8 --- /dev/null +++ b/app/lib/features/settings/presentation/widgets/bang_icon.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; +import 'package:weblibre/presentation/widgets/url_icon.dart'; + +class BangIcon extends ConsumerWidget { + final String trigger; + final double iconSize; + + const BangIcon({super.key, required this.trigger, this.iconSize = 20}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bang = ref.watch( + bangDataProvider(trigger).select((value) => value.valueOrNull), + ); + + return UrlIcon([ + if (bang != null) bang.getTemplateUrl(''), + ], iconSize: iconSize); + } +} diff --git a/app/lib/features/settings/presentation/widgets/default_search_selector.dart b/app/lib/features/settings/presentation/widgets/default_search_selector.dart index 641923be..b6bc3597 100644 --- a/app/lib/features/settings/presentation/widgets/default_search_selector.dart +++ b/app/lib/features/settings/presentation/widgets/default_search_selector.dart @@ -12,7 +12,9 @@ class DefaultSearchSelector extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final activeBang = ref.watch(defaultSearchBangDataProvider).valueOrNull; + final activeBang = ref.watch( + defaultSearchBangDataProvider.select((value) => value.valueOrNull), + ); final availableBangs = ref.watch(frequentBangListProvider); Future updateSearchProvider(String trigger) async { diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index 9bbd7f77..8087fc24 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -2,10 +2,12 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart'; part 'general_settings.g.dart'; const _fallbackSearchProvider = 'ddg'; +const _fallbackAutocompleteProvider = SearchSuggestionProviders.ddg; enum DeleteBrowsingDataType { tabs('Open tabs'), @@ -29,6 +31,7 @@ class GeneralSettings with FastEquatable { final bool enforceReadability; final Set? deleteBrowsingDataOnQuit; final String defaultSearchProvider; + final SearchSuggestionProviders defaultSearchSuggestionsProvider; final bool createChildTabsOption; final bool proxyPrivateTabsTor; @@ -39,6 +42,7 @@ class GeneralSettings with FastEquatable { required this.enforceReadability, required this.deleteBrowsingDataOnQuit, required this.defaultSearchProvider, + required this.defaultSearchSuggestionsProvider, required this.createChildTabsOption, required this.proxyPrivateTabsTor, }); @@ -49,12 +53,15 @@ class GeneralSettings with FastEquatable { bool? enforceReadability, this.deleteBrowsingDataOnQuit, String? defaultSearchProvider, + SearchSuggestionProviders? defaultSearchSuggestionsProvider, bool? createChildTabsOption, bool? proxyPrivateTabsTor, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, defaultSearchProvider = defaultSearchProvider ?? _fallbackSearchProvider, + defaultSearchSuggestionsProvider = + defaultSearchSuggestionsProvider ?? _fallbackAutocompleteProvider, createChildTabsOption = createChildTabsOption ?? false, proxyPrivateTabsTor = proxyPrivateTabsTor ?? false; @@ -70,6 +77,7 @@ class GeneralSettings with FastEquatable { enforceReadability, deleteBrowsingDataOnQuit, defaultSearchProvider, + defaultSearchSuggestionsProvider, createChildTabsOption, proxyPrivateTabsTor, ]; diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index b7237a4d..58411efc 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -19,6 +19,10 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings defaultSearchProvider(String defaultSearchProvider); + GeneralSettings defaultSearchSuggestionsProvider( + SearchSuggestionProviders defaultSearchSuggestionsProvider, + ); + GeneralSettings createChildTabsOption(bool createChildTabsOption); GeneralSettings proxyPrivateTabsTor(bool proxyPrivateTabsTor); @@ -35,6 +39,7 @@ abstract class _$GeneralSettingsCWProxy { bool enforceReadability, Set? deleteBrowsingDataOnQuit, String defaultSearchProvider, + SearchSuggestionProviders defaultSearchSuggestionsProvider, bool createChildTabsOption, bool proxyPrivateTabsTor, }); @@ -66,6 +71,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings defaultSearchProvider(String defaultSearchProvider) => this(defaultSearchProvider: defaultSearchProvider); + @override + GeneralSettings defaultSearchSuggestionsProvider( + SearchSuggestionProviders defaultSearchSuggestionsProvider, + ) => this(defaultSearchSuggestionsProvider: defaultSearchSuggestionsProvider); + @override GeneralSettings createChildTabsOption(bool createChildTabsOption) => this(createChildTabsOption: createChildTabsOption); @@ -87,6 +97,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? enforceReadability = const $CopyWithPlaceholder(), Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(), Object? defaultSearchProvider = const $CopyWithPlaceholder(), + Object? defaultSearchSuggestionsProvider = const $CopyWithPlaceholder(), Object? createChildTabsOption = const $CopyWithPlaceholder(), Object? proxyPrivateTabsTor = const $CopyWithPlaceholder(), }) { @@ -113,6 +124,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.defaultSearchProvider // ignore: cast_nullable_to_non_nullable : defaultSearchProvider as String, + defaultSearchSuggestionsProvider: + defaultSearchSuggestionsProvider == const $CopyWithPlaceholder() + ? _value.defaultSearchSuggestionsProvider + // ignore: cast_nullable_to_non_nullable + : defaultSearchSuggestionsProvider as SearchSuggestionProviders, createChildTabsOption: createChildTabsOption == const $CopyWithPlaceholder() ? _value.createChildTabsOption @@ -146,6 +162,10 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => ?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e)) .toSet(), defaultSearchProvider: json['defaultSearchProvider'] as String?, + defaultSearchSuggestionsProvider: $enumDecodeNullable( + _$SearchSuggestionProvidersEnumMap, + json['defaultSearchSuggestionsProvider'], + ), createChildTabsOption: json['createChildTabsOption'] as bool?, proxyPrivateTabsTor: json['proxyPrivateTabsTor'] as bool?, ); @@ -159,6 +179,9 @@ Map _$GeneralSettingsToJson(GeneralSettings instance) => ?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!) .toList(), 'defaultSearchProvider': instance.defaultSearchProvider, + 'defaultSearchSuggestionsProvider': + _$SearchSuggestionProvidersEnumMap[instance + .defaultSearchSuggestionsProvider]!, 'createChildTabsOption': instance.createChildTabsOption, 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, }; @@ -177,3 +200,10 @@ const _$DeleteBrowsingDataTypeEnumMap = { DeleteBrowsingDataType.permissions: 'permissions', DeleteBrowsingDataType.downloads: 'downloads', }; + +const _$SearchSuggestionProvidersEnumMap = { + SearchSuggestionProviders.brave: 'brave', + SearchSuggestionProviders.ddg: 'ddg', + SearchSuggestionProviders.kagi: 'kagi', + SearchSuggestionProviders.qwant: 'qwant', +}; diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index d4280f57..76988344 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -43,6 +43,11 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.string, db.typeMapping, ), + 'defaultSearchSuggestionsProvider': + settings['defaultSearchSuggestionsProvider']?.readAs( + DriftSqlType.string, + db.typeMapping, + ), 'createChildTabsOption': settings['createChildTabsOption']?.readAs( DriftSqlType.bool, db.typeMapping, diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index 0bd3e2d9..63b48338 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -7,7 +7,7 @@ part of 'general_settings.dart'; // ************************************************************************** String _$generalSettingsRepositoryHash() => - r'4aea6ef10e883aa715141cc314d02d8af90dbdb5'; + r'5335acea5fb175b665b80b2241ce463ae8ad9f89'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository)