···
1
+
import 'package:flutter/material.dart';
2
+
import 'package:provider/provider.dart';
4
+
import '../../constants/app_colors.dart';
5
+
import '../../models/comment.dart';
6
+
import '../../models/post.dart';
7
+
import '../../providers/comments_provider.dart';
8
+
import '../../utils/error_messages.dart';
9
+
import '../../widgets/comment_thread.dart';
10
+
import '../../widgets/comments_header.dart';
11
+
import '../../widgets/loading_error_states.dart';
12
+
import '../../widgets/post_card.dart';
14
+
/// Post Detail Screen
16
+
/// Displays a full post with its comments.
17
+
/// Architecture: Standalone screen for route destination and PageView child.
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
26
+
class PostDetailScreen extends StatefulWidget {
27
+
const PostDetailScreen({required this.post, super.key});
29
+
/// Post to display (passed via route extras)
30
+
final FeedViewPost post;
33
+
State<PostDetailScreen> createState() => _PostDetailScreenState();
36
+
class _PostDetailScreenState extends State<PostDetailScreen> {
37
+
final ScrollController _scrollController = ScrollController();
39
+
// Current sort option
40
+
String _currentSort = 'hot';
46
+
// Initialize scroll controller for pagination
47
+
_scrollController.addListener(_onScroll);
49
+
// Load comments after frame is built using provider from tree
50
+
WidgetsBinding.instance.addPostFrameCallback((_) {
59
+
_scrollController.dispose();
63
+
/// Load comments for the current post
64
+
void _loadComments() {
65
+
context.read<CommentsProvider>().loadComments(
66
+
postUri: widget.post.post.uri,
71
+
/// Handle sort changes from dropdown
72
+
Future<void> _onSortChanged(String newSort) async {
73
+
final previousSort = _currentSort;
76
+
_currentSort = newSort;
79
+
final commentsProvider = context.read<CommentsProvider>();
80
+
final success = await commentsProvider.setSortOption(newSort);
82
+
// Show error snackbar and revert UI if sort change failed
83
+
if (!success && mounted) {
85
+
_currentSort = previousSort;
88
+
ScaffoldMessenger.of(context).showSnackBar(
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(
96
+
textColor: AppColors.textPrimary,
98
+
_onSortChanged(newSort);
106
+
/// Handle scroll for pagination
108
+
if (_scrollController.position.pixels >=
109
+
_scrollController.position.maxScrollExtent - 200) {
110
+
context.read<CommentsProvider>().loadMoreComments();
114
+
/// Handle pull-to-refresh
115
+
Future<void> _onRefresh() async {
116
+
final commentsProvider = context.read<CommentsProvider>();
117
+
await commentsProvider.refreshComments();
121
+
Widget build(BuildContext context) {
123
+
backgroundColor: AppColors.background,
125
+
backgroundColor: AppColors.background,
126
+
foregroundColor: AppColors.textPrimary,
127
+
title: Text(widget.post.post.title ?? 'Post'),
131
+
// Explicitly set bottom to prevent iOS home indicator overlap
133
+
child: _buildContent(),
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;
149
+
// Loading state (only show full-screen loader for initial load)
150
+
if (isLoading && comments.isEmpty) {
151
+
return const FullScreenLoading();
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,
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
171
+
1 + comments.length + (isLoadingMore || error != null ? 1 : 0),
172
+
itemBuilder: (context, index) {
173
+
// Post card (index 0)
177
+
// Reuse PostCard (hide comment button in detail view)
178
+
// Use ValueListenableBuilder to only rebuild when time changes
181
+
currentTimeNotifier: commentsProvider.currentTimeNotifier,
183
+
// Comments header with sort dropdown
185
+
commentCount: comments.length,
186
+
currentSort: _currentSort,
187
+
onSortChanged: _onSortChanged,
193
+
// Loading indicator or error at the end
194
+
if (index == comments.length + 1) {
195
+
if (isLoadingMore) {
196
+
return const InlineLoading();
198
+
if (error != null) {
199
+
return InlineError(
200
+
message: ErrorMessages.getUserFriendly(error),
204
+
..loadMoreComments();
210
+
// Comment item - use existing CommentThread widget
211
+
final comment = comments[index - 1];
212
+
return _CommentItem(
214
+
currentTimeNotifier: commentsProvider.currentTimeNotifier,
225
+
/// Post header widget that only rebuilds when time changes
227
+
/// Extracted to prevent unnecessary rebuilds when comment list changes.
228
+
/// Uses ValueListenableBuilder to listen only to time updates.
229
+
class _PostHeader extends StatelessWidget {
230
+
const _PostHeader({
231
+
required this.post,
232
+
required this.currentTimeNotifier,
235
+
final FeedViewPost post;
236
+
final ValueNotifier<DateTime?> currentTimeNotifier;
239
+
Widget build(BuildContext context) {
240
+
return ValueListenableBuilder<DateTime?>(
241
+
valueListenable: currentTimeNotifier,
242
+
builder: (context, currentTime, child) {
245
+
currentTime: currentTime,
246
+
showCommentButton: false,
247
+
disableNavigation: true,
254
+
/// Comment item wrapper that only rebuilds when time changes
256
+
/// Uses ValueListenableBuilder to prevent rebuilds when unrelated
257
+
/// provider state changes (like loading state or error state).
258
+
class _CommentItem extends StatelessWidget {
259
+
const _CommentItem({
260
+
required this.comment,
261
+
required this.currentTimeNotifier,
264
+
final ThreadViewComment comment;
265
+
final ValueNotifier<DateTime?> currentTimeNotifier;
268
+
Widget build(BuildContext context) {
269
+
return ValueListenableBuilder<DateTime?>(
270
+
valueListenable: currentTimeNotifier,
271
+
builder: (context, currentTime, child) {
272
+
return CommentThread(
274
+
currentTime: currentTime,