From 1332fddeeb455cdf1597bebcf93d833c7d8e241c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C3=ABl=20Perret?= Date: Tue, 26 Aug 2025 22:32:11 +0200 Subject: [PATCH] feat : post respect ratio and height --- lib/ui/common/post_card_widget copy.dart | 478 ++++++++++++++++++ lib/ui/common/post_card_widget.dart | 231 ++++++--- .../event_details/event_details_view.dart | 1 + 3 files changed, 634 insertions(+), 76 deletions(-) create mode 100644 lib/ui/common/post_card_widget copy.dart diff --git a/lib/ui/common/post_card_widget copy.dart b/lib/ui/common/post_card_widget copy.dart new file mode 100644 index 0000000..554104b --- /dev/null +++ b/lib/ui/common/post_card_widget copy.dart @@ -0,0 +1,478 @@ +import 'package:flutter/material.dart'; + +class PostCardWidget extends StatelessWidget { + 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; + + 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, + }) : super(key: key); + + @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(authorImageUrl), + onBackgroundImageError: (_, __) {}, + child: authorImageUrl.isEmpty + ? const Icon(Icons.person, size: 20) + : null, + ), + const SizedBox(width: 12), + // Nom et date + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + authorName, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + const SizedBox(height: 2), + Text( + _formatDate(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 + Text( + content, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + height: 1.4, + ), + ), + ], + ), + ), + + // Images avec boutons d'action si présentes + if (imageUrls != null && imageUrls!.isNotEmpty) + Container( + //margin: const EdgeInsets.symmetric(vertical: 12.0), + height: 200, + child: Stack( + children: [ + // Images + PageView.builder( + itemCount: 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(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: likesCount, + onPressed: onLike, + ), + const SizedBox(height: 16), + _VerticalActionButton( + icon: Icons.comment_outlined, + count: commentsCount, + onPressed: onComment, + ), + const SizedBox(height: 16), + _VerticalActionButton( + icon: Icons.share_outlined, + count: sharesCount, + onPressed: onShare, + ), + ], + ), + ), + ], + ), + ), + + // Si pas d'image, boutons d'action en bas à droite + if (imageUrls == null || 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: likesCount, + onPressed: onLike, + ), + const SizedBox(width: 16), + _HorizontalActionButton( + icon: Icons.comment_outlined, + count: commentsCount, + onPressed: onComment, + ), + const SizedBox(width: 16), + _HorizontalActionButton( + icon: Icons.share_outlined, + count: sharesCount, + onPressed: 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'; + } + } + + 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: 24, + 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: 12, + 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, + ), + ), + ], + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/common/post_card_widget.dart b/lib/ui/common/post_card_widget.dart index 554104b..1f2942c 100644 --- a/lib/ui/common/post_card_widget.dart +++ b/lib/ui/common/post_card_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'dart:async'; -class PostCardWidget extends StatelessWidget { +class PostCardWidget extends StatefulWidget { final String title; final String content; final String authorName; @@ -13,6 +14,7 @@ class PostCardWidget extends StatelessWidget { final VoidCallback? onLike; final VoidCallback? onComment; final VoidCallback? onShare; + final double? aspectRatio; // Nouveau paramètre pour le ratio (largeur/hauteur) const PostCardWidget({ Key? key, @@ -28,8 +30,15 @@ class PostCardWidget extends StatelessWidget { 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( @@ -49,9 +58,9 @@ class PostCardWidget extends StatelessWidget { // Photo de profil de l'auteur CircleAvatar( radius: 20, - backgroundImage: NetworkImage(authorImageUrl), + backgroundImage: NetworkImage(widget.authorImageUrl), onBackgroundImageError: (_, __) {}, - child: authorImageUrl.isEmpty + child: widget.authorImageUrl.isEmpty ? const Icon(Icons.person, size: 20) : null, ), @@ -62,7 +71,7 @@ class PostCardWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - authorName, + widget.authorName, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, color: Colors.white, @@ -70,7 +79,7 @@ class PostCardWidget extends StatelessWidget { ), const SizedBox(height: 2), Text( - _formatDate(publishDate), + _formatDate(widget.publishDate), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey, ), @@ -97,7 +106,7 @@ class PostCardWidget extends StatelessWidget { children: [ // Contenu Text( - content, + widget.content, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white, height: 1.4, @@ -108,72 +117,82 @@ class PostCardWidget extends StatelessWidget { ), // Images avec boutons d'action si présentes - if (imageUrls != null && imageUrls!.isNotEmpty) - Container( - //margin: const EdgeInsets.symmetric(vertical: 12.0), - height: 200, - child: Stack( - children: [ - // Images - PageView.builder( - itemCount: 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(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: likesCount, - onPressed: onLike, - ), - const SizedBox(height: 16), - _VerticalActionButton( - icon: Icons.comment_outlined, - count: commentsCount, - onPressed: onComment, - ), - const SizedBox(height: 16), - _VerticalActionButton( - icon: Icons.share_outlined, - count: sharesCount, - onPressed: onShare, - ), - ], - ), - ), - ], + 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 (imageUrls == null || imageUrls!.isEmpty) + if (widget.imageUrls == null || widget.imageUrls!.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0), child: Row( @@ -182,20 +201,20 @@ class PostCardWidget extends StatelessWidget { children: [ _HorizontalActionButton( icon: Icons.favorite_outline, - count: likesCount, - onPressed: onLike, + count: widget.likesCount, + onPressed: widget.onLike, ), const SizedBox(width: 16), _HorizontalActionButton( icon: Icons.comment_outlined, - count: commentsCount, - onPressed: onComment, + count: widget.commentsCount, + onPressed: widget.onComment, ), const SizedBox(width: 16), _HorizontalActionButton( icon: Icons.share_outlined, - count: sharesCount, - onPressed: onShare, + count: widget.sharesCount, + onPressed: widget.onShare, ), ], ), @@ -223,6 +242,66 @@ class PostCardWidget extends StatelessWidget { } } + /// 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/')) { @@ -354,7 +433,7 @@ class _VerticalActionButtonState extends State<_VerticalActionButton> ), child: Icon( widget.icon, - size: 24, + size: 30, color: Colors.white, ), ), @@ -365,7 +444,7 @@ class _VerticalActionButtonState extends State<_VerticalActionButton> widget.count! > 999 ? '999+' : '${widget.count}', style: const TextStyle( color: Colors.white, - fontSize: 12, + fontSize: 18, fontWeight: FontWeight.bold, ), ), diff --git a/lib/ui/views/event_details/event_details_view.dart b/lib/ui/views/event_details/event_details_view.dart index 5ebff29..d790a82 100644 --- a/lib/ui/views/event_details/event_details_view.dart +++ b/lib/ui/views/event_details/event_details_view.dart @@ -214,6 +214,7 @@ class EventDetailsView extends StackedView { imageUrls: index % 3 == 0 ? ['assets/images/Affiche.jpg'] : null, likesCount: (index + 1) * 5, commentsCount: (index + 1) * 2, + aspectRatio: 4/5, onLike: () { // Action lors du clic sur "J'aime" print('Like publication ${index + 1}');