posts_screen.dart 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:veloe_kemono_party_flutter/app_layout.dart';
  5. import 'package:veloe_kemono_party_flutter/main.dart';
  6. import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart';
  7. final searchQueryProvider = StateProvider.autoDispose<String>((ref) => '');
  8. final debouncedSearchQueryProvider = Provider.autoDispose<String>((ref) {
  9. final query = ref.watch(searchQueryProvider);
  10. final debouncer = Debouncer(delay: const Duration(milliseconds: 500));
  11. debouncer.run(() {});
  12. return query;
  13. });
  14. // Add Debouncer class
  15. class Debouncer {
  16. final Duration delay;
  17. Timer? _timer;
  18. Debouncer({required this.delay});
  19. void run(VoidCallback action) {
  20. _timer?.cancel();
  21. _timer = Timer(delay, action);
  22. }
  23. }
  24. class PostsScreen extends ConsumerStatefulWidget {
  25. final String creatorId;
  26. final String service;
  27. final bool withAppBar;
  28. const PostsScreen({
  29. super.key,
  30. required this.creatorId,
  31. required this.service,
  32. this.withAppBar = true
  33. });
  34. @override
  35. ConsumerState<PostsScreen> createState() => _PostsScreenState();
  36. }
  37. class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin {
  38. late final TextEditingController _searchController;
  39. @override
  40. void initState() {
  41. super.initState();
  42. _searchController = TextEditingController();
  43. Future.microtask(() => ref.read(postsProvider((
  44. widget.creatorId,
  45. widget.service,
  46. ref.read(searchQueryProvider) // Initial query
  47. )).notifier).loadInitial());
  48. }
  49. @override
  50. void dispose() {
  51. _searchController.dispose();
  52. super.dispose();
  53. }
  54. @override
  55. Widget build(BuildContext context) {
  56. final currentQuery = ref.watch(debouncedSearchQueryProvider);
  57. final postsAsync = ref.watch(postsProvider((
  58. widget.creatorId,
  59. widget.service,
  60. currentQuery
  61. )));
  62. return Scaffold(
  63. appBar: AppBar(
  64. title: _buildSearchField(),
  65. leading: widget.withAppBar ?
  66. IconButton(
  67. icon: const Icon(Icons.arrow_back),
  68. onPressed: () => Navigator.pop(context),
  69. ) :
  70. Builder(
  71. builder: (context) => IconButton(
  72. icon: const Icon(Icons.menu),
  73. onPressed: () => AppLayout.openDrawer(context),
  74. ),
  75. ),
  76. ),
  77. body: postsAsync.when(
  78. loading: () => const Center(child: CircularProgressIndicator()),
  79. error: (error, _) => Center(
  80. child: Column(
  81. mainAxisSize: MainAxisSize.min,
  82. children: [
  83. Text('Error: ${error.toString()}'),
  84. ElevatedButton(
  85. onPressed: () => ref.invalidate(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))),
  86. child: const Text('Retry'),
  87. )
  88. ],
  89. ),
  90. ),
  91. data: (allPosts) => NotificationListener<ScrollNotification>(
  92. onNotification: (notification) {
  93. if (notification is ScrollUpdateNotification) {
  94. _scrollListener();
  95. }
  96. return false;
  97. },
  98. child: CustomScrollView(
  99. controller: _scrollController,
  100. slivers: [
  101. SliverList(
  102. delegate: SliverChildBuilderDelegate(
  103. (context, index) => PostContainer(
  104. key: ValueKey(allPosts[index].id),
  105. post: allPosts[index],
  106. postIndex: index,
  107. allPosts: allPosts,
  108. loadMorePosts: () async { _loadNextPage(); },
  109. ),
  110. childCount: allPosts.length,
  111. ),
  112. ),
  113. SliverToBoxAdapter(
  114. child: _buildLoadMoreIndicator(),
  115. ),
  116. ],
  117. ),
  118. ),
  119. ),
  120. );
  121. }
  122. Widget _buildSearchField() {
  123. return TextField(
  124. controller: _searchController,
  125. decoration: InputDecoration(
  126. hintText: 'Search posts...',
  127. suffixIcon: IconButton(
  128. icon: const Icon(Icons.clear),
  129. onPressed: () {
  130. _searchController.clear();
  131. ref.read(searchQueryProvider.notifier).state = '';
  132. },
  133. ),
  134. ),
  135. onChanged: (value) => ref.read(searchQueryProvider.notifier).state = value,
  136. );
  137. }
  138. Widget _buildLoadMoreIndicator() {
  139. return ref.watch(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))).maybeWhen(
  140. loading: () => const Padding(
  141. padding: EdgeInsets.all(16),
  142. child: Center(child: CircularProgressIndicator()),
  143. ),
  144. error: (error, _) => Column(
  145. children: [
  146. Text('Load more failed: ${error.toString()}'),
  147. ElevatedButton(
  148. onPressed: () => ref.read(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider))).notifier).loadNext(),
  149. child: const Text('Retry'),
  150. )
  151. ],
  152. ),
  153. orElse: () => const SizedBox.shrink(),
  154. );
  155. }
  156. }
  157. mixin PaginationMixin<T extends ConsumerStatefulWidget> on ConsumerState<T> {
  158. final ScrollController _scrollController = ScrollController();
  159. bool _isLoading = false;
  160. @override
  161. void initState() {
  162. super.initState();
  163. _scrollController.addListener(_scrollListener);
  164. }
  165. void _scrollListener() {
  166. if (_scrollController.position.pixels >=
  167. _scrollController.position.maxScrollExtent * 0.8) {
  168. _loadNextPage();
  169. }
  170. }
  171. PostsScreen get postsWidget => widget as PostsScreen;
  172. Future<void> _loadNextPage() async {
  173. if (_isLoading || !mounted) return;
  174. _isLoading = true;
  175. final notifier = ref.read(postsProvider((
  176. postsWidget.creatorId,
  177. postsWidget.service,
  178. ref.read(searchQueryProvider)
  179. )).notifier);
  180. if (!notifier.mounted) return;
  181. await notifier.loadNext();
  182. _isLoading = false;
  183. }
  184. @override
  185. void dispose() {
  186. _scrollController.removeListener(_scrollListener);
  187. _scrollController.dispose();
  188. super.dispose();
  189. }
  190. }