Add provider tag suggestions and tag categories
This commit is contained in:
@@ -73,10 +73,12 @@ class FeedController extends AsyncNotifier<FeedState> {
|
||||
return;
|
||||
}
|
||||
final lastToken = query.trim().split(RegExp(r'\s+')).last;
|
||||
final result =
|
||||
await ref.read(searchServiceProvider).autocomplete(lastToken);
|
||||
final suggestions =
|
||||
result is Success<List<String>> ? result.data : const <String>[];
|
||||
final result = await ref
|
||||
.read(searchServiceProvider)
|
||||
.autocompleteDetailed(lastToken, limit: 16);
|
||||
final suggestions = result is Success<List<TagSuggestion>>
|
||||
? result.data
|
||||
: const <TagSuggestion>[];
|
||||
state = AsyncData(current.copyWith(tagSuggestions: suggestions));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class FeedState {
|
||||
final List<String> selectedProviderIds;
|
||||
final String? ratingFilter;
|
||||
final TopPeriodFilter topPeriodFilter;
|
||||
final List<String> tagSuggestions;
|
||||
final List<TagSuggestion> tagSuggestions;
|
||||
final List<ContentProviderConfig> providers;
|
||||
final bool isLoadingMore;
|
||||
final bool hasMore;
|
||||
@@ -31,7 +31,7 @@ class FeedState {
|
||||
List<String>? selectedProviderIds,
|
||||
String? ratingFilter,
|
||||
TopPeriodFilter? topPeriodFilter,
|
||||
List<String>? tagSuggestions,
|
||||
List<TagSuggestion>? tagSuggestions,
|
||||
bool clearRating = false,
|
||||
List<ContentProviderConfig>? providers,
|
||||
bool? isLoadingMore,
|
||||
|
||||
@@ -33,7 +33,7 @@ class FeedToolbar extends StatefulWidget {
|
||||
final List<String> selectedTags;
|
||||
final List<String> selectedProviderIds;
|
||||
final TopPeriodFilter topPeriodFilter;
|
||||
final List<String> tagSuggestions;
|
||||
final List<TagSuggestion> tagSuggestions;
|
||||
final String? rating;
|
||||
final ValueChanged<String> onQuickProviderToggle;
|
||||
final ValueChanged<String> onSearchChanged;
|
||||
@@ -154,9 +154,9 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) => ActionChip(
|
||||
avatar: const Icon(Icons.tag_rounded, size: 15),
|
||||
label: Text(widget.tagSuggestions[index]),
|
||||
label: _SuggestionLabel(widget.tagSuggestions[index]),
|
||||
onPressed: () =>
|
||||
_applySuggestion(widget.tagSuggestions[index]),
|
||||
_applySuggestion(widget.tagSuggestions[index].name),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -255,9 +255,9 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
itemCount: widget.tagSuggestions.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 6),
|
||||
itemBuilder: (context, index) => ActionChip(
|
||||
label: Text(widget.tagSuggestions[index]),
|
||||
label: _SuggestionLabel(widget.tagSuggestions[index]),
|
||||
onPressed: () =>
|
||||
_applySuggestion(widget.tagSuggestions[index]),
|
||||
_applySuggestion(widget.tagSuggestions[index].name),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -282,6 +282,27 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
}
|
||||
}
|
||||
|
||||
class _SuggestionLabel extends StatelessWidget {
|
||||
const _SuggestionLabel(this.suggestion);
|
||||
|
||||
final TagSuggestion suggestion;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(suggestion.name),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
suggestion.categoryLabel,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>?> showProviderFilterSheet(
|
||||
BuildContext context, {
|
||||
required List<ContentProviderConfig> providers,
|
||||
|
||||
@@ -325,6 +325,9 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
|
||||
super.initState();
|
||||
_currentIndex = widget.initialIndex;
|
||||
_controller = PageController(initialPage: widget.initialIndex);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_prefetchAround(widget.initialIndex);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -337,6 +340,9 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
|
||||
if (_controller.hasClients) {
|
||||
_controller.jumpToPage(widget.initialIndex);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_prefetchAround(widget.initialIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +359,7 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
|
||||
itemCount: widget.posts.length,
|
||||
onPageChanged: (index) {
|
||||
_currentIndex = index;
|
||||
_prefetchAround(index);
|
||||
final post = widget.posts[index];
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
@@ -363,13 +370,52 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return KeyedSubtree(
|
||||
key: ValueKey(widget.posts[index].cacheKey),
|
||||
child: widget.buildDetails(context, widget.posts[index]),
|
||||
return _KeepAlivePostPage(
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(widget.posts[index].cacheKey),
|
||||
child: widget.buildDetails(context, widget.posts[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _prefetchAround(int index) {
|
||||
for (final offset in [-1, 0, 1]) {
|
||||
final target = index + offset;
|
||||
if (target < 0 || target >= widget.posts.length) continue;
|
||||
final post = widget.posts[target];
|
||||
final urls = [
|
||||
post.previewUrl,
|
||||
post.sampleUrl,
|
||||
if (post.fileType.toLowerCase().contains('gif')) post.fileUrl,
|
||||
].where((url) => url.trim().isNotEmpty).toSet();
|
||||
for (final url in urls) {
|
||||
precacheImage(NetworkImage(url), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _KeepAlivePostPage extends StatefulWidget {
|
||||
const _KeepAlivePostPage({required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<_KeepAlivePostPage> createState() => _KeepAlivePostPageState();
|
||||
}
|
||||
|
||||
class _KeepAlivePostPageState extends State<_KeepAlivePostPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class _CommentsSection extends ConsumerStatefulWidget {
|
||||
|
||||
@@ -214,11 +214,10 @@ class _PostMediaViewerState extends State<PostMediaViewer> {
|
||||
}
|
||||
|
||||
List<String> _buildImageUrls(Post post) {
|
||||
return {
|
||||
post.sampleUrl,
|
||||
post.fileUrl,
|
||||
post.previewUrl,
|
||||
}.where((url) => url.trim().isNotEmpty).toList();
|
||||
final urls = post.fileType.toLowerCase().contains('gif')
|
||||
? [post.fileUrl, post.sampleUrl, post.previewUrl]
|
||||
: [post.sampleUrl, post.fileUrl, post.previewUrl];
|
||||
return urls.where((url) => url.trim().isNotEmpty).toSet().toList();
|
||||
}
|
||||
|
||||
List<String> _buildVideoUrls(Post post) {
|
||||
|
||||
@@ -11,16 +11,66 @@ class PostTagsPanel extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
final groups = _groups(post);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final tag in post.tags.take(80))
|
||||
TagChip(
|
||||
tag: tag,
|
||||
onTap: () => context.go('/?q=${Uri.encodeQueryComponent(tag)}'),
|
||||
for (final entry in groups.entries) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 6),
|
||||
child: Text(
|
||||
_label(entry.key),
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final tag in entry.value.take(80))
|
||||
TagChip(
|
||||
tag: tag,
|
||||
onTap: () =>
|
||||
context.go('/?q=${Uri.encodeQueryComponent(tag)}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, List<String>> _groups(Post post) {
|
||||
if (post.tagGroups.isNotEmpty) {
|
||||
final ordered = <String, List<String>>{};
|
||||
for (final key in [
|
||||
'artist',
|
||||
'character',
|
||||
'copyright',
|
||||
'species',
|
||||
'meta',
|
||||
'general',
|
||||
]) {
|
||||
final tags = post.tagGroups[key];
|
||||
if (tags != null && tags.isNotEmpty) ordered[key] = tags;
|
||||
}
|
||||
for (final entry in post.tagGroups.entries) {
|
||||
ordered.putIfAbsent(entry.key, () => entry.value);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
return {'general': post.tags};
|
||||
}
|
||||
|
||||
String _label(String key) {
|
||||
return switch (key) {
|
||||
'artist' => 'Artist',
|
||||
'character' => 'Character',
|
||||
'copyright' => 'Copyright / Title',
|
||||
'species' => 'Species',
|
||||
'meta' => 'Meta',
|
||||
'general' => 'General',
|
||||
_ => 'Other',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ class SearchController extends AsyncNotifier<SearchState> {
|
||||
Future<void> updateQuery(String query) async {
|
||||
final lastToken =
|
||||
query.trim().isEmpty ? '' : query.trim().split(RegExp(r'\s+')).last;
|
||||
final suggestionsResult =
|
||||
await ref.read(searchServiceProvider).autocomplete(lastToken);
|
||||
final suggestions = suggestionsResult is Success<List<String>>
|
||||
final suggestionsResult = await ref
|
||||
.read(searchServiceProvider)
|
||||
.autocompleteDetailed(lastToken, limit: 16);
|
||||
final suggestions = suggestionsResult is Success<List<TagSuggestion>>
|
||||
? suggestionsResult.data
|
||||
: <String>[];
|
||||
: <TagSuggestion>[];
|
||||
state = AsyncData(
|
||||
(state.value ?? const SearchState()).copyWith(
|
||||
query: query,
|
||||
|
||||
@@ -8,12 +8,12 @@ class SearchState {
|
||||
});
|
||||
|
||||
final List<SearchHistory> recent;
|
||||
final List<String> suggestions;
|
||||
final List<TagSuggestion> suggestions;
|
||||
final String query;
|
||||
|
||||
SearchState copyWith({
|
||||
List<SearchHistory>? recent,
|
||||
List<String>? suggestions,
|
||||
List<TagSuggestion>? suggestions,
|
||||
String? query,
|
||||
}) {
|
||||
return SearchState(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../backend/backend.dart';
|
||||
|
||||
class SearchSuggestions extends StatelessWidget {
|
||||
const SearchSuggestions(
|
||||
{required this.suggestions, required this.onTap, super.key});
|
||||
|
||||
final List<String> suggestions;
|
||||
final List<TagSuggestion> suggestions;
|
||||
final ValueChanged<String> onTap;
|
||||
|
||||
@override
|
||||
@@ -16,8 +18,8 @@ class SearchSuggestions extends StatelessWidget {
|
||||
children: [
|
||||
for (final suggestion in suggestions)
|
||||
ActionChip(
|
||||
label: Text(suggestion),
|
||||
onPressed: () => onTap(suggestion),
|
||||
label: Text('${suggestion.name} · ${suggestion.categoryLabel}'),
|
||||
onPressed: () => onTap(suggestion.name),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user