bang and search

This commit is contained in:
Fabian Freund
2025-02-13 14:29:37 +01:00
parent fe4baa58f5
commit e09219e07c
21 changed files with 499 additions and 314 deletions
+6 -3
View File
@@ -9,13 +9,16 @@ part 'bang.g.dart';
enum BangGroup { enum BangGroup {
general( general(
'https://raw.githubusercontent.com/kagisearch/bangs/main/data/bangs.json', 'https://raw.githubusercontent.com/FaFre/bangs/main/data/bangs.json',
), ),
assistant( assistant(
'https://raw.githubusercontent.com/kagisearch/bangs/main/data/assistant_bangs.json', 'https://raw.githubusercontent.com/FaFre/bangs/main/data/assistant_bangs.json',
), ),
kagi( 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; final String url;
@@ -17,7 +17,7 @@ Stream<BangData?> bangData(Ref ref, String? trigger) {
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
Stream<BangData?> defaultSearchBangData(Ref ref) { Stream<BangData?> defaultSearchBangData(Ref ref) {
final repository = ref.watch(bangDataRepositoryProvider.notifier); final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBang('kagi'); return repository.watchBang('lai');
} }
@Riverpod() @Riverpod()
@@ -157,7 +157,7 @@ class _BangDataProviderElement
} }
String _$defaultSearchBangDataHash() => String _$defaultSearchBangDataHash() =>
r'b7547084b261ccb1dc02a6c7230fe57f6902dd58'; r'c93026fe7b1650fe8a39bb2a8850af3ee416c306';
/// See also [defaultSearchBangData]. /// See also [defaultSearchBangData].
@ProviderFor(defaultSearchBangData) @ProviderFor(defaultSearchBangData)
@@ -39,10 +39,7 @@ class SiteSearch extends HookConsumerWidget {
}, },
); );
final selectedBang = ref.watch( final selectedBang = ref.watch(selectedBangDataProvider(domain: domain));
selectedBangDataProvider(domain: domain)
.select((value) => value.valueOrNull),
);
final activeBang = selectedBang ?? availableBangs.firstOrNull; final activeBang = selectedBang ?? availableBangs.firstOrNull;
@@ -36,11 +36,25 @@ class SelectedBangTrigger extends _$SelectedBangTrigger {
} }
@Riverpod() @Riverpod()
Stream<BangData?> selectedBangData(Ref ref, {String? domain}) { class SelectedBangData extends _$SelectedBangData {
final repository = ref.watch(bangDataRepositoryProvider.notifier); @override
final selectedBangTrigger = BangData? build({String? domain}) {
ref.watch(selectedBangTriggerProvider(domain: domain)); final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBang(selectedBangTrigger); 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) @Riverpod(keepAlive: true)
@@ -6,7 +6,7 @@ part of 'providers.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$selectedBangDataHash() => r'd43e679c40d35c4f4c692ee286dc21789d7287dd'; String _$availableTabIdsHash() => r'd8b55b012b38111c8d5e069987a55645d1a71b89';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -29,137 +29,6 @@ class _SystemHash {
} }
} }
/// See also [selectedBangData].
@ProviderFor(selectedBangData)
const selectedBangDataProvider = SelectedBangDataFamily();
/// See also [selectedBangData].
class SelectedBangDataFamily extends Family<AsyncValue<BangData?>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'selectedBangDataProvider';
}
/// See also [selectedBangData].
class SelectedBangDataProvider extends AutoDisposeStreamProvider<BangData?> {
/// 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<BangData?> 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<BangData?> 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<BangData?> {
/// The parameter `domain` of this provider.
String? get domain;
}
class _SelectedBangDataProviderElement
extends AutoDisposeStreamProviderElement<BangData?>
with SelectedBangDataRef {
_SelectedBangDataProviderElement(super.provider);
@override
String? get domain => (origin as SelectedBangDataProvider).domain;
}
String _$availableTabIdsHash() => r'd8b55b012b38111c8d5e069987a55645d1a71b89';
/// See also [availableTabIds]. /// See also [availableTabIds].
@ProviderFor(availableTabIds) @ProviderFor(availableTabIds)
const availableTabIdsProvider = AvailableTabIdsFamily(); const availableTabIdsProvider = AvailableTabIdsFamily();
@@ -893,6 +762,152 @@ class _SelectedBangTriggerProviderElement
String? get domain => (origin as SelectedBangTriggerProvider).domain; String? get domain => (origin as SelectedBangTriggerProvider).domain;
} }
String _$selectedBangDataHash() => r'28c394e5989e108870ca5733e916b80b3cf8972c';
abstract class _$SelectedBangData
extends BuildlessAutoDisposeNotifier<BangData?> {
late final String? domain;
BangData? build({
String? domain,
});
}
/// See also [SelectedBangData].
@ProviderFor(SelectedBangData)
const selectedBangDataProvider = SelectedBangDataFamily();
/// See also [SelectedBangData].
class SelectedBangDataFamily extends Family<BangData?> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'selectedBangDataProvider';
}
/// See also [SelectedBangData].
class SelectedBangDataProvider
extends AutoDisposeNotifierProviderImpl<SelectedBangData, BangData?> {
/// 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<SelectedBangData, BangData?>
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<BangData?> {
/// The parameter `domain` of this provider.
String? get domain;
}
class _SelectedBangDataProviderElement
extends AutoDisposeNotifierProviderElement<SelectedBangData, BangData?>
with SelectedBangDataRef {
_SelectedBangDataProviderElement(super.provider);
@override
String? get domain => (origin as SelectedBangDataProvider).domain;
}
String _$lastUsedAssistantModeHash() => String _$lastUsedAssistantModeHash() =>
r'1255a2754a3ea5ea058d110fd9743897fe103a69'; r'1255a2754a3ea5ea058d110fd9743897fe103a69';
@@ -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/providers/tab_state.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.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/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/data/entities/modes.dart';
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; 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/share_intent/domain/entities/shared_content.dart';
import 'package:lensai/features/user/domain/providers.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/failure_widget.dart';
import 'package:lensai/presentation/widgets/website_title_tile.dart'; import 'package:lensai/presentation/widgets/website_title_tile.dart';
import 'package:lensai/utils/ui_helper.dart' as ui_helper; 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'; import 'package:share_plus/share_plus.dart';
class WebPageDialog extends HookConsumerWidget { class WebPageDialog extends HookConsumerWidget {
@@ -52,12 +56,8 @@ class WebPageDialog extends HookConsumerWidget {
), ),
), ),
); );
final availableBangCount = availableBangsAsync.valueOrNull?.length;
final formKey = useMemoized(() => GlobalKey<FormState>()); final availableBangCount = availableBangsAsync.valueOrNull?.length;
final addressTextController =
useTextEditingController(text: url.toString());
final addressTextFocusNode = useFocusNode();
return MediaQuery.removeViewInsets( return MediaQuery.removeViewInsets(
context: context, context: context,
@@ -77,42 +77,73 @@ class WebPageDialog extends HookConsumerWidget {
width: double.maxFinite, width: double.maxFinite,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Form( child: HookConsumer(
key: formKey, builder: (context, ref, child) {
child: TextFormField( final addressTextController =
controller: addressTextController, useTextEditingController(text: url.toString());
focusNode: addressTextFocusNode, final addressTextFocusNode = useFocusNode();
enableIMEPersonalizedLearning: !incognitoEnabled,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'Address',
),
validator: (value) {
if (value != null && Uri.tryParse(value) != null) {
return null;
}
return 'Invalid Address'; final suggestion = useState<String?>(null);
},
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)!);
if (context.mounted) { useListenableCallback(
context.pop(); 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();
}
}
}
},
);
},
), ),
), ),
), ),
@@ -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/providers/web_extensions_state.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.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/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/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/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/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/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/create_tab.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
@@ -125,57 +125,74 @@ class BrowserScreen extends HookConsumerWidget {
extra: tabState, extra: tabState,
); );
}, },
onDoubleTap: () async {
final newUrl = await showDialog<Uri?>(
context: context,
builder: (context) => EditUrlDialog(
initialUrl: tabState.url,
),
);
if (newUrl != null) {
await ref
.read(
tabSessionProvider(tabId: null)
.notifier,
)
.loadUrl(url: newUrl);
}
},
) )
: const SizedBox.shrink(); : const SizedBox.shrink();
}, },
) )
: HookBuilder( : HookBuilder(
builder: (context) { builder: (context) {
final activeTool = useListenableSelector( // final activeTool = useListenableSelector(
activeKagiTool, // activeKagiTool,
() { // () {
if (displayedSheet is CreateTabSheetWidget) { // if (displayedSheet is CreateTabSheetWidget) {
return null; // return null;
} // }
return activeKagiTool.value; // return activeKagiTool.value;
}, // },
); // );
return Row( return Row(
children: [ children: [
IconButton( // IconButton(
color: (activeTool == KagiTool.search) // color: (activeTool == KagiTool.search)
? Theme.of(context).colorScheme.primary // ? Theme.of(context).colorScheme.primary
: null, // : null,
onPressed: () async { // onPressed: () async {
// ref // // ref
// .read(createTabStreamProvider.notifier) // // .read(createTabStreamProvider.notifier)
// .createTab( // // .createTab(
// CreateTabSheet( // // CreateTabSheet(
// preferredTool: KagiTool.search, // // preferredTool: KagiTool.search,
// ), // // ),
// ); // // );
await context // await context
.push(const SearchRoute().location); // .push(const SearchRoute().location);
}, // },
icon: Icon(KagiTool.search.icon), // icon: Icon(KagiTool.search.icon),
), // ),
IconButton( // IconButton(
color: (activeTool == KagiTool.summarizer) // color: (activeTool == KagiTool.summarizer)
? Theme.of(context).colorScheme.primary // ? Theme.of(context).colorScheme.primary
: null, // : null,
onPressed: () { // onPressed: () {
ref // ref
.read(createTabStreamProvider.notifier) // .read(createTabStreamProvider.notifier)
.createTab( // .createTab(
CreateTabSheet( // CreateTabSheet(
preferredTool: KagiTool.summarizer, // preferredTool: KagiTool.summarizer,
), // ),
); // );
}, // },
icon: Icon(KagiTool.summarizer.icon), // icon: Icon(KagiTool.summarizer.icon),
), // ),
// if (showEarlyAccessFeatures) // if (showEarlyAccessFeatures)
// IconButton( // IconButton(
// color: (activeTool == KagiTool.assistant) // color: (activeTool == KagiTool.assistant)
@@ -199,7 +216,9 @@ class BrowserScreen extends HookConsumerWidget {
}, },
), ),
actions: [ actions: [
if (selectedTabId != null) ReaderButton(), if (selectedTabId != null &&
displayedSheet is! ViewTabsSheet)
ReaderButton(),
// if (quickAction != null) // if (quickAction != null)
// InkWell( // InkWell(
// onTap: () async { // onTap: () async {
@@ -268,9 +287,7 @@ class BrowserScreen extends HookConsumerWidget {
), ),
MenuItemButton( MenuItemButton(
onPressed: () async { onPressed: () async {
await ref await context.push(const SearchRoute().location);
.read(tabRepositoryProvider.notifier)
.addTab(url: Uri.https('kagi.com'));
}, },
leadingIcon: const Icon(Icons.add), leadingIcon: const Icon(Icons.add),
child: const Text('Add Tab'), child: const Text('Add Tab'),
@@ -586,14 +603,6 @@ class BrowserScreen extends HookConsumerWidget {
leadingIcon: const Icon(Icons.info), leadingIcon: const Icon(Icons.info),
child: const Text('Auth'), child: const Text('Auth'),
), ),
MenuItemButton(
onPressed: () async {
await context
.push(WebEngineHardeningRoute().location);
},
leadingIcon: const Icon(Icons.info),
child: const Text('Pref'),
),
], ],
), ),
], ],
@@ -8,8 +8,14 @@ class AppBarTitle extends StatelessWidget {
final TabState tab; final TabState tab;
final void Function()? onTap; 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) { Icon _securityStatusIcon(BuildContext context) {
if (tab.url.isScheme('http')) { if (tab.url.isScheme('http')) {
@@ -42,6 +48,7 @@ class AppBarTitle extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
onDoubleTap: onDoubleTap,
child: Row( child: Row(
children: [ children: [
Skeletonizer( Skeletonizer(
@@ -5,6 +5,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/logger.dart'; import 'package:lensai/core/logger.dart';
import 'package:lensai/features/geckoview/domain/providers.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/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/web_extensions_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/domain/repositories/tab.dart';
@@ -49,6 +50,9 @@ class _BrowserViewState extends ConsumerState<BrowserView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasTab =
ref.watch(selectedTabProvider.select((value) => value != null));
ref.listen( ref.listen(
selectedTabStateProvider.select( selectedTabStateProvider.select(
(state) => (tabId: state?.id, isLoading: state?.isLoading), (state) => (tabId: state?.id, isLoading: state?.isLoading),
@@ -69,34 +73,42 @@ class _BrowserViewState extends ConsumerState<BrowserView>
}, },
); );
return GeckoView( return Visibility(
preInitializationStep: () async { visible: hasTab,
await ref replacement: SizedBox.expand(
.read(generalSettingsRepositoryProvider.notifier) child: Container(
.fetch() color: Colors.grey[800],
.then((settings) { ),
ref ),
.read(deleteBrowserDataServiceProvider.notifier) child: GeckoView(
.deleteData(settings.deleteBrowsingDataOnQuit); preInitializationStep: () async {
}); await ref
.read(generalSettingsRepositoryProvider.notifier)
.fetch()
.then((settings) {
ref
.read(deleteBrowserDataServiceProvider.notifier)
.deleteData(settings.deleteBrowsingDataOnQuit);
});
await ref await ref
.read(eventServiceProvider) .read(eventServiceProvider)
.viewReadyStateEvents .viewReadyStateEvents
.firstWhere((state) => state == true) .firstWhere((state) => state == true)
.timeout( .timeout(
const Duration(seconds: 3), const Duration(seconds: 3),
onTimeout: () { onTimeout: () {
logger.e( logger.e(
'Browser fragement not reported ready, trying to intitialize anyways', 'Browser fragement not reported ready, trying to intitialize anyways',
); );
return true; return true;
}, },
); );
}, },
postInitializationStep: () async { postInitializationStep: () async {
await widget.postInitializationStep?.call(); await widget.postInitializationStep?.call();
}, },
),
); );
} }
@@ -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<FormState>());
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'),
),
],
);
}
}
@@ -4,8 +4,10 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_reorderable_grid_view/widgets/widgets.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/providers.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/data/models/drag_data.dart';
import 'package:lensai/features/geckoview/domain/providers/selected_tab.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/tab_state.dart';
@@ -428,10 +430,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
), ),
child: FloatingActionButton.small( child: FloatingActionButton.small(
onPressed: () async { onPressed: () async {
await ref.read(tabRepositoryProvider.notifier).addTab( await context.push(const SearchRoute().location);
url: Uri.https('setcookie.net'),
// contextId: uuid.v4(),
);
onClose(); onClose();
}, },
@@ -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/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/search_suggestions.dart';
import 'package:lensai/features/geckoview/features/search/presentation/widgets/search_modules/tab_search.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 { class SearchScreen extends HookConsumerWidget {
final String? initialSearchText; final String? initialSearchText;
@@ -37,13 +38,11 @@ class SearchScreen extends HookConsumerWidget {
ref.listen( ref.listen(
selectedBangDataProvider(), selectedBangDataProvider(),
(previous, next) { (previous, next) {
if (next.hasValue) { if (previous != next) {
if ((previous?.hasValue ?? false) && previous!.value != next.value) { showBangIcon.value = true;
showBangIcon.value = true;
}
selectedBang.value = next.value;
} }
selectedBang.value = next;
}, },
); );
@@ -77,18 +76,31 @@ class SearchScreen extends HookConsumerWidget {
textEditingController: searchTextController, textEditingController: searchTextController,
focusNode: searchFocusNode, focusNode: searchFocusNode,
autofocus: true, autofocus: true,
onSubmitted: (text) async { onSubmitted: (value) async {
if (Uri.tryParse(text) case final Uri url) { if (value.isNotEmpty) {
await ref var newUrl =
.read(tabRepositoryProvider.notifier) uri_parser.tryParseUrl(value, eagerParsing: true);
.addTab(url: url);
if (context.mounted) { if (newUrl == null) {
ref final defaultSearchBang = ref
.read(bottomSheetControllerProvider.notifier) .read(selectedBangDataProvider()) ??
.dismiss(); 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();
}
} }
} }
}, },
@@ -12,6 +12,7 @@ import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
class SearchField extends HookConsumerWidget { class SearchField extends HookConsumerWidget {
final TextEditingController textEditingController; final TextEditingController textEditingController;
final FocusNode? focusNode; final FocusNode? focusNode;
final Widget? label;
final bool autofocus; final bool autofocus;
final void Function(String)? onSubmitted; final void Function(String)? onSubmitted;
final bool showSuggestions; final bool showSuggestions;
@@ -25,6 +26,7 @@ class SearchField extends HookConsumerWidget {
required this.onSubmitted, required this.onSubmitted,
required this.activeBang, required this.activeBang,
required this.showSuggestions, required this.showSuggestions,
this.label,
this.focusNode, this.focusNode,
this.showBangIcon = true, this.showBangIcon = true,
this.autofocus = false, this.autofocus = false,
@@ -70,7 +72,7 @@ class SearchField extends HookConsumerWidget {
child: UrlIcon(activeBang!.getUrl(''), iconSize: 24.0), child: UrlIcon(activeBang!.getUrl(''), iconSize: 24.0),
) )
: null, : null,
label: const Text('Search'), label: label ?? const Text('Search'),
// hintText: 'Ask anything...', // hintText: 'Ask anything...',
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: hasText suffixIcon: hasText
@@ -81,8 +81,8 @@ class HistorySuggestions extends HookConsumerWidget {
subtitle: suggestion.description.mapNotNull( subtitle: suggestion.description.mapNotNull(
(description) => Text( (description) => Text(
description, description,
softWrap: false, maxLines: 2,
overflow: TextOverflow.fade, overflow: TextOverflow.ellipsis,
), ),
), ),
onTap: () async { onTap: () async {
@@ -118,20 +118,30 @@ class TabSearch extends HookConsumerWidget {
), ),
), ),
), ),
subtitle: MarkdownBody( subtitle: (bodyHasMatch || urlHasMatch)
data: (bodyHasMatch && !urlHasMatch) ? MarkdownBody(
? result.content! data: bodyHasMatch
: result.highlightedUrl ?? result.url.toString(), ? result.content!
styleSheet: MarkdownStyleSheet( : result.highlightedUrl!,
p: Theme.of(context).textTheme.bodyMedium?.copyWith( styleSheet: MarkdownStyleSheet(
color: Theme.of(context).colorScheme.onSurfaceVariant, 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, : Text(
decoration: TextDecoration.none, result.url.toString(),
), maxLines: 2,
), overflow: TextOverflow.ellipsis,
), ),
onTap: () async { onTap: () async {
await ref await ref
.read(tabRepositoryProvider.notifier) .read(tabRepositoryProvider.notifier)
@@ -30,7 +30,7 @@ class QAMemoryChain extends _$QAMemoryChain {
contextId: contextId, contextId: contextId,
).asRetriever( ).asRetriever(
defaultOptions: const VectorStoreRetrieverOptions( defaultOptions: const VectorStoreRetrieverOptions(
searchType: VectorStoreSimilaritySearch(k: 3), searchType: VectorStoreSimilaritySearch(k: 6),
), ),
); );
@@ -6,7 +6,7 @@ part of 'qa_memory_chain.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$qAMemoryChainHash() => r'd5efc3fc01a6a88c67164d7eefc06412c9ab7c5c'; String _$qAMemoryChainHash() => r'f05ff182416cd8ac8c35f046081652f8964684a8';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -49,6 +49,11 @@ class BangSettingsScreen extends HookConsumerWidget {
title: 'Kagi Bangs', title: 'Kagi Bangs',
subtitle: 'Automatically syncs every 7 days', subtitle: 'Automatically syncs every 7 days',
), ),
const BangGroupListTile(
group: BangGroup.custom,
title: 'Lensai Bangs',
subtitle: 'Automatically syncs every 7 days',
),
], ],
); );
}, },
@@ -27,6 +27,7 @@ class AutoSuggestTextField extends HookWidget {
final Color? suggestionHighlightColor; final Color? suggestionHighlightColor;
final bool? enableIMEPersonalizedLearning; final bool? enableIMEPersonalizedLearning;
final TapRegionCallback? onTapOutside; final TapRegionCallback? onTapOutside;
final VoidCallback? onTap;
const AutoSuggestTextField({ const AutoSuggestTextField({
super.key, super.key,
@@ -53,6 +54,7 @@ class AutoSuggestTextField extends HookWidget {
this.suggestionHighlightColor, this.suggestionHighlightColor,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.onTapOutside, this.onTapOutside,
this.onTap,
}); });
bool _suggestionHasMatch() => bool _suggestionHasMatch() =>
@@ -69,8 +71,12 @@ class AutoSuggestTextField extends HookWidget {
if (suggestion != null) if (suggestion != null)
AbsorbPointer( AbsorbPointer(
child: TextField( child: TextField(
minLines: minLines,
maxLines: maxLines,
maxLength: maxLength,
decoration: baseDecoration.copyWith( decoration: baseDecoration.copyWith(
floatingLabelBehavior: FloatingLabelBehavior.never, floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
label: HookBuilder( label: HookBuilder(
builder: (context) { builder: (context) {
final text = useListenableSelector( final text = useListenableSelector(
@@ -107,6 +113,7 @@ class AutoSuggestTextField extends HookWidget {
} }
return RichText( return RichText(
maxLines: maxLines,
text: TextSpan( text: TextSpan(
text: text, text: text,
style: (style ?? Theme.of(context).textTheme.bodyLarge) style: (style ?? Theme.of(context).textTheme.bodyLarge)
@@ -168,6 +175,7 @@ class AutoSuggestTextField extends HookWidget {
cursorColor: cursorColor, cursorColor: cursorColor,
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning ?? true, enableIMEPersonalizedLearning: enableIMEPersonalizedLearning ?? true,
onTapOutside: onTapOutside, onTapOutside: onTapOutside,
onTap: onTap,
), ),
], ],
); );
+17 -9
View File
@@ -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}) { Uri? tryParseUrl(String? input, {bool eagerParsing = false}) {
if (input != null) { if (input != null) {
var uri = Uri.tryParse(input); var uri = Uri.tryParse(input);
if (uri != null) { if (uri != null) {
if (uri.authority.isEmpty && eagerParsing) { if (uri.authority.isEmpty && eagerParsing) {
//When there is no scheme, there will be no authority/host and all becomes a path if (uri.pathSegments.isNotEmpty) {
//so we make sure there are at least 2 segments where the first one looks like a domain if (_domainRegex.hasMatch(uri.pathSegments.first)) {
if (uri.pathSegments.length > 1 && uri = Uri.tryParse('https://$input');
RegExp(r'.[a-z]{2,}$').hasMatch(uri.pathSegments.first)) { }
uri = Uri.tryParse('https://$input');
} }
} }
if (uri != null && if (uri != null) {
uri.authority.isNotEmpty && if (uri.isScheme('about')) {
(uri.isScheme('http') || uri.isScheme('https') || !uri.hasScheme)) { return uri;
return uri; }
if (uri.authority.isNotEmpty) {
if (_supportedSchemes.contains(uri.scheme) || !uri.hasScheme) {
return uri;
}
}
} }
} }
} }