Main coves client
1import 'package:flutter/material.dart';
2import 'package:provider/provider.dart';
3
4import '../../constants/app_colors.dart';
5import '../../models/comment.dart';
6import '../../models/post.dart';
7import '../../providers/comments_provider.dart';
8import '../../utils/error_messages.dart';
9import '../../widgets/comment_thread.dart';
10import '../../widgets/comments_header.dart';
11import '../../widgets/loading_error_states.dart';
12import '../../widgets/post_card.dart';
13
14/// Post Detail Screen
15///
16/// Displays a full post with its comments.
17/// Architecture: Standalone screen for route destination and PageView child.
18///
19/// Features:
20/// - Full post display (reuses PostCard widget)
21/// - Sort selector (Hot/Top/New) using dropdown
22/// - Comment list with ListView.builder for performance
23/// - Pull-to-refresh with RefreshIndicator
24/// - Loading, empty, and error states
25/// - Automatic comment loading on screen init
26class PostDetailScreen extends StatefulWidget {
27 const PostDetailScreen({required this.post, super.key});
28
29 /// Post to display (passed via route extras)
30 final FeedViewPost post;
31
32 @override
33 State<PostDetailScreen> createState() => _PostDetailScreenState();
34}
35
36class _PostDetailScreenState extends State<PostDetailScreen> {
37 final ScrollController _scrollController = ScrollController();
38
39 // Current sort option
40 String _currentSort = 'hot';
41
42 @override
43 void initState() {
44 super.initState();
45
46 // Initialize scroll controller for pagination
47 _scrollController.addListener(_onScroll);
48
49 // Load comments after frame is built using provider from tree
50 WidgetsBinding.instance.addPostFrameCallback((_) {
51 if (mounted) {
52 _loadComments();
53 }
54 });
55 }
56
57 @override
58 void dispose() {
59 _scrollController.dispose();
60 super.dispose();
61 }
62
63 /// Load comments for the current post
64 void _loadComments() {
65 context.read<CommentsProvider>().loadComments(
66 postUri: widget.post.post.uri,
67 refresh: true,
68 );
69 }
70
71 /// Handle sort changes from dropdown
72 Future<void> _onSortChanged(String newSort) async {
73 final previousSort = _currentSort;
74
75 setState(() {
76 _currentSort = newSort;
77 });
78
79 final commentsProvider = context.read<CommentsProvider>();
80 final success = await commentsProvider.setSortOption(newSort);
81
82 // Show error snackbar and revert UI if sort change failed
83 if (!success && mounted) {
84 setState(() {
85 _currentSort = previousSort;
86 });
87
88 ScaffoldMessenger.of(context).showSnackBar(
89 SnackBar(
90 content: const Text('Failed to change sort order. Please try again.'),
91 backgroundColor: AppColors.primary,
92 behavior: SnackBarBehavior.floating,
93 duration: const Duration(seconds: 3),
94 action: SnackBarAction(
95 label: 'Retry',
96 textColor: AppColors.textPrimary,
97 onPressed: () {
98 _onSortChanged(newSort);
99 },
100 ),
101 ),
102 );
103 }
104 }
105
106 /// Handle scroll for pagination
107 void _onScroll() {
108 if (_scrollController.position.pixels >=
109 _scrollController.position.maxScrollExtent - 200) {
110 context.read<CommentsProvider>().loadMoreComments();
111 }
112 }
113
114 /// Handle pull-to-refresh
115 Future<void> _onRefresh() async {
116 final commentsProvider = context.read<CommentsProvider>();
117 await commentsProvider.refreshComments();
118 }
119
120 @override
121 Widget build(BuildContext context) {
122 return Scaffold(
123 backgroundColor: AppColors.background,
124 appBar: AppBar(
125 backgroundColor: AppColors.background,
126 foregroundColor: AppColors.textPrimary,
127 title: Text(widget.post.post.title ?? 'Post'),
128 elevation: 0,
129 ),
130 body: SafeArea(
131 // Explicitly set bottom to prevent iOS home indicator overlap
132 bottom: true,
133 child: _buildContent(),
134 ),
135 );
136 }
137
138
139 /// Build main content area
140 Widget _buildContent() {
141 // Use Consumer to rebuild when comments provider changes
142 return Consumer<CommentsProvider>(
143 builder: (context, commentsProvider, child) {
144 final isLoading = commentsProvider.isLoading;
145 final error = commentsProvider.error;
146 final comments = commentsProvider.comments;
147 final isLoadingMore = commentsProvider.isLoadingMore;
148
149 // Loading state (only show full-screen loader for initial load)
150 if (isLoading && comments.isEmpty) {
151 return const FullScreenLoading();
152 }
153
154 // Error state (only show full-screen error when no comments loaded yet)
155 if (error != null && comments.isEmpty) {
156 return FullScreenError(
157 title: 'Failed to load comments',
158 message: ErrorMessages.getUserFriendly(error),
159 onRetry: commentsProvider.retry,
160 );
161 }
162
163 // Content with RefreshIndicator
164 return RefreshIndicator(
165 onRefresh: _onRefresh,
166 color: AppColors.primary,
167 child: ListView.builder(
168 controller: _scrollController,
169 // Post + comments + loading indicator
170 itemCount:
171 1 + comments.length + (isLoadingMore || error != null ? 1 : 0),
172 itemBuilder: (context, index) {
173 // Post card (index 0)
174 if (index == 0) {
175 return Column(
176 children: [
177 // Reuse PostCard (hide comment button in detail view)
178 // Use ValueListenableBuilder to only rebuild when time changes
179 _PostHeader(
180 post: widget.post,
181 currentTimeNotifier: commentsProvider.currentTimeNotifier,
182 ),
183 // Comments header with sort dropdown
184 CommentsHeader(
185 commentCount: comments.length,
186 currentSort: _currentSort,
187 onSortChanged: _onSortChanged,
188 ),
189 ],
190 );
191 }
192
193 // Loading indicator or error at the end
194 if (index == comments.length + 1) {
195 if (isLoadingMore) {
196 return const InlineLoading();
197 }
198 if (error != null) {
199 return InlineError(
200 message: ErrorMessages.getUserFriendly(error),
201 onRetry: () {
202 commentsProvider
203 ..clearError()
204 ..loadMoreComments();
205 },
206 );
207 }
208 }
209
210 // Comment item - use existing CommentThread widget
211 final comment = comments[index - 1];
212 return _CommentItem(
213 comment: comment,
214 currentTimeNotifier: commentsProvider.currentTimeNotifier,
215 );
216 },
217 ),
218 );
219 },
220 );
221 }
222
223}
224
225/// Post header widget that only rebuilds when time changes
226///
227/// Extracted to prevent unnecessary rebuilds when comment list changes.
228/// Uses ValueListenableBuilder to listen only to time updates.
229class _PostHeader extends StatelessWidget {
230 const _PostHeader({
231 required this.post,
232 required this.currentTimeNotifier,
233 });
234
235 final FeedViewPost post;
236 final ValueNotifier<DateTime?> currentTimeNotifier;
237
238 @override
239 Widget build(BuildContext context) {
240 return ValueListenableBuilder<DateTime?>(
241 valueListenable: currentTimeNotifier,
242 builder: (context, currentTime, child) {
243 return PostCard(
244 post: post,
245 currentTime: currentTime,
246 showCommentButton: false,
247 disableNavigation: true,
248 );
249 },
250 );
251 }
252}
253
254/// Comment item wrapper that only rebuilds when time changes
255///
256/// Uses ValueListenableBuilder to prevent rebuilds when unrelated
257/// provider state changes (like loading state or error state).
258class _CommentItem extends StatelessWidget {
259 const _CommentItem({
260 required this.comment,
261 required this.currentTimeNotifier,
262 });
263
264 final ThreadViewComment comment;
265 final ValueNotifier<DateTime?> currentTimeNotifier;
266
267 @override
268 Widget build(BuildContext context) {
269 return ValueListenableBuilder<DateTime?>(
270 valueListenable: currentTimeNotifier,
271 builder: (context, currentTime, child) {
272 return CommentThread(
273 thread: comment,
274 currentTime: currentTime,
275 maxDepth: 6,
276 );
277 },
278 );
279 }
280}