fix(chat): Correction race condition + Implémentation TODOs
## Corrections Critiques ### Race Condition - Statuts de Messages - Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas - Cause : WebSocket delivery confirmations arrivaient avant messages locaux - Solution : Pattern Optimistic UI dans chat_bloc.dart - Création message temporaire immédiate - Ajout à la liste AVANT requête HTTP - Remplacement par message serveur à la réponse - Fichier : lib/presentation/state_management/chat_bloc.dart ## Implémentation TODOs (13/21) ### Social (social_header_widget.dart) - ✅ Copier lien du post dans presse-papiers - ✅ Partage natif via Share.share() - ✅ Dialogue de signalement avec 5 raisons ### Partage (share_post_dialog.dart) - ✅ Interface sélection d'amis avec checkboxes - ✅ Partage externe via Share API ### Média (media_upload_service.dart) - ✅ Parsing JSON réponse backend - ✅ Méthode deleteMedia() pour suppression - ✅ Génération miniature vidéo ### Posts (create_post_dialog.dart, edit_post_dialog.dart) - ✅ Extraction URL depuis uploads - ✅ Documentation chargement médias ### Chat (conversations_screen.dart) - ✅ Navigation vers notifications - ✅ ConversationSearchDelegate pour recherche ## Nouveaux Fichiers ### Configuration - build-prod.ps1 : Script build production avec dart-define - lib/core/constants/env_config.dart : Gestion environnements ### Documentation - TODOS_IMPLEMENTED.md : Documentation complète TODOs ## Améliorations ### Architecture - Refactoring injection de dépendances - Amélioration routing et navigation - Optimisation providers (UserProvider, FriendsProvider) ### UI/UX - Amélioration thème et couleurs - Optimisation animations - Meilleure gestion erreurs ### Services - Configuration API avec env_config - Amélioration datasources (events, users) - Optimisation modèles de données
This commit is contained in:
321
lib/presentation/widgets/social/media_picker.dart
Normal file
321
lib/presentation/widgets/social/media_picker.dart
Normal file
@@ -0,0 +1,321 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../../core/constants/design_system.dart';
|
||||
|
||||
/// Widget de sélection de médias pour la création de posts.
|
||||
///
|
||||
/// Permet de sélectionner des images et vidéos depuis la galerie ou l'appareil photo.
|
||||
class MediaPicker extends StatefulWidget {
|
||||
const MediaPicker({
|
||||
required this.onMediasChanged,
|
||||
this.maxMedias = 10,
|
||||
this.initialMedias = const [],
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ValueChanged<List<XFile>> onMediasChanged;
|
||||
final int maxMedias;
|
||||
final List<XFile> initialMedias;
|
||||
|
||||
@override
|
||||
State<MediaPicker> createState() => _MediaPickerState();
|
||||
}
|
||||
|
||||
class _MediaPickerState extends State<MediaPicker> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
List<XFile> _selectedMedias = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedMedias = List.from(widget.initialMedias);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Titre avec compteur
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Médias',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'${_selectedMedias.length}/${widget.maxMedias}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacingMd),
|
||||
|
||||
// Boutons d'action
|
||||
Row(
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context,
|
||||
icon: Icons.photo_library_outlined,
|
||||
label: 'Galerie',
|
||||
onTap: _pickFromGallery,
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacingSm),
|
||||
_buildActionButton(
|
||||
context,
|
||||
icon: Icons.camera_alt_outlined,
|
||||
label: 'Caméra',
|
||||
onTap: _pickFromCamera,
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacingSm),
|
||||
_buildActionButton(
|
||||
context,
|
||||
icon: Icons.videocam_outlined,
|
||||
label: 'Vidéo',
|
||||
onTap: _pickVideo,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Grille de médias sélectionnés
|
||||
if (_selectedMedias.isNotEmpty) ...[
|
||||
const SizedBox(height: DesignSystem.spacingMd),
|
||||
_buildMediasGrid(theme),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final canAddMore = _selectedMedias.length < widget.maxMedias;
|
||||
|
||||
return Expanded(
|
||||
child: Material(
|
||||
color: canAddMore
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
|
||||
child: InkWell(
|
||||
onTap: canAddMore ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DesignSystem.spacingSm,
|
||||
vertical: DesignSystem.spacingMd,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 24,
|
||||
color: canAddMore
|
||||
? theme.colorScheme.onPrimaryContainer
|
||||
: theme.colorScheme.onSurfaceVariant.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: canAddMore
|
||||
? theme.colorScheme.onPrimaryContainer
|
||||
: theme.colorScheme.onSurfaceVariant.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediasGrid(ThemeData theme) {
|
||||
return SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _selectedMedias.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildMediaItem(theme, index);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaItem(ThemeData theme, int index) {
|
||||
final media = _selectedMedias[index];
|
||||
final isVideo = media.path.toLowerCase().endsWith('.mp4') ||
|
||||
media.path.toLowerCase().endsWith('.mov');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: DesignSystem.spacingSm),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Image/Vidéo
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusMd),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: theme.colorScheme.surfaceVariant,
|
||||
child: isVideo
|
||||
? Center(
|
||||
child: Icon(
|
||||
Icons.videocam_rounded,
|
||||
size: 32,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
: Image.file(
|
||||
File(media.path),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Icon(
|
||||
Icons.broken_image_rounded,
|
||||
size: 32,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton de suppression
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => _removeMedia(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close_rounded,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Indicateur vidéo
|
||||
if (isVideo)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
left: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radiusSm),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickFromGallery() async {
|
||||
try {
|
||||
final images = await _picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
_addMedias(images);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MediaPicker] Erreur lors de la sélection: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickFromCamera() async {
|
||||
try {
|
||||
final image = await _picker.pickImage(source: ImageSource.camera);
|
||||
if (image != null) {
|
||||
_addMedias([image]);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MediaPicker] Erreur lors de la capture: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickVideo() async {
|
||||
try {
|
||||
final video = await _picker.pickVideo(source: ImageSource.gallery);
|
||||
if (video != null) {
|
||||
_addMedias([video]);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MediaPicker] Erreur lors de la sélection vidéo: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _addMedias(List<XFile> medias) {
|
||||
final availableSlots = widget.maxMedias - _selectedMedias.length;
|
||||
final mediasToAdd = medias.take(availableSlots).toList();
|
||||
|
||||
setState(() {
|
||||
_selectedMedias.addAll(mediasToAdd);
|
||||
});
|
||||
|
||||
widget.onMediasChanged(_selectedMedias);
|
||||
}
|
||||
|
||||
void _removeMedia(int index) {
|
||||
setState(() {
|
||||
_selectedMedias.removeAt(index);
|
||||
});
|
||||
|
||||
widget.onMediasChanged(_selectedMedias);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user