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:
dahoud
2026-01-10 10:43:17 +00:00
parent 06031b01f2
commit 92612abbd7
321 changed files with 43137 additions and 4285 deletions

View 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);
}
}