import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:veloe_kemono_party_flutter/app_layout.dart'; import 'package:veloe_kemono_party_flutter/main.dart'; import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart'; final searchQueryProvider = StateProvider.autoDispose((ref) => ''); final debouncedSearchQueryProvider = Provider.autoDispose((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; final bool withAppBar; const PostsScreen({ super.key, required this.creatorId, required this.service, this.withAppBar = true }); @override ConsumerState createState() => _PostsScreenState(); } class _PostsScreenState extends ConsumerState with PaginationMixin { late final TextEditingController _searchController; @override void initState() { super.initState(); _searchController = TextEditingController(); Future.microtask(() => ref.read(postsProvider(( widget.creatorId, widget.service, ref.read(searchQueryProvider) // Initial query )).notifier).loadInitial()); } @override 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: AppBar( title: _buildSearchField(), leading: widget.withAppBar ? IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), ) : Builder( builder: (context) => IconButton( icon: const Icon(Icons.menu), onPressed: () => AppLayout.openDrawer(context), ), ), ), body: postsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Error: ${error.toString()}'), ElevatedButton( onPressed: () => ref.invalidate(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))), child: const Text('Retry'), ) ], ), ), data: (allPosts) => NotificationListener( onNotification: (notification) { if (notification is ScrollUpdateNotification) { _scrollListener(); } return false; }, child: CustomScrollView( controller: _scrollController, slivers: [ SliverList( delegate: SliverChildBuilderDelegate( (context, index) => PostContainer( key: ValueKey(allPosts[index].id), post: allPosts[index], postIndex: index, allPosts: allPosts, loadMorePosts: () async { _loadNextPage(); }, ), childCount: allPosts.length, ), ), SliverToBoxAdapter( child: _buildLoadMoreIndicator(), ), ], ), ), ), ); } 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,ref.read(searchQueryProvider)))).maybeWhen( loading: () => const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ), error: (error, _) => Column( children: [ Text('Load more failed: ${error.toString()}'), ElevatedButton( onPressed: () => ref.read(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider))).notifier).loadNext(), child: const Text('Retry'), ) ], ), orElse: () => const SizedBox.shrink(), ); } } mixin PaginationMixin on ConsumerState { final ScrollController _scrollController = ScrollController(); bool _isLoading = false; @override void initState() { super.initState(); _scrollController.addListener(_scrollListener); } void _scrollListener() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.8) { _loadNextPage(); } } PostsScreen get postsWidget => widget as PostsScreen; Future _loadNextPage() async { if (_isLoading || !mounted) return; _isLoading = true; final notifier = ref.read(postsProvider(( postsWidget.creatorId, 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(); } }