posts_screen.dart 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. ),
  99. childCount: allPosts.length,
  100. ),
  101. ),
  102. SliverToBoxAdapter(
  103. child: _buildLoadMoreIndicator(),
  104. ),
  105. ],
  106. ),
  107. ),
  108. ),
  109. );
  110. }
  111. Widget _buildSearchField() {
  112. return TextField(
  113. controller: _searchController,
  114. decoration: InputDecoration(
  115. hintText: 'Search posts...',
  116. suffixIcon: IconButton(
  117. icon: const Icon(Icons.clear),
  118. onPressed: () {
  119. _searchController.clear();
  120. ref.read(searchQueryProvider.notifier).state = '';
  121. },
  122. ),
  123. ),
  124. onChanged: (value) => ref.read(searchQueryProvider.notifier).state = value,
  125. );
  126. }
  127. Widget _buildLoadMoreIndicator() {
  128. return ref.watch(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider)))).maybeWhen(
  129. loading: () => const Padding(
  130. padding: EdgeInsets.all(16),
  131. child: Center(child: CircularProgressIndicator()),
  132. ),
  133. error: (error, _) => Column(
  134. children: [
  135. Text('Load more failed: ${error.toString()}'),
  136. ElevatedButton(
  137. onPressed: () => ref.read(postsProvider((widget.creatorId, widget.service,ref.read(searchQueryProvider))).notifier).loadNext(),
  138. child: const Text('Retry'),
  139. )
  140. ],
  141. ),
  142. orElse: () => const SizedBox.shrink(),
  143. );
  144. }
  145. }
  146. mixin PaginationMixin<T extends ConsumerStatefulWidget> on ConsumerState<T> {
  147. final ScrollController _scrollController = ScrollController();
  148. bool _isLoading = false;
  149. @override
  150. void initState() {
  151. super.initState();
  152. _scrollController.addListener(_scrollListener);
  153. }
  154. void _scrollListener() {
  155. if (_scrollController.position.pixels >=
  156. _scrollController.position.maxScrollExtent * 0.8) {
  157. _loadNextPage();
  158. }
  159. }
  160. PostsScreen get postsWidget => widget as PostsScreen;
  161. Future<void> _loadNextPage() async {
  162. if (_isLoading || !mounted) return;
  163. _isLoading = true;
  164. final notifier = ref.read(postsProvider((
  165. postsWidget.creatorId, // Use converted widget reference
  166. postsWidget.service,
  167. ref.read(searchQueryProvider)
  168. )).notifier);
  169. if (!notifier.mounted) return;
  170. await notifier.loadNext();
  171. _isLoading = false;
  172. }
  173. @override
  174. void dispose() {
  175. _scrollController.removeListener(_scrollListener);
  176. _scrollController.dispose();
  177. super.dispose();
  178. }
  179. }