bang and search
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -17,7 +17,7 @@ Stream<BangData?> bangData(Ref ref, String? trigger) {
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<BangData?> defaultSearchBangData(Ref ref) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
return repository.watchBang('kagi');
|
||||
return repository.watchBang('lai');
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
|
||||
@@ -157,7 +157,7 @@ class _BangDataProviderElement
|
||||
}
|
||||
|
||||
String _$defaultSearchBangDataHash() =>
|
||||
r'b7547084b261ccb1dc02a6c7230fe57f6902dd58';
|
||||
r'c93026fe7b1650fe8a39bb2a8850af3ee416c306';
|
||||
|
||||
/// See also [defaultSearchBangData].
|
||||
@ProviderFor(defaultSearchBangData)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -36,11 +36,25 @@ class SelectedBangTrigger extends _$SelectedBangTrigger {
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<BangData?> selectedBangData(Ref ref, {String? domain}) {
|
||||
class SelectedBangData extends _$SelectedBangData {
|
||||
@override
|
||||
BangData? build({String? domain}) {
|
||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||
final selectedBangTrigger =
|
||||
ref.watch(selectedBangTriggerProvider(domain: domain));
|
||||
return repository.watchBang(selectedBangTrigger);
|
||||
|
||||
final subscription = repository.watchBang(selectedBangTrigger).listen(
|
||||
(value) {
|
||||
state = value;
|
||||
},
|
||||
);
|
||||
|
||||
ref.onDispose(() {
|
||||
unawaited(subscription.cancel());
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
|
||||
@@ -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<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].
|
||||
@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<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() =>
|
||||
r'1255a2754a3ea5ea058d110fd9743897fe103a69';
|
||||
|
||||
|
||||
+50
-19
@@ -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<FormState>());
|
||||
final addressTextController =
|
||||
useTextEditingController(text: url.toString());
|
||||
final addressTextFocusNode = useFocusNode();
|
||||
final availableBangCount = availableBangsAsync.valueOrNull?.length;
|
||||
|
||||
return MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
@@ -77,23 +77,39 @@ class WebPageDialog extends HookConsumerWidget {
|
||||
width: double.maxFinite,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
child: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final addressTextController =
|
||||
useTextEditingController(text: url.toString());
|
||||
final addressTextFocusNode = useFocusNode();
|
||||
|
||||
final suggestion = useState<String?>(null);
|
||||
|
||||
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(
|
||||
labelText: 'Address',
|
||||
label: Text('Address'),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value != null && Uri.tryParse(value) != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'Invalid Address';
|
||||
},
|
||||
onTap: () {
|
||||
if (!addressTextFocusNode.hasFocus) {
|
||||
// Select all text when the field is tapped
|
||||
@@ -103,16 +119,31 @@ class WebPageDialog extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
onFieldSubmitted: (value) async {
|
||||
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: Uri.tryParse(value)!);
|
||||
.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/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<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();
|
||||
},
|
||||
)
|
||||
: 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 {
|
||||
// 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.search,
|
||||
// preferredTool: KagiTool.summarizer,
|
||||
// ),
|
||||
// );
|
||||
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),
|
||||
),
|
||||
// },
|
||||
// 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<BrowserView>
|
||||
|
||||
@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,7 +73,14 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
},
|
||||
);
|
||||
|
||||
return GeckoView(
|
||||
return Visibility(
|
||||
visible: hasTab,
|
||||
replacement: SizedBox.expand(
|
||||
child: Container(
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
child: GeckoView(
|
||||
preInitializationStep: () async {
|
||||
await ref
|
||||
.read(generalSettingsRepositoryProvider.notifier)
|
||||
@@ -97,6 +108,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
postInitializationStep: () async {
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
if (previous != next) {
|
||||
showBangIcon.value = true;
|
||||
}
|
||||
|
||||
selectedBang.value = next.value;
|
||||
}
|
||||
selectedBang.value = next;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -77,11 +76,23 @@ class SearchScreen extends HookConsumerWidget {
|
||||
textEditingController: searchTextController,
|
||||
focusNode: searchFocusNode,
|
||||
autofocus: true,
|
||||
onSubmitted: (text) async {
|
||||
if (Uri.tryParse(text) case final Uri url) {
|
||||
onSubmitted: (value) async {
|
||||
if (value.isNotEmpty) {
|
||||
var newUrl =
|
||||
uri_parser.tryParseUrl(value, eagerParsing: true);
|
||||
|
||||
if (newUrl == null) {
|
||||
final defaultSearchBang = ref
|
||||
.read(selectedBangDataProvider()) ??
|
||||
await ref.read(defaultSearchBangDataProvider.future);
|
||||
|
||||
newUrl = defaultSearchBang?.getUrl(value);
|
||||
}
|
||||
|
||||
if (newUrl != null) {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(url: url);
|
||||
.addTab(url: newUrl);
|
||||
|
||||
if (context.mounted) {
|
||||
ref
|
||||
@@ -91,6 +102,7 @@ class SearchScreen extends HookConsumerWidget {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
activeBang: activeBang,
|
||||
showSuggestions: true,
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+15
-5
@@ -118,19 +118,29 @@ class TabSearch extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: MarkdownBody(
|
||||
data: (bodyHasMatch && !urlHasMatch)
|
||||
subtitle: (bodyHasMatch || urlHasMatch)
|
||||
? MarkdownBody(
|
||||
data: bodyHasMatch
|
||||
? result.content!
|
||||
: result.highlightedUrl ?? result.url.toString(),
|
||||
: result.highlightedUrl!,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
a: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
result.url.toString(),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () async {
|
||||
await ref
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class QAMemoryChain extends _$QAMemoryChain {
|
||||
contextId: contextId,
|
||||
).asRetriever(
|
||||
defaultOptions: const VectorStoreRetrieverOptions(
|
||||
searchType: VectorStoreSimilaritySearch(k: 3),
|
||||
searchType: VectorStoreSimilaritySearch(k: 6),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
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)) {
|
||||
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)) {
|
||||
if (uri != null) {
|
||||
if (uri.isScheme('about')) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
if (uri.authority.isNotEmpty) {
|
||||
if (_supportedSchemes.contains(uri.scheme) || !uri.hasScheme) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user