refactored bang search, added search with bang to title

This commit is contained in:
Fabian Freund
2024-06-16 06:30:22 +02:00
parent e2b2a46733
commit 2993ca59ea
10 changed files with 530 additions and 320 deletions
@@ -28,11 +28,26 @@ class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
return select(db.bangDataView)..where((t) => t.trigger.equals(trigger));
}
Selectable<BangData> getBangDataList({Iterable<BangGroup>? groups}) {
Selectable<BangData> getBangDataList({
Iterable<BangGroup>? groups,
String? domain,
String? category,
String? subCategory,
}) {
final selectable = select(db.bangDataView);
if (groups != null) {
selectable.where((t) => t.group.isInValues(groups));
}
if (domain != null) {
selectable.where((t) => t.domain.equals(domain));
}
if (category != null) {
selectable.where((t) => t.category.equals(category));
if (subCategory != null) {
selectable.where((t) => t.subCategory.equals(subCategory));
}
}
selectable.orderBy([(t) => OrderingTerm.asc(t.websiteName)]);
+19 -2
View File
@@ -19,9 +19,26 @@ Stream<BangData?> kagiSearchBangData(KagiSearchBangDataRef ref) {
}
@Riverpod()
Stream<List<BangData>> bangDataList(BangDataListRef ref) {
Stream<Map<String, List<String>>> bangCategories(BangCategoriesRef ref) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBangs();
return repository.watchCategories();
}
@Riverpod()
Stream<List<BangData>> bangDataList(
BangDataListRef ref, {
({
Iterable<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
})? filter,
}) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBangs(
groups: filter?.groups,
domain: filter?.domain,
categoryFilter: filter?.categoryFilter,
);
}
@Riverpod()
@@ -31,8 +31,19 @@ class BangDataRepository extends _$BangDataRepository {
return _db.bangDao.getBangCount(groups: [group]).watchSingle();
}
Stream<List<BangData>> watchBangs({Iterable<BangGroup>? groups}) {
return _db.bangDao.getBangDataList(groups: groups).watch();
Stream<List<BangData>> watchBangs({
Iterable<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
}) {
return _db.bangDao
.getBangDataList(
groups: groups,
domain: domain,
category: categoryFilter?.category,
subCategory: categoryFilter?.subCategory,
)
.watch();
}
Stream<List<BangData>> watchFrequentBangs({Iterable<BangGroup>? groups}) {
@@ -8,6 +8,7 @@ import 'package:bang_navigator/features/search_browser/domain/entities/sheet.dar
import 'package:bang_navigator/features/search_browser/domain/providers.dart';
import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart';
import 'package:bang_navigator/presentation/hooks/listenable_callback.dart';
import 'package:bang_navigator/presentation/widgets/failure_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
@@ -66,14 +67,12 @@ class BangSearchScreen extends HookConsumerWidget {
bang,
onTap: () {
ref
.read(selectedBangTriggerProvider.notifier)
.read(selectedBangTriggerProvider().notifier)
.setTrigger(bang.trigger);
if (ref.read(bottomSheetProvider) is! CreateTab) {
ref.read(bottomSheetProvider.notifier).show(
CreateTab(
preferredTool: KagiTool.search,
),
CreateTab(preferredTool: KagiTool.search),
);
}
@@ -82,8 +81,13 @@ class BangSearchScreen extends HookConsumerWidget {
);
},
),
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => SizedBox.shrink(),
error: (error, stackTrace) => Center(
child: FailureWidget(
title: 'Bang Search failed',
exception: error,
),
),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
}
@@ -0,0 +1,69 @@
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:flutter/material.dart';
class BangChips extends StatelessWidget {
final List<BangData> availableBangs;
final BangData? selectedBang;
final int maxBangCount;
final void Function(String trigger)? onSelected;
final void Function(String trigger)? onDeleted;
const BangChips({
required this.availableBangs,
this.selectedBang,
this.maxBangCount = 25,
this.onSelected,
this.onDeleted,
super.key,
});
@override
Widget build(BuildContext context) {
var bangs = availableBangs.take(maxBangCount).toList();
if (selectedBang != null) {
final selectedIndex =
bangs.indexWhere((bang) => bang.trigger == selectedBang?.trigger);
if (selectedIndex < 0) {
bangs = [
selectedBang!,
...bangs,
];
} else {
bangs = [
bangs.removeAt(selectedIndex),
...bangs,
];
}
}
return ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: bangs.length,
itemBuilder: (context, index) {
final bang = bangs[index];
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: FilterChip(
selected: bang.trigger == selectedBang?.trigger,
showCheckmark: false,
onSelected: (value) {
if (value) {
onSelected?.call(bang.trigger);
} else {
onDeleted?.call(bang.trigger);
}
},
onDeleted: () {
onDeleted?.call(bang.trigger);
},
avatar: BangIcon(bang, iconSize: 20),
label: Text(bang.websiteName),
),
);
},
);
}
}
@@ -0,0 +1,207 @@
// The default Material-style Autocomplete options.
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:bang_navigator/features/kagi/domain/repositories/autosuggest.dart';
import 'package:bang_navigator/features/search_browser/presentation/widgets/speech_to_text_button.dart';
import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart';
import 'package:bang_navigator/presentation/widgets/autocomplete.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class SearchField extends HookConsumerWidget {
static const defaultMaxOptionsHeight = 158.0;
final TextEditingController textController;
final BangData? activeBang;
final OptionsViewOpenDirection openDirection;
final double maxOptionsHeight;
final void Function(String)? onFieldSubmitted;
const SearchField({
required this.textController,
required this.activeBang,
this.openDirection = OptionsViewOpenDirection.up,
this.maxOptionsHeight = defaultMaxOptionsHeight,
this.onFieldSubmitted,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.incognitoMode ?? false),
);
final optionsStream = ref.watch(autosuggestRepositoryProvider);
final focusNode = useFocusNode();
final quickAnswer = useListenableSelector(
textController,
() => textController.text.endsWith('?'),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ExternalResultsAutocomplete<String>(
textEditingController: textController,
optionsStream: optionsStream,
focusNode: focusNode,
optionsViewOpenDirection: openDirection,
displayStringForOption:
// ignore: avoid_redundant_argument_values
RawAutocomplete.defaultStringForOption,
optionsViewBuilder: (context, onSelected, options) {
return _AutocompleteOptions(
//Must match RawAutocomplete parent
displayStringForOption: RawAutocomplete.defaultStringForOption,
onSelected: onSelected,
options: options,
//Must match RawAutocomplete parent
openDirection: openDirection,
maxOptionsHeight: maxOptionsHeight,
);
},
onTextChanged: (textEditingValue) {
ref
.read(autosuggestRepositoryProvider.notifier)
.addQuery(textEditingValue.text);
},
fieldViewBuilder: (
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return TextFormField(
controller: textEditingController,
enableIMEPersonalizedLearning: !incognitoEnabled,
focusNode: focusNode,
decoration: InputDecoration(
prefixIcon: (activeBang != null)
? Padding(
padding: const EdgeInsetsDirectional.all(12.0),
child: BangIcon(activeBang!, iconSize: 24.0),
)
: null,
label: const Text('Query'),
hintText: 'Ask anything...',
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: SpeechToTextButton(
onTextReceived: (data) {
textEditingController.text = data.toString();
},
),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value?.isEmpty ?? true) {
return '';
}
return null;
},
onTapOutside: (event) {
focusNode.unfocus();
},
onFieldSubmitted: this.onFieldSubmitted,
);
},
),
if (activeBang?.domain.endsWith('kagi.com') == true)
SwitchListTile(
value: quickAnswer,
onChanged: (_) {
if (textController.text.endsWith('?')) {
textController.text = textController.text
.substring(0, textController.text.length - 1);
} else {
textController.text = '${textController.text}?';
}
},
contentPadding: EdgeInsets.zero,
title: const Text('Quick Answer'),
secondary: const Icon(MdiIcons.lightningBolt),
),
],
);
}
}
class _AutocompleteOptions<T extends Object> extends StatelessWidget {
const _AutocompleteOptions({
super.key,
required this.displayStringForOption,
required this.onSelected,
required this.openDirection,
required this.options,
required this.maxOptionsHeight,
});
final AutocompleteOptionToString<T> displayStringForOption;
final AutocompleteOnSelected<T> onSelected;
final OptionsViewOpenDirection openDirection;
final Iterable<T> options;
final double maxOptionsHeight;
@override
Widget build(BuildContext context) {
final AlignmentDirectional optionsAlignment = switch (openDirection) {
OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart,
OptionsViewOpenDirection.down => AlignmentDirectional.topStart,
};
return Align(
alignment: optionsAlignment,
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
reverse: switch (openDirection) {
OptionsViewOpenDirection.up => true,
OptionsViewOpenDirection.down => false,
},
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final T option = options.elementAt(index);
return InkWell(
onTap: () {
onSelected(option);
},
child: Builder(
builder: (BuildContext context) {
final bool highlight =
AutocompleteHighlightedOption.of(context) == index;
// if (highlight) {
// SchedulerBinding.instance.addPostFrameCallback(
// (Duration timeStamp) async {
// await Scrollable.ensureVisible(
// context,
// alignment: 0.5,
// );
// },
// debugLabel: 'AutocompleteOptions.ensureVisible',
// );
// }
return Container(
color: highlight ? Theme.of(context).focusColor : null,
padding: const EdgeInsets.all(16.0),
child: Text(displayStringForOption(option)),
);
},
),
);
},
),
),
),
);
}
}
@@ -0,0 +1,103 @@
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/bang_data.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_chips.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/search_field.dart';
import 'package:bang_navigator/features/search_browser/domain/providers.dart';
import 'package:bang_navigator/features/web_view/presentation/controllers/switch_new_tab.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class SiteSearch extends HookConsumerWidget {
final String domain;
const SiteSearch({required this.domain, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final availableBangsAsync = ref.watch(
bangDataListProvider(
filter: (domain: domain, groups: null, categoryFilter: null),
),
);
final availableBangCount = availableBangsAsync.valueOrNull?.length ?? 0;
final selectedBang = ref.watch(
selectedBangDataProvider(domain: domain)
.select((value) => value.valueOrNull),
);
final activeBang =
selectedBang ?? availableBangsAsync.valueOrNull?.firstOrNull;
final textController = useTextEditingController();
Future<void> submitSearch() async {
if (activeBang != null && (formKey.currentState?.validate() == true)) {
await ref
.read(bangDataRepositoryProvider.notifier)
.increaseFrequency(activeBang.trigger);
await ref
.read(switchNewTabControllerProvider.notifier)
.add(activeBang.getUrl(textController.text));
ref.watch(overlayDialogProvider.notifier).dismiss();
}
}
return availableBangsAsync.when(
data: (availableBangs) {
return Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (availableBangCount > 1)
SizedBox(
height: 48,
width: double.maxFinite,
child: BangChips(
availableBangs: availableBangs,
selectedBang: selectedBang,
onSelected: (trigger) {
ref
.read(
selectedBangTriggerProvider(domain: domain)
.notifier,
)
.setTrigger(trigger);
},
onDeleted: (trigger) {
if (ref.read(
selectedBangTriggerProvider(domain: domain),
) ==
trigger) {
ref
.read(
selectedBangTriggerProvider(domain: domain)
.notifier,
)
.clearTrigger();
}
},
),
),
SearchField(
textController: textController,
activeBang: activeBang,
onFieldSubmitted: (_) async {
await submitSearch();
},
),
],
),
);
},
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox(height: 48),
);
}
}
@@ -1,93 +0,0 @@
import 'package:bang_navigator/core/routing/routes.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BangChips extends HookConsumerWidget {
static const _maxBangCount = 25;
final BangData? selectedBang;
final void Function(String trigger)? onSelected;
final void Function(String trigger)? onDeleted;
const BangChips({
this.selectedBang,
this.onSelected,
this.onDeleted,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final frequentBangsAsync = ref.watch(frequentBangDataListProvider);
return frequentBangsAsync.when(
data: (bangs) {
bangs = bangs.take(_maxBangCount).toList();
if (selectedBang != null) {
final selectedIndex =
bangs.indexWhere((bang) => bang.trigger == selectedBang?.trigger);
if (selectedIndex < 0) {
bangs = [
selectedBang!,
...bangs,
];
} else {
bangs = [
bangs.removeAt(selectedIndex),
...bangs,
];
}
}
return SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: bangs.length,
itemBuilder: (context, index) {
final bang = bangs[index];
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: FilterChip(
selected: bang.trigger == selectedBang?.trigger,
showCheckmark: false,
onSelected: (value) {
if (value) {
onSelected?.call(bang.trigger);
} else {
onDeleted?.call(bang.trigger);
}
},
onDeleted: () {
onDeleted?.call(bang.trigger);
},
avatar: BangIcon(bang, iconSize: 20),
label: Text(bang.websiteName),
),
);
},
),
),
IconButton(
onPressed: () async {
await context.push(BangSearchRoute().location);
},
icon: const Icon(Icons.chevron_right),
),
],
),
);
},
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => SizedBox.shrink(),
);
}
}
@@ -1,98 +1,18 @@
import 'package:bang_navigator/core/routing/routes.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/bang_data.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:bang_navigator/features/kagi/domain/repositories/autosuggest.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_chips.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/search_field.dart';
import 'package:bang_navigator/features/search_browser/domain/providers.dart';
import 'package:bang_navigator/features/search_browser/presentation/widgets/bang_chips.dart';
import 'package:bang_navigator/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart';
import 'package:bang_navigator/features/search_browser/presentation/widgets/speech_to_text_button.dart';
import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart';
import 'package:bang_navigator/features/share_intent/domain/entities/shared_content.dart';
import 'package:bang_navigator/presentation/widgets/autocomplete.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
// The default Material-style Autocomplete options.
class _AutocompleteOptions<T extends Object> extends StatelessWidget {
const _AutocompleteOptions({
super.key,
required this.displayStringForOption,
required this.onSelected,
required this.openDirection,
required this.options,
required this.maxOptionsHeight,
});
final AutocompleteOptionToString<T> displayStringForOption;
final AutocompleteOnSelected<T> onSelected;
final OptionsViewOpenDirection openDirection;
final Iterable<T> options;
final double maxOptionsHeight;
@override
Widget build(BuildContext context) {
final AlignmentDirectional optionsAlignment = switch (openDirection) {
OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart,
OptionsViewOpenDirection.down => AlignmentDirectional.topStart,
};
return Align(
alignment: optionsAlignment,
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
reverse: switch (openDirection) {
OptionsViewOpenDirection.up => true,
OptionsViewOpenDirection.down => false,
},
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final T option = options.elementAt(index);
return InkWell(
onTap: () {
onSelected(option);
},
child: Builder(
builder: (BuildContext context) {
final bool highlight =
AutocompleteHighlightedOption.of(context) == index;
// if (highlight) {
// SchedulerBinding.instance.addPostFrameCallback(
// (Duration timeStamp) async {
// await Scrollable.ensureVisible(
// context,
// alignment: 0.5,
// );
// },
// debugLabel: 'AutocompleteOptions.ensureVisible',
// );
// }
return Container(
color: highlight ? Theme.of(context).focusColor : null,
padding: const EdgeInsets.all(16.0),
child: Text(displayStringForOption(option)),
);
},
),
);
},
),
),
),
);
}
}
class SearchTab extends HookConsumerWidget {
static const _maxOptionsHeight = 158.0;
final SharedContent? sharedContent;
final OnSubmitUri onSubmit;
@@ -104,29 +24,29 @@ class SearchTab extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.incognitoMode ?? false),
);
useAutomaticKeepAlive();
final formKey = useMemoized(() => GlobalKey<FormState>());
final focusNode = useFocusNode();
final selectedBangAsync = ref.watch(selectedBangDataProvider);
final defaultSearchBangAsync = ref.watch(kagiSearchBangDataProvider);
final selectedBang = ref
.watch(selectedBangDataProvider().select((value) => value.valueOrNull));
final defaultSearchBang = ref
.watch(kagiSearchBangDataProvider.select((value) => value.valueOrNull));
final activeBang =
selectedBangAsync.valueOrNull ?? defaultSearchBangAsync.valueOrNull;
final activeBang = selectedBang ?? defaultSearchBang;
final textController =
useTextEditingController(text: sharedContent?.toString());
final quickAnswer = useListenableSelector(
textController,
() => textController.text.endsWith('?'),
);
Future<void> submitSearch() async {
if (activeBang != null && (formKey.currentState?.validate() == true)) {
await ref
.read(bangDataRepositoryProvider.notifier)
.increaseFrequency(activeBang.trigger);
onSubmit(activeBang.getUrl(textController.text));
}
}
return Form(
key: formKey,
@@ -134,37 +54,75 @@ class SearchTab extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
selectedBangAsync.when(
skipLoadingOnReload: true,
data: (selectedBang) {
return BangChips(
selectedBang: selectedBang,
onSelected: (trigger) {
ref
.read(selectedBangTriggerProvider.notifier)
.setTrigger(trigger);
},
onDeleted: (trigger) {
if (ref.read(selectedBangTriggerProvider) == trigger) {
ref
.read(selectedBangTriggerProvider.notifier)
.clearTrigger();
}
},
Consumer(
builder: (context, ref, child) {
final frequentBangsAsync =
ref.watch(frequentBangDataListProvider);
return frequentBangsAsync.when(
data: (availableBangs) => SizedBox(
height: 48,
child: Row(
children: [
if (selectedBang != null || availableBangs.isNotEmpty)
Expanded(
child: BangChips(
availableBangs: availableBangs,
selectedBang: selectedBang,
onSelected: (trigger) {
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(trigger);
},
onDeleted: (trigger) {
if (ref.read(selectedBangTriggerProvider()) ==
trigger) {
ref
.read(
selectedBangTriggerProvider().notifier,
)
.clearTrigger();
}
},
),
)
else
Expanded(
child: Text(
"Press '>' to search Bangs.",
style: TextStyle(
color: Theme.of(context).hintColor,
fontStyle: FontStyle.italic,
),
),
),
IconButton(
onPressed: () async {
await context.push(BangSearchRoute().location);
},
icon: const Icon(Icons.chevron_right),
),
],
),
),
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox(
height: 48,
width: double.infinity,
),
);
},
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => SizedBox.shrink(),
),
Consumer(
builder: (context, ref, child) {
final optionsStream = ref.watch(autosuggestRepositoryProvider);
const maxOptionsHeight = SearchField.defaultMaxOptionsHeight;
final openDirection = ref.watch(
bottomSheetExtendProvider.select((value) {
final extend = value.valueOrNull;
if (extend == null ||
(MediaQuery.of(context).size.height * (1 - extend)) >
_maxOptionsHeight) {
maxOptionsHeight) {
return OptionsViewOpenDirection.up;
} else {
return OptionsViewOpenDirection.down;
@@ -172,113 +130,23 @@ class SearchTab extends HookConsumerWidget {
}),
);
return ExternalResultsAutocomplete<String>(
textEditingController: textController,
optionsStream: optionsStream,
focusNode: focusNode,
optionsViewOpenDirection: openDirection,
displayStringForOption:
// ignore: avoid_redundant_argument_values
RawAutocomplete.defaultStringForOption,
optionsViewBuilder: (context, onSelected, options) {
return _AutocompleteOptions(
//Must match RawAutocomplete parent
displayStringForOption:
RawAutocomplete.defaultStringForOption,
onSelected: onSelected,
options: options,
//Must match RawAutocomplete parent
openDirection: openDirection,
maxOptionsHeight: _maxOptionsHeight,
);
},
onTextChanged: (textEditingValue) {
ref
.read(autosuggestRepositoryProvider.notifier)
.addQuery(textEditingValue.text);
},
fieldViewBuilder: (
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return TextFormField(
controller: textEditingController,
enableIMEPersonalizedLearning: !incognitoEnabled,
focusNode: focusNode,
decoration: InputDecoration(
prefixIcon: (activeBang != null)
? Padding(
padding: const EdgeInsetsDirectional.all(12.0),
child: BangIcon(activeBang, iconSize: 24.0),
)
: null,
label: const Text('Query'),
hintText: 'Ask anything...',
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: SpeechToTextButton(
onTextReceived: (data) {
textEditingController.text = data.toString();
},
),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value?.isEmpty ?? true) {
return '';
}
return null;
},
onTapOutside: (event) {
focusNode.unfocus();
},
onFieldSubmitted: (_) async {
if (activeBang != null &&
(formKey.currentState?.validate() == true)) {
await ref
.read(bangDataRepositoryProvider.notifier)
.increaseFrequency(activeBang.trigger);
onSubmit(activeBang.getUrl(textController.text));
}
},
);
return SearchField(
textController: textController,
activeBang: activeBang,
openDirection: openDirection,
onFieldSubmitted: (_) async {
await submitSearch();
},
);
},
),
SwitchListTile(
value: quickAnswer,
onChanged: (_) {
if (textController.text.endsWith('?')) {
textController.text = textController.text
.substring(0, textController.text.length - 1);
} else {
textController.text = '${textController.text}?';
}
},
contentPadding: EdgeInsets.zero,
title: const Text('Quick Answer'),
secondary: const Icon(MdiIcons.lightningBolt),
),
const SizedBox(
height: 12,
),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () async {
if (activeBang != null &&
(formKey.currentState?.validate() == true)) {
await ref
.read(bangDataRepositoryProvider.notifier)
.increaseFrequency(activeBang.trigger);
onSubmit(activeBang.getUrl(textController.text));
}
},
onPressed: submitSearch,
label: const Text('Search'),
icon: const Icon(MdiIcons.invoiceTextSend),
),
@@ -1,4 +1,5 @@
import 'package:bang_navigator/domain/entities/web_page_info.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/site_search.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/modes.dart';
import 'package:bang_navigator/features/search_browser/utils/url_builder.dart'
as uri_builder;
@@ -133,6 +134,14 @@ class WebPageDialog extends HookConsumerWidget {
),
),
),
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SiteSearch(
domain: page.url.host,
),
),
const Divider(),
ListTile(
leading: const Icon(MdiIcons.contentCopy),
title: const Text('Copy address'),