|
@@ -1,8 +1,30 @@
|
|
|
+import 'dart:async';
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
import 'package:veloe_kemono_party_flutter/main.dart';
|
|
|
import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart';
|
|
|
|
|
|
+final searchQueryProvider = StateProvider.autoDispose<String>((ref) => '');
|
|
|
+final debouncedSearchQueryProvider = Provider.autoDispose<String>((ref) {
|
|
|
+ final query = ref.watch(searchQueryProvider);
|
|
|
+ final debouncer = Debouncer(delay: const Duration(milliseconds: 500));
|
|
|
+ debouncer.run(() {});
|
|
|
+ return query;
|
|
|
+});
|
|
|
+
|
|
|
+// Add Debouncer class
|
|
|
+class Debouncer {
|
|
|
+ final Duration delay;
|
|
|
+ Timer? _timer;
|
|
|
+
|
|
|
+ Debouncer({required this.delay});
|
|
|
+
|
|
|
+ void run(VoidCallback action) {
|
|
|
+ _timer?.cancel();
|
|
|
+ _timer = Timer(delay, action);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
class PostsScreen extends ConsumerStatefulWidget {
|
|
|
final String creatorId;
|
|
|
final String service;
|
|
@@ -20,27 +42,41 @@ class PostsScreen extends ConsumerStatefulWidget {
|
|
|
}
|
|
|
|
|
|
class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin {
|
|
|
+ late final TextEditingController _searchController;
|
|
|
+
|
|
|
@override
|
|
|
void initState() {
|
|
|
super.initState();
|
|
|
+ _searchController = TextEditingController();
|
|
|
Future.microtask(() => ref.read(postsProvider((
|
|
|
- widget.creatorId, // Direct access now valid
|
|
|
- widget.service
|
|
|
+ widget.creatorId,
|
|
|
+ widget.service,
|
|
|
+ ref.read(searchQueryProvider) // Initial query
|
|
|
)).notifier).loadInitial());
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
- Widget build(BuildContext context) {
|
|
|
- final postsAsync = ref.watch(postsProvider((widget.creatorId, widget.service)));
|
|
|
+ void dispose() {
|
|
|
+ _searchController.dispose();
|
|
|
+ super.dispose();
|
|
|
+ }
|
|
|
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ final currentQuery = ref.watch(debouncedSearchQueryProvider);
|
|
|
+ final postsAsync = ref.watch(postsProvider((
|
|
|
+ widget.creatorId,
|
|
|
+ widget.service,
|
|
|
+ currentQuery
|
|
|
+ )));
|
|
|
return Scaffold(
|
|
|
- appBar: widget.withAppBar ? AppBar(
|
|
|
- title: const Text('Posts'),
|
|
|
- leading: IconButton(
|
|
|
+ appBar: AppBar(
|
|
|
+ title: _buildSearchField(),
|
|
|
+ leading: widget.withAppBar ? IconButton(
|
|
|
icon: const Icon(Icons.arrow_back),
|
|
|
onPressed: () => Navigator.pop(context),
|
|
|
- ),
|
|
|
- ) : null,
|
|
|
+ ) : null,
|
|
|
+ ),
|
|
|
body: postsAsync.when(
|
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
|
error: (error, _) => Center(
|
|
@@ -49,7 +85,7 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
|
|
|
children: [
|
|
|
Text('Error: ${error.toString()}'),
|
|
|
ElevatedButton(
|
|
|
- onPressed: () => ref.invalidate(postsProvider((widget.creatorId, widget.service))),
|
|
|
+ onPressed: () => ref.invalidate(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))),
|
|
|
child: const Text('Retry'),
|
|
|
)
|
|
|
],
|
|
@@ -84,8 +120,25 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ Widget _buildSearchField() {
|
|
|
+ return TextField(
|
|
|
+ controller: _searchController,
|
|
|
+ decoration: InputDecoration(
|
|
|
+ hintText: 'Search posts...',
|
|
|
+ suffixIcon: IconButton(
|
|
|
+ icon: const Icon(Icons.clear),
|
|
|
+ onPressed: () {
|
|
|
+ _searchController.clear();
|
|
|
+ ref.read(searchQueryProvider.notifier).state = '';
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ onChanged: (value) => ref.read(searchQueryProvider.notifier).state = value,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
Widget _buildLoadMoreIndicator() {
|
|
|
- return ref.watch(postsProvider((widget.creatorId, widget.service))).maybeWhen(
|
|
|
+ return ref.watch(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))).maybeWhen(
|
|
|
loading: () => const Padding(
|
|
|
padding: EdgeInsets.all(16),
|
|
|
child: Center(child: CircularProgressIndicator()),
|
|
@@ -94,7 +147,7 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
|
|
|
children: [
|
|
|
Text('Load more failed: ${error.toString()}'),
|
|
|
ElevatedButton(
|
|
|
- onPressed: () => ref.read(postsProvider((widget.creatorId, widget.service)).notifier).loadNext(),
|
|
|
+ onPressed: () => ref.read(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider))).notifier).loadNext(),
|
|
|
child: const Text('Retry'),
|
|
|
)
|
|
|
],
|
|
@@ -124,20 +177,24 @@ mixin PaginationMixin<T extends ConsumerStatefulWidget> on ConsumerState<T> {
|
|
|
PostsScreen get postsWidget => widget as PostsScreen;
|
|
|
|
|
|
Future<void> _loadNextPage() async {
|
|
|
- if (_isLoading) return;
|
|
|
+ if (_isLoading || !mounted) return;
|
|
|
_isLoading = true;
|
|
|
|
|
|
final notifier = ref.read(postsProvider((
|
|
|
postsWidget.creatorId, // Use converted widget reference
|
|
|
- postsWidget.service
|
|
|
+ postsWidget.service,
|
|
|
+ ref.read(searchQueryProvider)
|
|
|
)).notifier);
|
|
|
-
|
|
|
+
|
|
|
+ if (!notifier.mounted) return;
|
|
|
+
|
|
|
await notifier.loadNext();
|
|
|
_isLoading = false;
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void dispose() {
|
|
|
+ _scrollController.removeListener(_scrollListener);
|
|
|
_scrollController.dispose();
|
|
|
super.dispose();
|
|
|
}
|