intermediate

This commit is contained in:
Fabian Freund
2025-01-07 00:06:31 +01:00
parent ea0880b97a
commit 4a2191b01d
84 changed files with 2655 additions and 1383 deletions
@@ -52,7 +52,10 @@ sealed class FtsQueryBuilder {
//Merge short tokens
_mergeShortBarewords(barewords, minTokenLength);
_tokens = barewords.take(tokenLimit).toList();
_tokens = barewords
.where((bareword) => bareword.word.length >= minTokenLength)
.take(tokenLimit)
.toList();
}
static void _mergeShortBarewords(
@@ -62,7 +65,7 @@ sealed class FtsQueryBuilder {
for (var i = 0; i < barewords.length; i++) {
final bareword = barewords[i];
if (i != 0) {
if (i > 0) {
if (bareword.word.length < minTokenLength) {
barewords[i] = barewords[i - 1].join(bareword);
}
@@ -1,12 +1,16 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/providers/search.dart';
import 'package:lensai/features/bangs/domain/repositories/data.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/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
@@ -75,6 +79,20 @@ class SearchScreen extends HookConsumerWidget {
},
);
Future<void> submitSearch(String query) async {
if (activeBang != null && (formKey.currentState?.validate() == true)) {
final searchUri = await ref.read(
triggerBangSearchProvider(activeBang, query).future,
);
await ref.read(tabRepositoryProvider.notifier).addTab(url: searchUri);
if (context.mounted) {
context.pop();
}
}
}
final selectedContainer = useState<ContainerData?>(globalSelectedContainer);
final availableTabs = ref
@@ -111,7 +129,7 @@ class SearchScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Site Search',
'Search Provider',
style: Theme.of(context).textTheme.labelSmall,
),
BangChips(
@@ -150,7 +168,7 @@ class SearchScreen extends HookConsumerWidget {
),
HookConsumer(
builder: (context, ref, child) {
final hasSearchText = useListenableSelector(
useListenableSelector(
searchTextController,
() => searchTextController.text.isNotEmpty,
);
@@ -158,30 +176,65 @@ class SearchScreen extends HookConsumerWidget {
final suggestions =
useStream(ref.watch(searchSuggestionsProvider()));
return (!hasSearchText || !suggestions.hasData)
? SliverList.builder(
itemCount: 5,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.history),
title: Text('Search History $index'),
);
},
)
: SliverList.builder(
itemCount: suggestions.data!.length,
itemBuilder: (context, index) {
final suggestion = suggestions.data![index];
final searchHistory = ref.watch(searchHistoryProvider);
return ListTile(
leading: const Icon(Icons.search),
title: Text(suggestion),
onLongPress: () {
searchTextController.text = suggestion;
},
);
final searchText = searchTextController.text;
if ((!searchText.isNotEmpty || !suggestions.hasData) &&
(searchHistory.value?.isNotEmpty ?? false)) {
final entries = searchHistory.value!;
return SliverList.builder(
itemCount: entries.length,
itemBuilder: (context, index) {
final query = entries[index].searchQuery;
return ListTile(
leading: const Icon(Icons.history),
title: Text(query),
onLongPress: () {
searchTextController.text = query;
},
onTap: () async {
await submitSearch(query);
},
trailing: IconButton(
onPressed: () async {
await ref
.read(bangDataRepositoryProvider.notifier)
.removeSearchEntry(query);
},
icon: const Icon(Icons.close),
),
);
},
);
}
final prioritizedSuggestions = [
if (searchText.isNotEmpty) searchText,
if (suggestions.data != null)
...suggestions.data!
.whereNot((suggestion) => suggestion == searchText),
];
return SliverList.builder(
itemCount: prioritizedSuggestions.length,
itemBuilder: (context, index) {
final suggestion = prioritizedSuggestions[index];
return ListTile(
leading: const Icon(Icons.search),
title: Text(suggestion),
onLongPress: () {
searchTextController.text = suggestion;
},
onTap: () async {
await submitSearch(suggestion);
},
);
},
);
},
),
const SliverToBoxAdapter(
@@ -220,6 +273,10 @@ class SearchScreen extends HookConsumerWidget {
enabled: tabs.isLoading,
child: tabs.when(
data: (data) {
if (data == null) {
return const SizedBox.shrink();
}
return SliverList.builder(
itemCount: data.length,
itemBuilder: (context, index) {
@@ -282,10 +339,12 @@ class SearchScreen extends HookConsumerWidget {
}
return ListTile(
leading: RawImage(
image: tab.icon?.value,
height: 24,
width: 24,
leading: RepaintBoundary(
child: RawImage(
image: tab.icon?.value,
height: 24,
width: 24,
),
),
title: Text(tab.title),
subtitle: Text(
@@ -66,53 +66,55 @@ class BangChips extends HookConsumerWidget {
);
return availableBangs.when(
data: (availableBangs) => SizedBox(
height: 48,
child: Row(
children: [
if (activeBang != null || availableBangs.isNotEmpty)
Expanded(
child: SelectableChips(
itemId: (bang) => bang.trigger,
itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
itemLabel: (bang) => Text(bang.websiteName),
availableItems: availableBangs,
selectedItem: activeBang,
onSelected: onSelected,
onDeleted: onDeleted,
),
)
else
Visibility(
visible: displayMenu,
child: Expanded(
child: Text(
"Press '>' to search Bangs.",
style: TextStyle(
color: Theme.of(context).hintColor,
fontStyle: FontStyle.italic,
data: (availableBangs) {
return SizedBox(
height: 48,
child: Row(
children: [
if (activeBang != null || availableBangs.isNotEmpty)
Expanded(
child: SelectableChips(
itemId: (bang) => bang.trigger,
itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
itemLabel: (bang) => Text(bang.websiteName),
availableItems: availableBangs,
selectedItem: activeBang,
onSelected: onSelected,
onDeleted: onDeleted,
),
)
else
Visibility(
visible: displayMenu,
child: Expanded(
child: Text(
"Press '>' to search Bangs.",
style: TextStyle(
color: Theme.of(context).hintColor,
fontStyle: FontStyle.italic,
),
),
),
),
),
if (displayMenu)
IconButton(
onPressed: () async {
final searchText = searchTextController?.text.trim();
if (displayMenu)
IconButton(
onPressed: () async {
final searchText = searchTextController?.text.trim();
await context.push(
BangSearchRoute(
searchText: (searchText?.isEmpty ?? true)
? BangSearchRoute.emptySearchText
: searchText!,
).location,
);
},
icon: const Icon(Icons.chevron_right),
),
],
),
),
await context.push(
BangSearchRoute(
searchText: (searchText?.isEmpty ?? true)
? BangSearchRoute.emptySearchText
: searchText!,
).location,
);
},
icon: const Icon(Icons.chevron_right),
),
],
),
);
},
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox(
height: 48,
@@ -4,8 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
import 'package:lensai/features/settings/data/models/settings.dart';
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
class SearchField extends HookConsumerWidget {
final TextEditingController textEditingController;
@@ -28,7 +27,7 @@ class SearchField extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select(
(value) => (value.valueOrNull ?? Settings.withDefaults()).incognitoMode,
(value) => value.incognitoMode,
),
);