feat: display images within posts in UI

This commit is contained in:
sBubshait 2025-08-05 23:51:14 +03:00
parent 55c2d231c7
commit 2e43ea0dea
2 changed files with 197 additions and 12 deletions

View File

@ -31,6 +31,7 @@ class Post {
final int comments; final int comments;
final DateTime creationDate; final DateTime creationDate;
final bool liked; final bool liked;
final List<String>? images;
Post({ Post({
required this.id, required this.id,
@ -41,6 +42,7 @@ class Post {
required this.comments, required this.comments,
required this.creationDate, required this.creationDate,
this.liked = false, this.liked = false,
this.images,
}); });
factory Post.fromJson(Map<String, dynamic> json) { factory Post.fromJson(Map<String, dynamic> json) {
@ -53,6 +55,7 @@ class Post {
comments: int.tryParse(json['comments']?.toString() ?? '0') ?? 0, comments: int.tryParse(json['comments']?.toString() ?? '0') ?? 0,
creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()), creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()),
liked: json['liked'] == true, liked: json['liked'] == true,
images: json['images'] != null ? List<String>.from(json['images']) : null,
); );
} }
@ -66,6 +69,7 @@ class Post {
'comments': comments, 'comments': comments,
'creationDate': creationDate.toIso8601String(), 'creationDate': creationDate.toIso8601String(),
'liked': liked, 'liked': liked,
'images': images,
}; };
} }
@ -78,6 +82,7 @@ class Post {
int? comments, int? comments,
DateTime? creationDate, DateTime? creationDate,
bool? liked, bool? liked,
List<String>? images,
}) { }) {
return Post( return Post(
id: id ?? this.id, id: id ?? this.id,
@ -88,6 +93,7 @@ class Post {
comments: comments ?? this.comments, comments: comments ?? this.comments,
creationDate: creationDate ?? this.creationDate, creationDate: creationDate ?? this.creationDate,
liked: liked ?? this.liked, liked: liked ?? this.liked,
images: images ?? this.images,
); );
} }
} }
@ -129,6 +135,7 @@ class DetailedPost {
final bool liked; final bool liked;
final DateTime creationDate; final DateTime creationDate;
final List<LikedUser> likedUsers; final List<LikedUser> likedUsers;
final List<String>? images;
DetailedPost({ DetailedPost({
required this.id, required this.id,
@ -139,6 +146,7 @@ class DetailedPost {
required this.liked, required this.liked,
required this.creationDate, required this.creationDate,
required this.likedUsers, required this.likedUsers,
this.images,
}); });
factory DetailedPost.fromJson(Map<String, dynamic> json) { factory DetailedPost.fromJson(Map<String, dynamic> json) {
@ -153,6 +161,7 @@ class DetailedPost {
likedUsers: (json['likedUsers'] as List?) likedUsers: (json['likedUsers'] as List?)
?.map((user) => LikedUser.fromJson(user)) ?.map((user) => LikedUser.fromJson(user))
.toList() ?? [], .toList() ?? [],
images: json['images'] != null ? List<String>.from(json['images']) : null,
); );
} }
@ -166,6 +175,7 @@ class DetailedPost {
'liked': liked, 'liked': liked,
'creationDate': creationDate.toIso8601String(), 'creationDate': creationDate.toIso8601String(),
'likedUsers': likedUsers.map((user) => user.toJson()).toList(), 'likedUsers': likedUsers.map((user) => user.toJson()).toList(),
'images': images,
}; };
} }
} }

View File

@ -59,7 +59,9 @@ class PostsListState extends State<PostsList> {
_isLoading = false; _isLoading = false;
_errorMessage = ''; _errorMessage = '';
}); });
print('🎯 Posts UI updated with ${updatedPosts.length} posts - setState called'); print(
'🎯 Posts UI updated with ${updatedPosts.length} posts - setState called',
);
} else { } else {
print('⚠️ Widget not mounted, skipping UI update'); print('⚠️ Widget not mounted, skipping UI update');
} }
@ -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 { class PostCard extends StatefulWidget {
final Post post; final Post post;
@ -422,6 +585,11 @@ class _PostCardState extends State<PostCard> {
color: Colors.black87, color: Colors.black87,
), ),
), ),
if (_currentPost.images != null &&
_currentPost.images!.isNotEmpty) ...[
SizedBox(height: 12),
PostImagesCarousel(images: _currentPost.images!),
],
SizedBox(height: 16), SizedBox(height: 16),
Row( Row(
children: [ children: [
@ -528,7 +696,9 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
_userPostsStreamSubscription = PostService.getUserPostsStream().listen( _userPostsStreamSubscription = PostService.getUserPostsStream().listen(
(updatedPosts) { (updatedPosts) {
print('📱 User posts stream received data: ${updatedPosts.length} posts'); print(
'📱 User posts stream received data: ${updatedPosts.length} posts',
);
if (mounted) { if (mounted) {
// Update the posts directly instead of fetching again // Update the posts directly instead of fetching again
setState(() { setState(() {
@ -541,7 +711,9 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
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); 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 { } else {
print('⚠️ Widget not mounted, skipping UI update'); print('⚠️ Widget not mounted, skipping UI update');
} }
@ -583,7 +755,10 @@ class _ProfilePostsListState extends State<ProfilePostsList> {
_posts = result['posts']; _posts = result['posts'];
// Update parent stats // Update parent stats
if (widget.onStatsUpdate != null) { 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); widget.onStatsUpdate!(_posts.length, totalLikes);
} }
_errorMessage = ''; _errorMessage = '';