diff --git a/app/lib/features/bangs/data/models/bang.dart b/app/lib/features/bangs/data/models/bang.dart index a58af330..a7a3cc25 100644 --- a/app/lib/features/bangs/data/models/bang.dart +++ b/app/lib/features/bangs/data/models/bang.dart @@ -9,13 +9,16 @@ part 'bang.g.dart'; enum BangGroup { general( - 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/bangs.json', + 'https://raw.githubusercontent.com/FaFre/bangs/main/data/bangs.json', ), assistant( - 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/assistant_bangs.json', + 'https://raw.githubusercontent.com/FaFre/bangs/main/data/assistant_bangs.json', ), kagi( - 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/kagi_bangs.json', + 'https://raw.githubusercontent.com/FaFre/bangs/main/data/kagi_bangs.json', + ), + custom( + 'https://raw.githubusercontent.com/FaFre/bangs/refs/heads/custom/data/custom.json', ); final String url; diff --git a/app/lib/features/bangs/domain/providers/bangs.dart b/app/lib/features/bangs/domain/providers/bangs.dart index 2e7e69b5..186ad71d 100644 --- a/app/lib/features/bangs/domain/providers/bangs.dart +++ b/app/lib/features/bangs/domain/providers/bangs.dart @@ -17,7 +17,7 @@ Stream bangData(Ref ref, String? trigger) { @Riverpod(keepAlive: true) Stream defaultSearchBangData(Ref ref) { final repository = ref.watch(bangDataRepositoryProvider.notifier); - return repository.watchBang('kagi'); + return repository.watchBang('lai'); } @Riverpod() diff --git a/app/lib/features/bangs/domain/providers/bangs.g.dart b/app/lib/features/bangs/domain/providers/bangs.g.dart index 545c9ad3..ca6f5ac5 100644 --- a/app/lib/features/bangs/domain/providers/bangs.g.dart +++ b/app/lib/features/bangs/domain/providers/bangs.g.dart @@ -157,7 +157,7 @@ class _BangDataProviderElement } String _$defaultSearchBangDataHash() => - r'b7547084b261ccb1dc02a6c7230fe57f6902dd58'; + r'c93026fe7b1650fe8a39bb2a8850af3ee416c306'; /// See also [defaultSearchBangData]. @ProviderFor(defaultSearchBangData) diff --git a/app/lib/features/bangs/presentation/widgets/site_search.dart b/app/lib/features/bangs/presentation/widgets/site_search.dart index 021e7133..f921b399 100644 --- a/app/lib/features/bangs/presentation/widgets/site_search.dart +++ b/app/lib/features/bangs/presentation/widgets/site_search.dart @@ -39,10 +39,7 @@ class SiteSearch extends HookConsumerWidget { }, ); - final selectedBang = ref.watch( - selectedBangDataProvider(domain: domain) - .select((value) => value.valueOrNull), - ); + final selectedBang = ref.watch(selectedBangDataProvider(domain: domain)); final activeBang = selectedBang ?? availableBangs.firstOrNull; diff --git a/app/lib/features/geckoview/features/browser/domain/providers.dart b/app/lib/features/geckoview/features/browser/domain/providers.dart index e81b3338..d21e0bad 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.dart @@ -36,11 +36,25 @@ class SelectedBangTrigger extends _$SelectedBangTrigger { } @Riverpod() -Stream selectedBangData(Ref ref, {String? domain}) { - final repository = ref.watch(bangDataRepositoryProvider.notifier); - final selectedBangTrigger = - ref.watch(selectedBangTriggerProvider(domain: domain)); - return repository.watchBang(selectedBangTrigger); +class SelectedBangData extends _$SelectedBangData { + @override + BangData? build({String? domain}) { + final repository = ref.watch(bangDataRepositoryProvider.notifier); + final selectedBangTrigger = + ref.watch(selectedBangTriggerProvider(domain: domain)); + + final subscription = repository.watchBang(selectedBangTrigger).listen( + (value) { + state = value; + }, + ); + + ref.onDispose(() { + unawaited(subscription.cancel()); + }); + + return null; + } } @Riverpod(keepAlive: true) diff --git a/app/lib/features/geckoview/features/browser/domain/providers.g.dart b/app/lib/features/geckoview/features/browser/domain/providers.g.dart index 957d509f..a8af11a3 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$selectedBangDataHash() => r'd43e679c40d35c4f4c692ee286dc21789d7287dd'; +String _$availableTabIdsHash() => r'd8b55b012b38111c8d5e069987a55645d1a71b89'; /// Copied from Dart SDK class _SystemHash { @@ -29,137 +29,6 @@ class _SystemHash { } } -/// See also [selectedBangData]. -@ProviderFor(selectedBangData) -const selectedBangDataProvider = SelectedBangDataFamily(); - -/// See also [selectedBangData]. -class SelectedBangDataFamily extends Family> { - /// See also [selectedBangData]. - const SelectedBangDataFamily(); - - /// See also [selectedBangData]. - SelectedBangDataProvider call({ - String? domain, - }) { - return SelectedBangDataProvider( - domain: domain, - ); - } - - @override - SelectedBangDataProvider getProviderOverride( - covariant SelectedBangDataProvider provider, - ) { - return call( - domain: provider.domain, - ); - } - - 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'selectedBangDataProvider'; -} - -/// See also [selectedBangData]. -class SelectedBangDataProvider extends AutoDisposeStreamProvider { - /// See also [selectedBangData]. - SelectedBangDataProvider({ - String? domain, - }) : this._internal( - (ref) => selectedBangData( - ref as SelectedBangDataRef, - domain: domain, - ), - from: selectedBangDataProvider, - name: r'selectedBangDataProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$selectedBangDataHash, - dependencies: SelectedBangDataFamily._dependencies, - allTransitiveDependencies: - SelectedBangDataFamily._allTransitiveDependencies, - domain: domain, - ); - - SelectedBangDataProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.domain, - }) : super.internal(); - - final String? domain; - - @override - Override overrideWith( - Stream Function(SelectedBangDataRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: SelectedBangDataProvider._internal( - (ref) => create(ref as SelectedBangDataRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - domain: domain, - ), - ); - } - - @override - AutoDisposeStreamProviderElement createElement() { - return _SelectedBangDataProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is SelectedBangDataProvider && other.domain == domain; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, domain.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin SelectedBangDataRef on AutoDisposeStreamProviderRef { - /// The parameter `domain` of this provider. - String? get domain; -} - -class _SelectedBangDataProviderElement - extends AutoDisposeStreamProviderElement - with SelectedBangDataRef { - _SelectedBangDataProviderElement(super.provider); - - @override - String? get domain => (origin as SelectedBangDataProvider).domain; -} - -String _$availableTabIdsHash() => r'd8b55b012b38111c8d5e069987a55645d1a71b89'; - /// See also [availableTabIds]. @ProviderFor(availableTabIds) const availableTabIdsProvider = AvailableTabIdsFamily(); @@ -893,6 +762,152 @@ class _SelectedBangTriggerProviderElement String? get domain => (origin as SelectedBangTriggerProvider).domain; } +String _$selectedBangDataHash() => r'28c394e5989e108870ca5733e916b80b3cf8972c'; + +abstract class _$SelectedBangData + extends BuildlessAutoDisposeNotifier { + late final String? domain; + + BangData? build({ + String? domain, + }); +} + +/// See also [SelectedBangData]. +@ProviderFor(SelectedBangData) +const selectedBangDataProvider = SelectedBangDataFamily(); + +/// See also [SelectedBangData]. +class SelectedBangDataFamily extends Family { + /// See also [SelectedBangData]. + const SelectedBangDataFamily(); + + /// See also [SelectedBangData]. + SelectedBangDataProvider call({ + String? domain, + }) { + return SelectedBangDataProvider( + domain: domain, + ); + } + + @override + SelectedBangDataProvider getProviderOverride( + covariant SelectedBangDataProvider provider, + ) { + return call( + domain: provider.domain, + ); + } + + 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'selectedBangDataProvider'; +} + +/// See also [SelectedBangData]. +class SelectedBangDataProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [SelectedBangData]. + SelectedBangDataProvider({ + String? domain, + }) : this._internal( + () => SelectedBangData()..domain = domain, + from: selectedBangDataProvider, + name: r'selectedBangDataProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedBangDataHash, + dependencies: SelectedBangDataFamily._dependencies, + allTransitiveDependencies: + SelectedBangDataFamily._allTransitiveDependencies, + domain: domain, + ); + + SelectedBangDataProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.domain, + }) : super.internal(); + + final String? domain; + + @override + BangData? runNotifierBuild( + covariant SelectedBangData notifier, + ) { + return notifier.build( + domain: domain, + ); + } + + @override + Override overrideWith(SelectedBangData Function() create) { + return ProviderOverride( + origin: this, + override: SelectedBangDataProvider._internal( + () => create()..domain = domain, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + domain: domain, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _SelectedBangDataProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SelectedBangDataProvider && other.domain == domain; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, domain.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SelectedBangDataRef on AutoDisposeNotifierProviderRef { + /// The parameter `domain` of this provider. + String? get domain; +} + +class _SelectedBangDataProviderElement + extends AutoDisposeNotifierProviderElement + with SelectedBangDataRef { + _SelectedBangDataProviderElement(super.provider); + + @override + String? get domain => (origin as SelectedBangDataProvider).domain; +} + String _$lastUsedAssistantModeHash() => r'1255a2754a3ea5ea058d110fd9743897fe103a69'; diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart index e2af4cfe..23c34fe9 100644 --- a/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart @@ -19,13 +19,17 @@ import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; +import 'package:lensai/features/geckoview/features/search/domain/providers/engine_suggestions.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; import 'package:lensai/features/user/domain/providers.dart'; +import 'package:lensai/presentation/hooks/listenable_callback.dart'; +import 'package:lensai/presentation/widgets/auto_suggest_text_field.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/website_title_tile.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; +import 'package:lensai/utils/uri_parser.dart' as uri_parser; import 'package:share_plus/share_plus.dart'; class WebPageDialog extends HookConsumerWidget { @@ -52,12 +56,8 @@ class WebPageDialog extends HookConsumerWidget { ), ), ); - final availableBangCount = availableBangsAsync.valueOrNull?.length; - final formKey = useMemoized(() => GlobalKey()); - final addressTextController = - useTextEditingController(text: url.toString()); - final addressTextFocusNode = useFocusNode(); + final availableBangCount = availableBangsAsync.valueOrNull?.length; return MediaQuery.removeViewInsets( context: context, @@ -77,42 +77,73 @@ class WebPageDialog extends HookConsumerWidget { width: double.maxFinite, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Form( - key: formKey, - child: TextFormField( - controller: addressTextController, - focusNode: addressTextFocusNode, - enableIMEPersonalizedLearning: !incognitoEnabled, - keyboardType: TextInputType.url, - decoration: const InputDecoration( - labelText: 'Address', - ), - validator: (value) { - if (value != null && Uri.tryParse(value) != null) { - return null; - } + child: HookConsumer( + builder: (context, ref, child) { + final addressTextController = + useTextEditingController(text: url.toString()); + final addressTextFocusNode = useFocusNode(); - return 'Invalid Address'; - }, - onTap: () { - if (!addressTextFocusNode.hasFocus) { - // Select all text when the field is tapped - addressTextController.selection = TextSelection( - baseOffset: 0, - extentOffset: addressTextController.text.length, - ); - } - }, - onFieldSubmitted: (value) async { - await ref - .read(tabSessionProvider(tabId: null).notifier) - .loadUrl(url: Uri.tryParse(value)!); + final suggestion = useState(null); - if (context.mounted) { - context.pop(); - } - }, - ), + useListenableCallback( + addressTextController, + () async { + if (addressTextController.text.isNotEmpty) { + final result = await ref + .read(engineSuggestionsProvider.notifier) + .getAutocompleteSuggestion( + addressTextController.text, + ); + + suggestion.value = result; + } + }, + ); + + return AutoSuggestTextField( + controller: addressTextController, + focusNode: addressTextFocusNode, + suggestion: suggestion.value, + enableIMEPersonalizedLearning: !incognitoEnabled, + keyboardType: TextInputType.url, + decoration: const InputDecoration( + label: Text('Address'), + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + onTap: () { + if (!addressTextFocusNode.hasFocus) { + // Select all text when the field is tapped + addressTextController.selection = TextSelection( + baseOffset: 0, + extentOffset: addressTextController.text.length, + ); + } + }, + onSubmitted: (value) async { + if (value.isNotEmpty) { + var newUrl = + uri_parser.tryParseUrl(value, eagerParsing: true); + + if (newUrl == null) { + final defaultSearchBang = await ref + .read(defaultSearchBangDataProvider.future); + + newUrl = defaultSearchBang?.getUrl(value); + } + + if (newUrl != null) { + await ref + .read(tabSessionProvider(tabId: null).notifier) + .loadUrl(url: newUrl); + + if (context.mounted) { + context.pop(); + } + } + } + }, + ); + }, ), ), ), diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index bdf95355..959d2d37 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -16,10 +16,10 @@ import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; -import 'package:lensai/features/geckoview/features/browser/domain/services/create_tab.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart'; @@ -125,57 +125,74 @@ class BrowserScreen extends HookConsumerWidget { extra: tabState, ); }, + onDoubleTap: () async { + final newUrl = await showDialog( + context: context, + builder: (context) => EditUrlDialog( + initialUrl: tabState.url, + ), + ); + + if (newUrl != null) { + await ref + .read( + tabSessionProvider(tabId: null) + .notifier, + ) + .loadUrl(url: newUrl); + } + }, ) : const SizedBox.shrink(); }, ) : HookBuilder( builder: (context) { - final activeTool = useListenableSelector( - activeKagiTool, - () { - if (displayedSheet is CreateTabSheetWidget) { - return null; - } + // final activeTool = useListenableSelector( + // activeKagiTool, + // () { + // if (displayedSheet is CreateTabSheetWidget) { + // return null; + // } - return activeKagiTool.value; - }, - ); + // return activeKagiTool.value; + // }, + // ); return Row( children: [ - IconButton( - color: (activeTool == KagiTool.search) - ? Theme.of(context).colorScheme.primary - : null, - onPressed: () async { - // ref - // .read(createTabStreamProvider.notifier) - // .createTab( - // CreateTabSheet( - // preferredTool: KagiTool.search, - // ), - // ); - await context - .push(const SearchRoute().location); - }, - icon: Icon(KagiTool.search.icon), - ), - IconButton( - color: (activeTool == KagiTool.summarizer) - ? Theme.of(context).colorScheme.primary - : null, - onPressed: () { - ref - .read(createTabStreamProvider.notifier) - .createTab( - CreateTabSheet( - preferredTool: KagiTool.summarizer, - ), - ); - }, - icon: Icon(KagiTool.summarizer.icon), - ), + // IconButton( + // color: (activeTool == KagiTool.search) + // ? Theme.of(context).colorScheme.primary + // : null, + // onPressed: () async { + // // ref + // // .read(createTabStreamProvider.notifier) + // // .createTab( + // // CreateTabSheet( + // // preferredTool: KagiTool.search, + // // ), + // // ); + // await context + // .push(const SearchRoute().location); + // }, + // icon: Icon(KagiTool.search.icon), + // ), + // IconButton( + // color: (activeTool == KagiTool.summarizer) + // ? Theme.of(context).colorScheme.primary + // : null, + // onPressed: () { + // ref + // .read(createTabStreamProvider.notifier) + // .createTab( + // CreateTabSheet( + // preferredTool: KagiTool.summarizer, + // ), + // ); + // }, + // icon: Icon(KagiTool.summarizer.icon), + // ), // if (showEarlyAccessFeatures) // IconButton( // color: (activeTool == KagiTool.assistant) @@ -199,7 +216,9 @@ class BrowserScreen extends HookConsumerWidget { }, ), actions: [ - if (selectedTabId != null) ReaderButton(), + if (selectedTabId != null && + displayedSheet is! ViewTabsSheet) + ReaderButton(), // if (quickAction != null) // InkWell( // onTap: () async { @@ -268,9 +287,7 @@ class BrowserScreen extends HookConsumerWidget { ), MenuItemButton( onPressed: () async { - await ref - .read(tabRepositoryProvider.notifier) - .addTab(url: Uri.https('kagi.com')); + await context.push(const SearchRoute().location); }, leadingIcon: const Icon(Icons.add), child: const Text('Add Tab'), @@ -586,14 +603,6 @@ class BrowserScreen extends HookConsumerWidget { leadingIcon: const Icon(Icons.info), child: const Text('Auth'), ), - MenuItemButton( - onPressed: () async { - await context - .push(WebEngineHardeningRoute().location); - }, - leadingIcon: const Icon(Icons.info), - child: const Text('Pref'), - ), ], ), ], diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart index 45545fbb..8e0950ae 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart @@ -8,8 +8,14 @@ class AppBarTitle extends StatelessWidget { final TabState tab; final void Function()? onTap; + final void Function()? onDoubleTap; - const AppBarTitle({required this.tab, this.onTap, super.key}); + const AppBarTitle({ + required this.tab, + this.onTap, + this.onDoubleTap, + super.key, + }); Icon _securityStatusIcon(BuildContext context) { if (tab.url.isScheme('http')) { @@ -42,6 +48,7 @@ class AppBarTitle extends StatelessWidget { final theme = Theme.of(context); return GestureDetector( onTap: onTap, + onDoubleTap: onDoubleTap, child: Row( children: [ Skeletonizer( diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart index f1b7f6b7..42ebb26a 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart @@ -5,6 +5,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/logger.dart'; import 'package:lensai/features/geckoview/domain/providers.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/domain/providers/web_extensions_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; @@ -49,6 +50,9 @@ class _BrowserViewState extends ConsumerState @override Widget build(BuildContext context) { + final hasTab = + ref.watch(selectedTabProvider.select((value) => value != null)); + ref.listen( selectedTabStateProvider.select( (state) => (tabId: state?.id, isLoading: state?.isLoading), @@ -69,34 +73,42 @@ class _BrowserViewState extends ConsumerState }, ); - return GeckoView( - preInitializationStep: () async { - await ref - .read(generalSettingsRepositoryProvider.notifier) - .fetch() - .then((settings) { - ref - .read(deleteBrowserDataServiceProvider.notifier) - .deleteData(settings.deleteBrowsingDataOnQuit); - }); + return Visibility( + visible: hasTab, + replacement: SizedBox.expand( + child: Container( + color: Colors.grey[800], + ), + ), + child: GeckoView( + preInitializationStep: () async { + await ref + .read(generalSettingsRepositoryProvider.notifier) + .fetch() + .then((settings) { + ref + .read(deleteBrowserDataServiceProvider.notifier) + .deleteData(settings.deleteBrowsingDataOnQuit); + }); - await ref - .read(eventServiceProvider) - .viewReadyStateEvents - .firstWhere((state) => state == true) - .timeout( - const Duration(seconds: 3), - onTimeout: () { - logger.e( - 'Browser fragement not reported ready, trying to intitialize anyways', - ); - return true; - }, - ); - }, - postInitializationStep: () async { - await widget.postInitializationStep?.call(); - }, + await ref + .read(eventServiceProvider) + .viewReadyStateEvents + .firstWhere((state) => state == true) + .timeout( + const Duration(seconds: 3), + onTimeout: () { + logger.e( + 'Browser fragement not reported ready, trying to intitialize anyways', + ); + return true; + }, + ); + }, + postInitializationStep: () async { + await widget.postInitializationStep?.call(); + }, + ), ); } diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart new file mode 100644 index 00000000..b19d3828 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:lensai/utils/uri_parser.dart' as uri_parser; + +class EditUrlDialog extends HookWidget { + final Uri initialUrl; + + const EditUrlDialog({required this.initialUrl}); + + @override + Widget build(BuildContext context) { + final formKey = useMemoized(() => GlobalKey()); + final urlController = useTextEditingController(text: initialUrl.toString()); + + return AlertDialog( + title: const Text('Edit Address'), + content: Form( + key: formKey, + child: TextFormField( + controller: urlController, + maxLines: null, + keyboardType: TextInputType.url, + decoration: const InputDecoration( + hintText: 'Enter URL', + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a URL'; + } + if (uri_parser.tryParseUrl(value) == null) { + return 'Please enter a valid URL'; + } + return null; + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + if (formKey.currentState?.validate() ?? false) { + Navigator.of(context).pop(Uri.parse(urlController.text)); + } + }, + child: const Text('Edit'), + ), + ], + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index 3a63a0d2..0fa6c528 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -4,8 +4,10 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_reorderable_grid_view/widgets/widgets.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/providers.dart'; +import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/data/models/drag_data.dart'; import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; @@ -428,10 +430,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget { ), child: FloatingActionButton.small( onPressed: () async { - await ref.read(tabRepositoryProvider.notifier).addTab( - url: Uri.https('setcookie.net'), - // contextId: uuid.v4(), - ); + await context.push(const SearchRoute().location); onClose(); }, 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 f6b14878..53cc1d85 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -12,6 +12,7 @@ import 'package:lensai/features/geckoview/features/search/presentation/widgets/s import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart'; import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/search_suggestions.dart'; import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart'; +import 'package:lensai/utils/uri_parser.dart' as uri_parser; class SearchScreen extends HookConsumerWidget { final String? initialSearchText; @@ -37,13 +38,11 @@ class SearchScreen extends HookConsumerWidget { ref.listen( selectedBangDataProvider(), (previous, next) { - if (next.hasValue) { - if ((previous?.hasValue ?? false) && previous!.value != next.value) { - showBangIcon.value = true; - } - - selectedBang.value = next.value; + if (previous != next) { + showBangIcon.value = true; } + + selectedBang.value = next; }, ); @@ -77,18 +76,31 @@ class SearchScreen extends HookConsumerWidget { textEditingController: searchTextController, focusNode: searchFocusNode, autofocus: true, - onSubmitted: (text) async { - if (Uri.tryParse(text) case final Uri url) { - await ref - .read(tabRepositoryProvider.notifier) - .addTab(url: url); + onSubmitted: (value) async { + if (value.isNotEmpty) { + var newUrl = + uri_parser.tryParseUrl(value, eagerParsing: true); - if (context.mounted) { - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); + if (newUrl == null) { + final defaultSearchBang = ref + .read(selectedBangDataProvider()) ?? + await ref.read(defaultSearchBangDataProvider.future); - context.pop(); + newUrl = defaultSearchBang?.getUrl(value); + } + + if (newUrl != null) { + await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: newUrl); + + if (context.mounted) { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + + context.pop(); + } } } }, diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart index 48ed2a9c..6e2388b9 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart @@ -12,6 +12,7 @@ import 'package:lensai/presentation/widgets/speech_to_text_button.dart'; class SearchField extends HookConsumerWidget { final TextEditingController textEditingController; final FocusNode? focusNode; + final Widget? label; final bool autofocus; final void Function(String)? onSubmitted; final bool showSuggestions; @@ -25,6 +26,7 @@ class SearchField extends HookConsumerWidget { required this.onSubmitted, required this.activeBang, required this.showSuggestions, + this.label, this.focusNode, this.showBangIcon = true, this.autofocus = false, @@ -70,7 +72,7 @@ class SearchField extends HookConsumerWidget { child: UrlIcon(activeBang!.getUrl(''), iconSize: 24.0), ) : null, - label: const Text('Search'), + label: label ?? const Text('Search'), // hintText: 'Ask anything...', floatingLabelBehavior: FloatingLabelBehavior.always, suffixIcon: hasText 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 2ea52bf6..05bab9e9 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 @@ -81,8 +81,8 @@ class HistorySuggestions extends HookConsumerWidget { subtitle: suggestion.description.mapNotNull( (description) => Text( description, - softWrap: false, - overflow: TextOverflow.fade, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), onTap: () async { diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart index 5c2fd794..f0b98a1b 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.dart @@ -118,20 +118,30 @@ class TabSearch extends HookConsumerWidget { ), ), ), - subtitle: MarkdownBody( - data: (bodyHasMatch && !urlHasMatch) - ? result.content! - : result.highlightedUrl ?? result.url.toString(), - styleSheet: MarkdownStyleSheet( - p: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + subtitle: (bodyHasMatch || urlHasMatch) + ? MarkdownBody( + data: bodyHasMatch + ? result.content! + : result.highlightedUrl!, + styleSheet: MarkdownStyleSheet( + p: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + a: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + decoration: TextDecoration.none, + ), ), - a: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - decoration: TextDecoration.none, - ), - ), - ), + ) + : Text( + result.url.toString(), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), onTap: () async { await ref .read(tabRepositoryProvider.notifier) diff --git a/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart index c02dee16..6ec0a0c5 100644 --- a/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart +++ b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart @@ -30,7 +30,7 @@ class QAMemoryChain extends _$QAMemoryChain { contextId: contextId, ).asRetriever( defaultOptions: const VectorStoreRetrieverOptions( - searchType: VectorStoreSimilaritySearch(k: 3), + searchType: VectorStoreSimilaritySearch(k: 6), ), ); diff --git a/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.g.dart b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.g.dart index 500c320e..69271d12 100644 --- a/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.g.dart +++ b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.g.dart @@ -6,7 +6,7 @@ part of 'qa_memory_chain.dart'; // RiverpodGenerator // ************************************************************************** -String _$qAMemoryChainHash() => r'd5efc3fc01a6a88c67164d7eefc06412c9ab7c5c'; +String _$qAMemoryChainHash() => r'f05ff182416cd8ac8c35f046081652f8964684a8'; /// Copied from Dart SDK class _SystemHash { diff --git a/app/lib/features/settings/presentation/screens/bang_settings.dart b/app/lib/features/settings/presentation/screens/bang_settings.dart index bed86bc7..0ffa6292 100644 --- a/app/lib/features/settings/presentation/screens/bang_settings.dart +++ b/app/lib/features/settings/presentation/screens/bang_settings.dart @@ -49,6 +49,11 @@ class BangSettingsScreen extends HookConsumerWidget { title: 'Kagi Bangs', subtitle: 'Automatically syncs every 7 days', ), + const BangGroupListTile( + group: BangGroup.custom, + title: 'Lensai Bangs', + subtitle: 'Automatically syncs every 7 days', + ), ], ); }, diff --git a/app/lib/presentation/widgets/auto_suggest_text_field.dart b/app/lib/presentation/widgets/auto_suggest_text_field.dart index 5f49acec..c7d746a0 100644 --- a/app/lib/presentation/widgets/auto_suggest_text_field.dart +++ b/app/lib/presentation/widgets/auto_suggest_text_field.dart @@ -27,6 +27,7 @@ class AutoSuggestTextField extends HookWidget { final Color? suggestionHighlightColor; final bool? enableIMEPersonalizedLearning; final TapRegionCallback? onTapOutside; + final VoidCallback? onTap; const AutoSuggestTextField({ super.key, @@ -53,6 +54,7 @@ class AutoSuggestTextField extends HookWidget { this.suggestionHighlightColor, this.enableIMEPersonalizedLearning = true, this.onTapOutside, + this.onTap, }); bool _suggestionHasMatch() => @@ -69,8 +71,12 @@ class AutoSuggestTextField extends HookWidget { if (suggestion != null) AbsorbPointer( child: TextField( + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, decoration: baseDecoration.copyWith( floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, label: HookBuilder( builder: (context) { final text = useListenableSelector( @@ -107,6 +113,7 @@ class AutoSuggestTextField extends HookWidget { } return RichText( + maxLines: maxLines, text: TextSpan( text: text, style: (style ?? Theme.of(context).textTheme.bodyLarge) @@ -168,6 +175,7 @@ class AutoSuggestTextField extends HookWidget { cursorColor: cursorColor, enableIMEPersonalizedLearning: enableIMEPersonalizedLearning ?? true, onTapOutside: onTapOutside, + onTap: onTap, ), ], ); diff --git a/app/lib/utils/uri_parser.dart b/app/lib/utils/uri_parser.dart index e710b5d2..94113f5e 100644 --- a/app/lib/utils/uri_parser.dart +++ b/app/lib/utils/uri_parser.dart @@ -1,20 +1,28 @@ +final _domainRegex = RegExp(r'\.[a-zA-Z]{2,}$'); +const _supportedSchemes = {'https', 'http', 'ftp', 'file', 'about'}; + Uri? tryParseUrl(String? input, {bool eagerParsing = false}) { if (input != null) { var uri = Uri.tryParse(input); if (uri != null) { if (uri.authority.isEmpty && eagerParsing) { - //When there is no scheme, there will be no authority/host and all becomes a path - //so we make sure there are at least 2 segments where the first one looks like a domain - if (uri.pathSegments.length > 1 && - RegExp(r'.[a-z]{2,}$').hasMatch(uri.pathSegments.first)) { - uri = Uri.tryParse('https://$input'); + if (uri.pathSegments.isNotEmpty) { + if (_domainRegex.hasMatch(uri.pathSegments.first)) { + uri = Uri.tryParse('https://$input'); + } } } - if (uri != null && - uri.authority.isNotEmpty && - (uri.isScheme('http') || uri.isScheme('https') || !uri.hasScheme)) { - return uri; + if (uri != null) { + if (uri.isScheme('about')) { + return uri; + } + + if (uri.authority.isNotEmpty) { + if (_supportedSchemes.contains(uri.scheme) || !uri.hasScheme) { + return uri; + } + } } } }