Files
Bahla-Front/lib/ui/common/post_card_widget.dart
2025-09-02 19:16:40 +02:00

560 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'ReadMoreText.dart';
class PostCardWidget extends StatefulWidget {
final String title;
final String content;
final String authorName;
final String authorImageUrl;
final DateTime publishDate;
final List<String>? 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<PostCardWidget> createState() => _PostCardWidgetState();
}
class _PostCardWidgetState extends State<PostCardWidget> {
@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: [
ReadMoreText(widget.content,
trimLines: 2,
collapsedText: '... Voir plus',
expandedText: ' Voir moins',
textStyle: 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<double>(
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<Size?> 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<Size?> 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<double?> 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<double?> 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<double> 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<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
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<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
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,
),
),
],
],
),
),
);
},
),
);
}
}