Add viewed history, diagnostics, tag chips and SWF viewer
This commit is contained in:
@@ -47,9 +47,13 @@ class FeedController extends AsyncNotifier<FeedState> {
|
||||
if (current == null || current.isLoadingMore || !current.hasMore) return;
|
||||
state = AsyncData(current.copyWith(isLoadingMore: true, clearError: true));
|
||||
final posts = await _load(refresh: false, current: current);
|
||||
final existingKeys = current.posts.map((post) => post.cacheKey).toSet();
|
||||
final newPosts = posts
|
||||
.where((post) => existingKeys.add(post.cacheKey))
|
||||
.toList(growable: false);
|
||||
state = AsyncData(
|
||||
current.copyWith(
|
||||
posts: [...current.posts, ...posts],
|
||||
posts: [...current.posts, ...newPosts],
|
||||
isLoadingMore: false,
|
||||
hasMore: posts.isNotEmpty,
|
||||
),
|
||||
@@ -59,7 +63,9 @@ class FeedController extends AsyncNotifier<FeedState> {
|
||||
Future<void> search(String query) async {
|
||||
final tags = ref.read(searchServiceProvider).parseTags(query);
|
||||
final current = state.value ?? const FeedState();
|
||||
state = AsyncData(current.copyWith(selectedTags: tags, posts: []));
|
||||
state = AsyncData(
|
||||
current.copyWith(selectedTags: tags, posts: [], tagSuggestions: []),
|
||||
);
|
||||
await ref.read(searchServiceProvider).saveSearch(query, 0);
|
||||
await refresh();
|
||||
final count = state.value?.posts.length ?? 0;
|
||||
@@ -76,8 +82,13 @@ class FeedController extends AsyncNotifier<FeedState> {
|
||||
final result = await ref
|
||||
.read(searchServiceProvider)
|
||||
.autocompleteDetailed(lastToken, limit: 16);
|
||||
final normalizedToken = lastToken.trim().toLowerCase();
|
||||
final suggestions = result is Success<List<TagSuggestion>>
|
||||
? result.data
|
||||
.where((item) => item.name.toLowerCase().startsWith(
|
||||
normalizedToken,
|
||||
))
|
||||
.toList(growable: false)
|
||||
: const <TagSuggestion>[];
|
||||
state = AsyncData(current.copyWith(tagSuggestions: suggestions));
|
||||
}
|
||||
@@ -138,6 +149,7 @@ class FeedController extends AsyncNotifier<FeedState> {
|
||||
? <String?>[null]
|
||||
: current.selectedProviderIds;
|
||||
final posts = <Post>[];
|
||||
final seen = <String>{};
|
||||
for (final providerId in providerIds) {
|
||||
final result = refresh
|
||||
? await service.refresh(
|
||||
@@ -152,7 +164,11 @@ class FeedController extends AsyncNotifier<FeedState> {
|
||||
providerId: providerId,
|
||||
topPeriod: current.topPeriodFilter,
|
||||
);
|
||||
if (result is Success<List<Post>>) posts.addAll(result.data);
|
||||
if (result is Success<List<Post>>) {
|
||||
for (final post in result.data) {
|
||||
if (seen.add(post.cacheKey)) posts.add(post);
|
||||
}
|
||||
}
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
||||
final settings =
|
||||
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
|
||||
final favoriteKeys = ref.watch(favoriteKeysProvider).value ?? <String>{};
|
||||
final viewedKeys = ref.watch(viewedKeysProvider).value ?? <String>{};
|
||||
|
||||
if (!_usedInitialQuery && (widget.initialQuery?.isNotEmpty ?? false)) {
|
||||
_usedInitialQuery = true;
|
||||
@@ -165,6 +166,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
||||
nsfwEnabled: settings.nsfwEnabled,
|
||||
loading: state.isLoadingMore,
|
||||
favoriteKeys: favoriteKeys,
|
||||
viewedKeys: viewedKeys,
|
||||
onOpen: (post) => context.push(
|
||||
'/post/${post.providerId}/${post.id}',
|
||||
extra: post,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
import '../../../../app/responsive.dart';
|
||||
import '../../../../backend/backend.dart';
|
||||
@@ -75,13 +76,18 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppSearchBar(
|
||||
child: TagInputSearchBar(
|
||||
initialValue: _query,
|
||||
suggestions: widget.tagSuggestions,
|
||||
onChanged: (value) {
|
||||
_query = value;
|
||||
widget.onSearchChanged(value);
|
||||
},
|
||||
onSubmitted: widget.onSearch,
|
||||
onSuggestionApplied: (value) {
|
||||
_query = value;
|
||||
widget.onSuggestionTap(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -108,10 +114,12 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
_HorizontalWheelScroller(
|
||||
height: 36,
|
||||
child: ListView(
|
||||
builder: (controller) => ListView(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
primary: false,
|
||||
children: [
|
||||
if (widget.providers.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
@@ -144,23 +152,6 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.tagSuggestions.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.tagSuggestions.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) => ActionChip(
|
||||
avatar: const Icon(Icons.tag_rounded, size: 15),
|
||||
label: _SuggestionLabel(widget.tagSuggestions[index]),
|
||||
onPressed: () =>
|
||||
_applySuggestion(widget.tagSuggestions[index].name),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -171,19 +162,26 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||
child: Column(
|
||||
children: [
|
||||
AppSearchBar(
|
||||
TagInputSearchBar(
|
||||
initialValue: _query,
|
||||
suggestions: widget.tagSuggestions,
|
||||
onChanged: (value) {
|
||||
_query = value;
|
||||
widget.onSearchChanged(value);
|
||||
},
|
||||
onSubmitted: widget.onSearch,
|
||||
onSuggestionApplied: (value) {
|
||||
_query = value;
|
||||
widget.onSuggestionTap(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
_HorizontalWheelScroller(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
builder: (controller) => ListView(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
primary: false,
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Providers',
|
||||
@@ -230,10 +228,12 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
_HorizontalWheelScroller(
|
||||
height: 36,
|
||||
child: ListView(
|
||||
builder: (controller) => ListView(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
primary: false,
|
||||
children: [
|
||||
for (final period in TopPeriodFilter.values) ...[
|
||||
ChoiceChip(
|
||||
@@ -246,59 +246,53 @@ class _FeedToolbarState extends State<FeedToolbar> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.tagSuggestions.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.tagSuggestions.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 6),
|
||||
itemBuilder: (context, index) => ActionChip(
|
||||
label: _SuggestionLabel(widget.tagSuggestions[index]),
|
||||
onPressed: () =>
|
||||
_applySuggestion(widget.tagSuggestions[index].name),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _applySuggestion(String suggestion) {
|
||||
final tokens = _query.trim().isEmpty
|
||||
? <String>[]
|
||||
: _query.trim().split(RegExp(r'\s+'));
|
||||
if (tokens.isEmpty) {
|
||||
tokens.add(suggestion);
|
||||
} else {
|
||||
tokens[tokens.length - 1] = suggestion;
|
||||
}
|
||||
final next = tokens.toSet().join(' ');
|
||||
setState(() => _query = next);
|
||||
widget.onSuggestionTap(next);
|
||||
}
|
||||
}
|
||||
|
||||
class _SuggestionLabel extends StatelessWidget {
|
||||
const _SuggestionLabel(this.suggestion);
|
||||
class _HorizontalWheelScroller extends StatefulWidget {
|
||||
const _HorizontalWheelScroller({
|
||||
required this.builder,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final TagSuggestion suggestion;
|
||||
final Widget Function(ScrollController controller) builder;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
State<_HorizontalWheelScroller> createState() =>
|
||||
_HorizontalWheelScrollerState();
|
||||
}
|
||||
|
||||
class _HorizontalWheelScrollerState extends State<_HorizontalWheelScroller> {
|
||||
final _controller = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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,
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: Listener(
|
||||
onPointerSignal: (event) {
|
||||
if (event is! PointerScrollEvent || !_controller.hasClients) return;
|
||||
final target = (_controller.offset + event.scrollDelta.dy)
|
||||
.clamp(0.0, _controller.position.maxScrollExtent)
|
||||
.toDouble();
|
||||
_controller.jumpTo(target);
|
||||
},
|
||||
child: Scrollbar(
|
||||
controller: _controller,
|
||||
thumbVisibility: false,
|
||||
child: widget.builder(_controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user