···
1
+
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
3
+
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
5
+
import 'package:share_plus/share_plus.dart';
import '../../constants/app_colors.dart';
import '../../models/comment.dart';
import '../../models/post.dart';
10
+
import '../../providers/auth_provider.dart';
import '../../providers/comments_provider.dart';
12
+
import '../../providers/vote_provider.dart';
13
+
import '../../utils/community_handle_utils.dart';
import '../../utils/error_messages.dart';
import '../../widgets/comment_thread.dart';
import '../../widgets/comments_header.dart';
17
+
import '../../widgets/icons/share_icon.dart';
import '../../widgets/loading_error_states.dart';
19
+
import '../../widgets/post_action_bar.dart';
import '../../widgets/post_card.dart';
···
Widget build(BuildContext context) {
backgroundColor: AppColors.background,
125
-
backgroundColor: AppColors.background,
126
-
foregroundColor: AppColors.textPrimary,
127
-
title: Text(widget.post.post.title ?? 'Post'),
132
+
body: _buildContent(),
133
+
bottomNavigationBar: _buildActionBar(),
137
+
/// Build community title with avatar and handle
138
+
Widget _buildCommunityTitle() {
139
+
final community = widget.post.post.community;
140
+
final displayHandle = CommunityHandleUtils.formatHandleForDisplay(
145
+
mainAxisSize: MainAxisSize.min,
147
+
// Community avatar
148
+
if (community.avatar != null && community.avatar!.isNotEmpty)
150
+
borderRadius: BorderRadius.circular(16),
151
+
child: CachedNetworkImage(
152
+
imageUrl: community.avatar!,
156
+
placeholder: (context, url) => _buildFallbackAvatar(community),
158
+
(context, url, error) => _buildFallbackAvatar(community),
162
+
_buildFallbackAvatar(community),
163
+
const SizedBox(width: 8),
164
+
// Community handle with styled parts
165
+
if (displayHandle != null)
166
+
Flexible(child: _buildStyledHandle(displayHandle))
171
+
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
172
+
overflow: TextOverflow.ellipsis,
179
+
/// Build styled community handle with color-coded parts
180
+
Widget _buildStyledHandle(String displayHandle) {
181
+
// Format: !gaming@coves.social
182
+
final atIndex = displayHandle.indexOf('@');
183
+
final communityPart = displayHandle.substring(0, atIndex);
184
+
final instancePart = displayHandle.substring(atIndex);
190
+
text: communityPart,
191
+
style: const TextStyle(
192
+
color: AppColors.communityName,
194
+
fontWeight: FontWeight.w600,
198
+
text: instancePart,
200
+
color: AppColors.textSecondary.withValues(alpha: 0.8),
202
+
fontWeight: FontWeight.w600,
131
-
// Explicitly set bottom to prevent iOS home indicator overlap
133
-
child: _buildContent(),
207
+
overflow: TextOverflow.ellipsis,
211
+
/// Build fallback avatar with first letter
212
+
Widget _buildFallbackAvatar(CommunityRef community) {
213
+
final firstLetter = community.name.isNotEmpty ? community.name[0] : '?';
217
+
decoration: BoxDecoration(
218
+
color: AppColors.primary,
219
+
borderRadius: BorderRadius.circular(16),
223
+
firstLetter.toUpperCase(),
224
+
style: const TextStyle(
225
+
color: AppColors.textPrimary,
227
+
fontWeight: FontWeight.bold,
234
+
/// Handle share button tap
235
+
Future<void> _handleShare() async {
236
+
// Add haptic feedback
237
+
await HapticFeedback.lightImpact();
239
+
// TODO: Generate proper deep link URL when deep linking is implemented
240
+
final postUri = widget.post.post.uri;
241
+
final title = widget.post.post.title ?? 'Check out this post';
243
+
await Share.share('$title\n\n$postUri', subject: title);
246
+
/// Build bottom action bar with comment input and buttons
247
+
Widget _buildActionBar() {
248
+
return Consumer<VoteProvider>(
249
+
builder: (context, voteProvider, child) {
250
+
final isVoted = voteProvider.isLiked(widget.post.post.uri);
251
+
final adjustedScore = voteProvider.getAdjustedScore(
252
+
widget.post.post.uri,
253
+
widget.post.post.stats.score,
256
+
// Create a modified post with adjusted score for display
257
+
final displayPost = FeedViewPost(
259
+
uri: widget.post.post.uri,
260
+
cid: widget.post.post.cid,
261
+
rkey: widget.post.post.rkey,
262
+
author: widget.post.post.author,
263
+
community: widget.post.post.community,
264
+
createdAt: widget.post.post.createdAt,
265
+
indexedAt: widget.post.post.indexedAt,
266
+
text: widget.post.post.text,
267
+
title: widget.post.post.title,
269
+
upvotes: widget.post.post.stats.upvotes,
270
+
downvotes: widget.post.post.stats.downvotes,
271
+
score: adjustedScore,
272
+
commentCount: widget.post.post.stats.commentCount,
274
+
embed: widget.post.post.embed,
275
+
facets: widget.post.post.facets,
277
+
reason: widget.post.reason,
280
+
return PostActionBar(
284
+
// TODO: Open comment composer
285
+
ScaffoldMessenger.of(context).showSnackBar(
287
+
content: Text('Comment composer coming soon!'),
288
+
behavior: SnackBarBehavior.floating,
292
+
onVoteTap: () async {
293
+
// Check authentication
294
+
final authProvider = context.read<AuthProvider>();
295
+
if (!authProvider.isAuthenticated) {
296
+
ScaffoldMessenger.of(context).showSnackBar(
298
+
content: Text('Sign in to vote on posts'),
299
+
behavior: SnackBarBehavior.floating,
305
+
// Light haptic feedback on both like and unlike
306
+
await HapticFeedback.lightImpact();
309
+
final messenger = ScaffoldMessenger.of(context);
311
+
await voteProvider.toggleVote(
312
+
postUri: widget.post.post.uri,
313
+
postCid: widget.post.post.cid,
315
+
} on Exception catch (e) {
317
+
messenger.showSnackBar(
319
+
content: Text('Failed to vote: $e'),
320
+
behavior: SnackBarBehavior.floating,
327
+
// TODO: Add save functionality
328
+
ScaffoldMessenger.of(context).showSnackBar(
330
+
content: Text('Save feature coming soon!'),
331
+
behavior: SnackBarBehavior.floating,
/// Build main content area
···
163
-
// Content with RefreshIndicator
364
+
// Content with RefreshIndicator and floating SliverAppBar
color: AppColors.primary,
167
-
child: ListView.builder(
368
+
child: CustomScrollView(
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,
371
+
// Floating app bar that hides on scroll down, shows on scroll up
373
+
backgroundColor: AppColors.background,
374
+
surfaceTintColor: Colors.transparent,
375
+
foregroundColor: AppColors.textPrimary,
376
+
title: _buildCommunityTitle(),
377
+
centerTitle: false,
383
+
icon: const ShareIcon(color: AppColors.textPrimary),
384
+
onPressed: _handleShare,
390
+
// Post + comments + loading indicator
393
+
sliver: SliverList(
394
+
delegate: SliverChildBuilderDelegate(
396
+
// Post card (index 0)
400
+
// Reuse PostCard (hide comment button in detail view)
401
+
// Use ValueListenableBuilder to only rebuild when time changes
404
+
currentTimeNotifier:
405
+
commentsProvider.currentTimeNotifier,
407
+
// Comments header with sort dropdown
409
+
commentCount: comments.length,
410
+
currentSort: _currentSort,
411
+
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();
417
+
// Loading indicator or error at the end
418
+
if (index == comments.length + 1) {
419
+
if (isLoadingMore) {
420
+
return const InlineLoading();
422
+
if (error != null) {
423
+
return InlineError(
424
+
message: ErrorMessages.getUserFriendly(error),
428
+
..loadMoreComments();
210
-
// Comment item - use existing CommentThread widget
211
-
final comment = comments[index - 1];
212
-
return _CommentItem(
214
-
currentTimeNotifier: commentsProvider.currentTimeNotifier,
434
+
// Comment item - use existing CommentThread widget
435
+
final comment = comments[index - 1];
436
+
return _CommentItem(
438
+
currentTimeNotifier:
439
+
commentsProvider.currentTimeNotifier,
445
+
(isLoadingMore || error != null ? 1 : 0),
/// Post header widget that only rebuilds when time changes
···
/// Extracted to prevent unnecessary rebuilds when comment list changes.
/// Uses ValueListenableBuilder to listen only to time updates.
class _PostHeader extends StatelessWidget {
230
-
const _PostHeader({
231
-
required this.post,
232
-
required this.currentTimeNotifier,
462
+
const _PostHeader({required this.post, required this.currentTimeNotifier});
final ValueNotifier<DateTime?> currentTimeNotifier;
···
currentTime: currentTime,
showCommentButton: false,
477
+
showActions: false,