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}