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 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,
};
}
}

View File

@ -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 = '';