import 'package:flutter/material.dart'; import '../../../core/constants/design_system.dart'; import '../fullscreen_image_viewer.dart'; import 'fullscreen_video_player.dart'; /// Type de média dans un post. enum MediaType { image, video } /// Modèle de média pour un post. class PostMedia { const PostMedia({ required this.url, required this.type, this.thumbnailUrl, this.duration, }); final String url; final MediaType type; final String? thumbnailUrl; final Duration? duration; } /// Widget d'affichage de médias dans un post social. /// /// Supporte les images et vidéos avec différentes dispositions. class PostMediaViewer extends StatelessWidget { const PostMediaViewer({ required this.medias, required this.postId, this.onTap, super.key, }); final List medias; final String postId; final VoidCallback? onTap; @override Widget build(BuildContext context) { if (medias.isEmpty) return const SizedBox.shrink(); if (medias.length == 1) { return _buildSingleMedia(context, medias[0], 0); } else if (medias.length == 2) { return _buildDoubleMedia(context); } else if (medias.length == 3) { return _buildTripleMedia(context); } else { return _buildMultipleMedia(context); } } /// Affiche un seul média Widget _buildSingleMedia(BuildContext context, PostMedia media, int index) { return GestureDetector( onTap: onTap, child: AspectRatio( aspectRatio: 1.0, child: _MediaItem( media: media, postId: postId, index: index, ), ), ); } /// Affiche deux médias côte à côte Widget _buildDoubleMedia(BuildContext context) { return SizedBox( height: 300, child: Row( children: [ Expanded( child: _MediaItem( media: medias[0], postId: postId, index: 0, ), ), const SizedBox(width: 2), Expanded( child: _MediaItem( media: medias[1], postId: postId, index: 1, ), ), ], ), ); } /// Affiche trois médias (1 grand + 2 petits) Widget _buildTripleMedia(BuildContext context) { return SizedBox( height: 300, child: Row( children: [ Expanded( flex: 2, child: _MediaItem( media: medias[0], postId: postId, index: 0, ), ), const SizedBox(width: 2), Expanded( child: Column( children: [ Expanded( child: _MediaItem( media: medias[1], postId: postId, index: 1, ), ), const SizedBox(height: 2), Expanded( child: _MediaItem( media: medias[2], postId: postId, index: 2, ), ), ], ), ), ], ), ); } /// Affiche 4+ médias (grille 2x2 avec compteur) Widget _buildMultipleMedia(BuildContext context) { return SizedBox( height: 300, child: Row( children: [ Expanded( child: Column( children: [ Expanded( child: _MediaItem( media: medias[0], postId: postId, index: 0, ), ), const SizedBox(height: 2), Expanded( child: _MediaItem( media: medias[2], postId: postId, index: 2, ), ), ], ), ), const SizedBox(width: 2), Expanded( child: Column( children: [ Expanded( child: _MediaItem( media: medias[1], postId: postId, index: 1, ), ), const SizedBox(height: 2), Expanded( child: Stack( fit: StackFit.expand, children: [ _MediaItem( media: medias[3], postId: postId, index: 3, ), if (medias.length > 4) Container( color: Colors.black.withOpacity(0.6), child: Center( child: Text( '+${medias.length - 4}', style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), ], ), ), ], ), ); } } /// Widget d'affichage d'un média individuel. class _MediaItem extends StatelessWidget { const _MediaItem({ required this.media, required this.postId, required this.index, }); final PostMedia media; final String postId; final int index; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Hero( tag: 'post_media_${postId}_$index', child: Container( decoration: BoxDecoration( color: theme.colorScheme.surfaceVariant.withOpacity(0.3), ), child: media.type == MediaType.image ? _buildImage(context, theme) : _buildVideo(context, theme), ), ); } Widget _buildImage(BuildContext context, ThemeData theme) { return GestureDetector( onTap: () => _openFullscreen(context), child: Image.network( media.url, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, strokeWidth: 2, ), ); }, errorBuilder: (context, error, stackTrace) { return Center( child: Icon( Icons.broken_image_rounded, size: 32, color: theme.colorScheme.onSurfaceVariant.withOpacity(0.5), ), ); }, ), ); } Widget _buildVideo(BuildContext context, ThemeData theme) { return Stack( fit: StackFit.expand, children: [ // Thumbnail ou image de preview if (media.thumbnailUrl != null) Image.network( media.thumbnailUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( color: theme.colorScheme.surfaceVariant, ); }, ) else Container( color: theme.colorScheme.surfaceVariant, ), // Overlay de play Center( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), child: const Icon( Icons.play_arrow_rounded, color: Colors.white, size: 32, ), ), ), // Durée de la vidéo if (media.duration != null) Positioned( bottom: 8, right: 8, child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 3, ), decoration: BoxDecoration( color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(DesignSystem.radiusSm), ), child: Text( _formatDuration(media.duration!), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white, height: 1.2, ), ), ), ), ], ); } void _openFullscreen(BuildContext context) { if (media.type == MediaType.image) { Navigator.push( context, PageRouteBuilder( opaque: false, barrierColor: Colors.black, pageBuilder: (context, animation, secondaryAnimation) { return FadeTransition( opacity: animation, child: FullscreenImageViewer( imageUrl: media.url, heroTag: 'post_media_${postId}_$index', title: '', ), ); }, ), ); } else { // Ouvrir le lecteur vidéo en plein écran FullscreenVideoPlayer.show( context: context, videoUrl: media.url, heroTag: 'post_media_${postId}_$index', title: '', ); } } String _formatDuration(Duration duration) { final minutes = duration.inMinutes; final seconds = duration.inSeconds % 60; return '$minutes:${seconds.toString().padLeft(2, '0')}'; } }