intermediate
This commit is contained in:
@@ -3,4 +3,5 @@ abstract interface class IQueryBuilder {
|
||||
int get ftsMinTokenLength;
|
||||
|
||||
String buildFtsQuery(String input);
|
||||
String buildLikeQuery(String input);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
||||
import 'package:lensai/features/search/domain/entities/bareword.dart';
|
||||
import 'package:lensai/features/search/domain/unix_tokenizer.dart';
|
||||
|
||||
typedef _Phrase = List<Bareword>;
|
||||
|
||||
@@ -228,7 +229,26 @@ mixin PrefixQueryBuilderMixin implements IQueryBuilder {
|
||||
tokenLimit: ftsTokenLimit,
|
||||
);
|
||||
|
||||
return queryBuilder.build();
|
||||
if (queryBuilder.hasTokens) {
|
||||
return queryBuilder.build();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
String buildLikeQuery(String input) {
|
||||
final likeQueryBuilder = UnixLikeQueryBuilder.tokenize(
|
||||
input: input,
|
||||
minTokenLength: 1,
|
||||
tokenLimit: 5,
|
||||
);
|
||||
|
||||
if (likeQueryBuilder.hasTokens) {
|
||||
return likeQueryBuilder.build();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +261,25 @@ mixin TrigramQueryBuilderMixin implements IQueryBuilder {
|
||||
tokenLimit: ftsTokenLimit,
|
||||
);
|
||||
|
||||
return queryBuilder.build();
|
||||
if (queryBuilder.hasTokens) {
|
||||
return queryBuilder.build();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
String buildLikeQuery(String input) {
|
||||
final likeQueryBuilder = UnixLikeQueryBuilder.tokenize(
|
||||
input: input,
|
||||
minTokenLength: 1,
|
||||
tokenLimit: 5,
|
||||
);
|
||||
|
||||
if (likeQueryBuilder.hasTokens) {
|
||||
return likeQueryBuilder.build();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/con
|
||||
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
||||
import 'package:lensai/features/search/presentation/widgets/bang_chips.dart';
|
||||
import 'package:lensai/features/search/presentation/widgets/search_field.dart';
|
||||
import 'package:lensai/features/search/presentation/widgets/search_suggestion_list.dart';
|
||||
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
@@ -166,76 +167,9 @@ class SearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
useListenableSelector(
|
||||
searchTextController,
|
||||
() => searchTextController.text.isNotEmpty,
|
||||
);
|
||||
|
||||
final suggestions =
|
||||
useStream(ref.watch(searchSuggestionsProvider()));
|
||||
|
||||
final searchHistory = ref.watch(searchHistoryProvider);
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
SearchSuggestionList(
|
||||
searchTextController: searchTextController,
|
||||
submitSearch: submitSearch,
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Divider(),
|
||||
@@ -274,7 +208,9 @@ class SearchScreen extends HookConsumerWidget {
|
||||
child: tabs.when(
|
||||
data: (data) {
|
||||
if (data == null) {
|
||||
return const SizedBox.shrink();
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverList.builder(
|
||||
@@ -283,12 +219,14 @@ class SearchScreen extends HookConsumerWidget {
|
||||
final result = data[index];
|
||||
|
||||
final headHasMatch =
|
||||
result.title.contains(_matchPrefix);
|
||||
result.title?.contains(_matchPrefix) ?? false;
|
||||
final bodyResult =
|
||||
result.extractedContent ?? result.fullContent;
|
||||
|
||||
return ListTile(
|
||||
title: MarkdownBody(data: result.title),
|
||||
title: (result.title != null)
|
||||
? MarkdownBody(data: result.title!)
|
||||
: null,
|
||||
subtitle: (!headHasMatch && bodyResult != null)
|
||||
? MarkdownBody(data: bodyResult)
|
||||
: null,
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
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/user/domain/repositories/settings.dart';
|
||||
import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
|
||||
|
||||
class SearchField extends HookConsumerWidget {
|
||||
final TextEditingController textEditingController;
|
||||
@@ -64,14 +64,6 @@ class SearchField extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
// autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
// validator: (value) {
|
||||
// if (value?.isEmpty ?? true) {
|
||||
// return '';
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// },
|
||||
onTapOutside: (focusNode != null)
|
||||
? (event) {
|
||||
focusNode!.unfocus();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
||||
|
||||
class SearchSuggestionList extends HookConsumerWidget {
|
||||
final TextEditingController searchTextController;
|
||||
final Future<void> Function(String query) submitSearch;
|
||||
final bool showHistory;
|
||||
|
||||
const SearchSuggestionList({
|
||||
required this.searchTextController,
|
||||
required this.submitSearch,
|
||||
this.showHistory = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
useListenableSelector(
|
||||
searchTextController,
|
||||
() => searchTextController.text.isNotEmpty,
|
||||
);
|
||||
|
||||
final suggestions = useStream(ref.watch(searchSuggestionsProvider()));
|
||||
|
||||
final searchHistory = ref.watch(searchHistoryProvider);
|
||||
|
||||
final searchText = searchTextController.text;
|
||||
|
||||
if (showHistory &&
|
||||
(!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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user