import 'package:flutter/material.dart'; import 'dart:async'; import 'package:readmore/readmore.dart'; class PostCardWidget extends StatefulWidget { final String title; final String content; final String authorName; final String authorImageUrl; final DateTime publishDate; final List? imageUrls; final int likesCount; final int commentsCount; final int sharesCount; final VoidCallback? onLike; final VoidCallback? onComment; final VoidCallback? onShare; final double? aspectRatio; // Nouveau paramètre pour le ratio (largeur/hauteur) const PostCardWidget({ Key? key, required this.title, required this.content, required this.authorName, required this.authorImageUrl, required this.publishDate, this.imageUrls, this.likesCount = 0, this.commentsCount = 0, this.sharesCount = 0, this.onLike, this.onComment, this.onShare, this.aspectRatio, // null = ratio naturel de l'image, ex: 16/9, 4/3, 1/1 }) : super(key: key); @override State createState() => _PostCardWidgetState(); } class _PostCardWidgetState extends State { @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.only(bottom: 16.0), //elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête avec auteur et date Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ // Photo de profil de l'auteur CircleAvatar( radius: 20, backgroundImage: NetworkImage(widget.authorImageUrl), onBackgroundImageError: (_, __) {}, child: widget.authorImageUrl.isEmpty ? const Icon(Icons.person, size: 20) : null, ), const SizedBox(width: 12), // Nom et date Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.authorName, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, color: Colors.white, ), ), const SizedBox(height: 2), Text( _formatDate(widget.publishDate), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey, ), ), ], ), ), // Menu options IconButton( icon: const Icon(Icons.more_vert, color: Colors.grey), onPressed: () { // Action pour le menu }, ), ], ), ), // Contenu du post Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Contenu ReadMoreText( widget.content, trimMode: TrimMode.Line, trimLines: 2, colorClickableText: Theme.of(context).colorScheme.primary, trimCollapsedText: 'Voir plus', trimExpandedText: 'Voir moins', moreStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white, height: 1.4, ), ), ], ), ), // Images avec boutons d'action si présentes if (widget.imageUrls != null && widget.imageUrls!.isNotEmpty) FutureBuilder( future: getImageHeightWithRatio( widget.imageUrls![0], // Utilise la première image pour calculer la hauteur MediaQuery.of(context).size.width - 32, // Largeur du conteneur (avec padding) widget.aspectRatio, // Ratio forcé ou null pour le ratio naturel ), builder: (context, snapshot) { final imageHeight = snapshot.data ?? 200.0; // Hauteur par défaut si pas encore calculée return Container( height: imageHeight, child: Stack( children: [ // Images PageView.builder( itemCount: widget.imageUrls!.length, itemBuilder: (context, index) { return Container( //margin: const EdgeInsets.symmetric(horizontal: 16.0), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(0), topRight: Radius.circular(0), bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8), ), ), child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(0), topRight: Radius.circular(0), bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8), ), child: _buildImage(widget.imageUrls![index]), ), ); }, ), // Boutons d'action en overlay à droite Positioned( right: 24, top: 0, bottom: 0, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _VerticalActionButton( icon: Icons.favorite_outline, count: widget.likesCount, onPressed: widget.onLike, ), const SizedBox(height: 16), _VerticalActionButton( icon: Icons.comment_outlined, count: widget.commentsCount, onPressed: widget.onComment, ), const SizedBox(height: 16), _VerticalActionButton( icon: Icons.share_outlined, count: widget.sharesCount, onPressed: widget.onShare, ), ], ), ), ], ), ); }, ), // Si pas d'image, boutons d'action en bas à droite if (widget.imageUrls == null || widget.imageUrls!.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ _HorizontalActionButton( icon: Icons.favorite_outline, count: widget.likesCount, onPressed: widget.onLike, ), const SizedBox(width: 16), _HorizontalActionButton( icon: Icons.comment_outlined, count: widget.commentsCount, onPressed: widget.onComment, ), const SizedBox(width: 16), _HorizontalActionButton( icon: Icons.share_outlined, count: widget.sharesCount, onPressed: widget.onShare, ), ], ), ), ], ), ); } String _formatDate(DateTime date) { final now = DateTime.now(); final difference = now.difference(date); if (difference.inDays > 7) { return '${date.day}/${date.month}/${date.year}'; } else if (difference.inDays > 0) { return '${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}'; } else if (difference.inHours > 0) { return '${difference.inHours}h'; } else if (difference.inMinutes > 0) { return '${difference.inMinutes}min'; } else { return 'À l\'instant'; } } /// Récupère les dimensions de l'image (largeur et hauteur) Future getImageDimensions(String imageUrl) async { try { ImageProvider imageProvider; if (imageUrl.startsWith('assets/')) { imageProvider = AssetImage(imageUrl); } else { imageProvider = NetworkImage(imageUrl); } final ImageStream stream = imageProvider.resolve(const ImageConfiguration()); final Completer completer = Completer(); late ImageStreamListener listener; listener = ImageStreamListener((ImageInfo info, bool synchronousCall) { final double width = info.image.width.toDouble(); final double height = info.image.height.toDouble(); stream.removeListener(listener); completer.complete(Size(width, height)); }, onError: (dynamic exception, StackTrace? stackTrace) { stream.removeListener(listener); completer.complete(null); }); stream.addListener(listener); return completer.future; } catch (e) { print('Erreur lors de la récupération des dimensions de l\'image: $e'); return null; } } /// Récupère uniquement la hauteur de l'image Future getImageHeight(String imageUrl) async { final dimensions = await getImageDimensions(imageUrl); return dimensions?.height; } /// Calcule la hauteur de l'image en fonction de la largeur du conteneur Future getImageHeightForWidth(String imageUrl, double containerWidth) async { final dimensions = await getImageDimensions(imageUrl); if (dimensions == null) return null; final aspectRatio = dimensions.width / dimensions.height; return containerWidth / aspectRatio; } /// Calcule la hauteur en respectant un ratio spécifique ou le ratio naturel Future getImageHeightWithRatio(String imageUrl, double containerWidth, double? forcedRatio) async { if (forcedRatio != null) { // Utilise le ratio forcé (largeur/hauteur) return containerWidth / forcedRatio; } else { // Utilise le ratio naturel de l'image final naturalHeight = await getImageHeightForWidth(imageUrl, containerWidth); return naturalHeight ?? 200.0; // Fallback si l'image ne peut pas être chargée } } Widget _buildImage(String imageUrl) { // Vérifie si c'est une image d'assets ou une URL réseau if (imageUrl.startsWith('assets/')) { return Image.asset( imageUrl, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { print('Erreur de chargement d\'asset: $error'); return _buildErrorWidget(); }, ); } else { return Image.network( imageUrl, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( color: Colors.grey[300], child: Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ), ); }, errorBuilder: (context, error, stackTrace) { print('Erreur de chargement d\'image réseau: $error'); return _buildErrorWidget(); }, ); } } Widget _buildErrorWidget() { return Container( color: Colors.grey[300], child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.broken_image, size: 50, color: Colors.grey[600], ), const SizedBox(height: 8), Text( 'Image non disponible', style: TextStyle( color: Colors.grey[600], fontSize: 12, ), ), ], ), ); } } class _VerticalActionButton extends StatefulWidget { final IconData icon; final int? count; final VoidCallback? onPressed; const _VerticalActionButton({ required this.icon, this.count, this.onPressed, }); @override State<_VerticalActionButton> createState() => _VerticalActionButtonState(); } class _VerticalActionButtonState extends State<_VerticalActionButton> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 150), vsync: this, ); _scaleAnimation = Tween( begin: 1.0, end: 0.85, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, )); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _handleTap() async { await _animationController.forward(); await _animationController.reverse(); widget.onPressed?.call(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Container( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), borderRadius: BorderRadius.circular(20), ), child: Icon( widget.icon, size: 30, color: Colors.white, ), ), if (widget.count != null && widget.count! > 0) ...[ const SizedBox(height: 4), Text( widget.count! > 999 ? '999+' : '${widget.count}', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ], ), ), ); }, ), ); } } class _HorizontalActionButton extends StatefulWidget { final IconData icon; final int? count; final VoidCallback? onPressed; const _HorizontalActionButton({ required this.icon, this.count, this.onPressed, }); @override State<_HorizontalActionButton> createState() => _HorizontalActionButtonState(); } class _HorizontalActionButtonState extends State<_HorizontalActionButton> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 150), vsync: this, ); _scaleAnimation = Tween( begin: 1.0, end: 0.85, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, )); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _handleTap() async { await _animationController.forward(); await _animationController.reverse(); widget.onPressed?.call(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(20), ), child: Icon( widget.icon, size: 20, color: Colors.grey[900], ), ), if (widget.count != null && widget.count! > 0) ...[ const SizedBox(height: 4), Text( widget.count! > 999 ? '999+' : '${widget.count}', style: TextStyle( color: Colors.grey[100], fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ], ), ), ); }, ), ); } }