feat: display images within posts in UI
This commit is contained in:
parent
55c2d231c7
commit
2e43ea0dea
@ -31,6 +31,7 @@ class Post {
|
||||
final int comments;
|
||||
final DateTime creationDate;
|
||||
final bool liked;
|
||||
final List<String>? images;
|
||||
|
||||
Post({
|
||||
required this.id,
|
||||
@ -41,6 +42,7 @@ class Post {
|
||||
required this.comments,
|
||||
required this.creationDate,
|
||||
this.liked = false,
|
||||
this.images,
|
||||
});
|
||||
|
||||
factory Post.fromJson(Map<String, dynamic> json) {
|
||||
@ -53,6 +55,7 @@ class Post {
|
||||
comments: int.tryParse(json['comments']?.toString() ?? '0') ?? 0,
|
||||
creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()),
|
||||
liked: json['liked'] == true,
|
||||
images: json['images'] != null ? List<String>.from(json['images']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
@ -66,6 +69,7 @@ class Post {
|
||||
'comments': comments,
|
||||
'creationDate': creationDate.toIso8601String(),
|
||||
'liked': liked,
|
||||
'images': images,
|
||||
};
|
||||
}
|
||||
|
||||
@ -78,6 +82,7 @@ class Post {
|
||||
int? comments,
|
||||
DateTime? creationDate,
|
||||
bool? liked,
|
||||
List<String>? images,
|
||||
}) {
|
||||
return Post(
|
||||
id: id ?? this.id,
|
||||
@ -88,6 +93,7 @@ class Post {
|
||||
comments: comments ?? this.comments,
|
||||
creationDate: creationDate ?? this.creationDate,
|
||||
liked: liked ?? this.liked,
|
||||
images: images ?? this.images,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -129,6 +135,7 @@ class DetailedPost {
|
||||
final bool liked;
|
||||
final DateTime creationDate;
|
||||
final List<LikedUser> likedUsers;
|
||||
final List<String>? images;
|
||||
|
||||
DetailedPost({
|
||||
required this.id,
|
||||
@ -139,6 +146,7 @@ class DetailedPost {
|
||||
required this.liked,
|
||||
required this.creationDate,
|
||||
required this.likedUsers,
|
||||
this.images,
|
||||
});
|
||||
|
||||
factory DetailedPost.fromJson(Map<String, dynamic> json) {
|
||||
@ -153,6 +161,7 @@ class DetailedPost {
|
||||
likedUsers: (json['likedUsers'] as List?)
|
||||
?.map((user) => LikedUser.fromJson(user))
|
||||
.toList() ?? [],
|
||||
images: json['images'] != null ? List<String>.from(json['images']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
@ -166,6 +175,7 @@ class DetailedPost {
|
||||
'liked': liked,
|
||||
'creationDate': creationDate.toIso8601String(),
|
||||
'likedUsers': likedUsers.map((user) => user.toJson()).toList(),
|
||||
'images': images,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ class PostsListState extends State<PostsList> {
|
||||
// Only set up stream for main feed posts, not user posts
|
||||
print('Setting up posts stream for main feed');
|
||||
_postsStreamSubscription?.cancel(); // Cancel any existing subscription
|
||||
|
||||
|
||||
_postsStreamSubscription = PostService.getPostsStream().listen(
|
||||
(updatedPosts) {
|
||||
print('📱 Posts stream received data: ${updatedPosts.length} posts');
|
||||
@ -59,7 +59,9 @@ class PostsListState extends State<PostsList> {
|
||||
_isLoading = false;
|
||||
_errorMessage = '';
|
||||
});
|
||||
print('🎯 Posts UI updated with ${updatedPosts.length} posts - setState called');
|
||||
print(
|
||||
'🎯 Posts UI updated with ${updatedPosts.length} posts - setState called',
|
||||
);
|
||||
} else {
|
||||
print('⚠️ Widget not mounted, skipping UI update');
|
||||
}
|
||||
@ -73,10 +75,10 @@ class PostsListState extends State<PostsList> {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
// Also trigger an initial load to populate the stream
|
||||
_loadPosts();
|
||||
|
||||
|
||||
// Trigger stream with any existing cached data
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
@ -88,7 +90,7 @@ class PostsListState extends State<PostsList> {
|
||||
Future<void> _loadPosts({bool forceRefresh = false}) async {
|
||||
// Don't show loading for cached data unless forcing refresh
|
||||
final isInitialLoad = _posts.isEmpty;
|
||||
|
||||
|
||||
if (isInitialLoad || forceRefresh) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@ -217,6 +219,167 @@ class PostsListState extends State<PostsList> {
|
||||
}
|
||||
}
|
||||
|
||||
class PostImageViewer extends StatelessWidget {
|
||||
final String imageUrl;
|
||||
|
||||
const PostImageViewer({Key? key, required this.imageUrl}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.white),
|
||||
),
|
||||
body: Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: InteractiveViewer(
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.broken_image, size: 64, color: Colors.white54),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Failed to load image',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostImagesCarousel extends StatelessWidget {
|
||||
final List<String> images;
|
||||
final Function(String imageUrl)? onImageTap;
|
||||
|
||||
const PostImagesCarousel({Key? key, required this.images, this.onImageTap})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (images.isEmpty) return SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
height: 200,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: images.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
width: 280,
|
||||
margin: EdgeInsets.only(right: index < images.length - 1 ? 12 : 0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (onImageTap != null) {
|
||||
onImageTap!(images[index]);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
PostImageViewer(imageUrl: images[index]),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
images[index],
|
||||
fit: BoxFit.cover,
|
||||
width: 280,
|
||||
height: 200,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
width: 280,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
print('Image load error: $error');
|
||||
print('Stack trace: $stackTrace');
|
||||
print('Image URL: ${images[index]}');
|
||||
return Container(
|
||||
width: 280,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.broken_image,
|
||||
size: 48,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Failed to load image',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostCard extends StatefulWidget {
|
||||
final Post post;
|
||||
|
||||
@ -240,7 +403,7 @@ class _PostCardState extends State<PostCard> {
|
||||
void didUpdateWidget(PostCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Force update current post to reflect any changes
|
||||
if (oldWidget.post.id != widget.post.id ||
|
||||
if (oldWidget.post.id != widget.post.id ||
|
||||
oldWidget.post.likes != widget.post.likes ||
|
||||
oldWidget.post.liked != widget.post.liked ||
|
||||
oldWidget.post.comments != widget.post.comments) {
|
||||
@ -422,6 +585,11 @@ class _PostCardState extends State<PostCard> {
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
if (_currentPost.images != null &&
|
||||
_currentPost.images!.isNotEmpty) ...[
|
||||
SizedBox(height: 12),
|
||||
PostImagesCarousel(images: _currentPost.images!),
|
||||
],
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
@ -525,10 +693,12 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
|
||||
void _setupUserPostsStream() {
|
||||
print('Setting up user posts stream for profile');
|
||||
_userPostsStreamSubscription?.cancel(); // Cancel any existing subscription
|
||||
|
||||
|
||||
_userPostsStreamSubscription = PostService.getUserPostsStream().listen(
|
||||
(updatedPosts) {
|
||||
print('📱 User posts stream received data: ${updatedPosts.length} posts');
|
||||
print(
|
||||
'📱 User posts stream received data: ${updatedPosts.length} posts',
|
||||
);
|
||||
if (mounted) {
|
||||
// Update the posts directly instead of fetching again
|
||||
setState(() {
|
||||
@ -541,7 +711,9 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
|
||||
final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes);
|
||||
widget.onStatsUpdate!(_posts.length, totalLikes);
|
||||
}
|
||||
print('🎯 User posts UI updated with ${updatedPosts.length} posts - setState called');
|
||||
print(
|
||||
'🎯 User posts UI updated with ${updatedPosts.length} posts - setState called',
|
||||
);
|
||||
} else {
|
||||
print('⚠️ Widget not mounted, skipping UI update');
|
||||
}
|
||||
@ -555,7 +727,7 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
// Trigger stream with any existing cached data
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
@ -567,7 +739,7 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
|
||||
Future<void> _loadPosts() async {
|
||||
// Don't show loading for cached data unless it's the initial load
|
||||
final isInitialLoad = _posts.isEmpty;
|
||||
|
||||
|
||||
if (isInitialLoad) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@ -583,7 +755,10 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
|
||||
_posts = result['posts'];
|
||||
// Update parent stats
|
||||
if (widget.onStatsUpdate != null) {
|
||||
final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes);
|
||||
final totalLikes = _posts.fold(
|
||||
0,
|
||||
(sum, post) => sum + post.likes,
|
||||
);
|
||||
widget.onStatsUpdate!(_posts.length, totalLikes);
|
||||
}
|
||||
_errorMessage = '';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user