123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- 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<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;
- final bool withAppBar;
- const PostsScreen({
- super.key,
- required this.creatorId,
- required this.service,
- this.withAppBar = true
- });
- @override
- ConsumerState<PostsScreen> createState() => _PostsScreenState();
- }
- 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,
- 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<ScrollNotification>(
- 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<T extends ConsumerStatefulWidget> on ConsumerState<T> {
- 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<void> _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();
- }
- }
|