diff --git a/frontend/lib/models/post_models.dart b/frontend/lib/models/post_models.dart index 9d8a1ae..0e18493 100644 --- a/frontend/lib/models/post_models.dart +++ b/frontend/lib/models/post_models.dart @@ -31,6 +31,7 @@ class Post { final int comments; final DateTime creationDate; final bool liked; + final List? 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 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.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? 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 likedUsers; + final List? 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 json) { @@ -153,6 +161,7 @@ class DetailedPost { likedUsers: (json['likedUsers'] as List?) ?.map((user) => LikedUser.fromJson(user)) .toList() ?? [], + images: json['images'] != null ? List.from(json['images']) : null, ); } @@ -166,6 +175,7 @@ class DetailedPost { 'liked': liked, 'creationDate': creationDate.toIso8601String(), 'likedUsers': likedUsers.map((user) => user.toJson()).toList(), + 'images': images, }; } } diff --git a/frontend/lib/widgets/posts_list.dart b/frontend/lib/widgets/posts_list.dart index e87731e..edd4eed 100644 --- a/frontend/lib/widgets/posts_list.dart +++ b/frontend/lib/widgets/posts_list.dart @@ -48,7 +48,7 @@ class PostsListState extends State { // 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 { _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 { } }, ); - + // 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 { Future _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 { } } +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(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 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(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 { 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 { 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 { 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 { 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 { } }, ); - + // Trigger stream with any existing cached data Future.delayed(Duration(milliseconds: 100), () { if (mounted) { @@ -567,7 +739,7 @@ class _ProfilePostsListState extends State { Future _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 { _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 = '';