posts_screen.dart 5.8 KB

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