Files
afterwork/lib/presentation/widgets/social/edit_post_dialog.dart
dahoud 92612abbd7 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
2026-01-10 10:43:17 +00:00

332 lines
9.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../../../core/constants/design_system.dart';
import '../../../domain/entities/social_post.dart';
import 'media_picker.dart';
/// Dialogue d'édition de post existant avec support d'images et vidéos.
class EditPostDialog extends StatefulWidget {
const EditPostDialog({
required this.post,
required this.onPostUpdated,
super.key,
});
final SocialPost post;
final Future<void> Function(String content, List<XFile> medias) onPostUpdated;
@override
State<EditPostDialog> createState() => _EditPostDialogState();
/// Affiche le dialogue d'édition de post
static Future<void> show({
required BuildContext context,
required SocialPost post,
required Future<void> Function(String content, List<XFile> medias)
onPostUpdated,
}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => EditPostDialog(
post: post,
onPostUpdated: onPostUpdated,
),
);
}
}
class _EditPostDialogState extends State<EditPostDialog> {
late final TextEditingController _contentController;
final FocusNode _contentFocusNode = FocusNode();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
List<XFile> _selectedMedias = [];
bool _isUpdating = false;
@override
void initState() {
super.initState();
_contentController = TextEditingController(text: widget.post.content);
// Note: Les médias existants du post sont déjà stockés sous forme d'URLs
// dans widget.post.mediaUrls. Ils seront affichés via ces URLs.
// _selectedMedias permet d'ajouter de NOUVEAUX médias locaux.
// Lors de la sauvegarde, il faut combiner:
// - Les URLs existantes (widget.post.mediaUrls)
// - Les nouveaux médias uploadés (URLs générées depuis _selectedMedias)
_selectedMedias = [];
// Auto-focus sur le champ de texte
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
_contentFocusNode.requestFocus();
}
});
}
@override
void dispose() {
_contentController.dispose();
_contentFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
return Container(
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(DesignSystem.radiusLg),
),
),
padding: EdgeInsets.only(
bottom: mediaQuery.viewInsets.bottom,
),
child: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(DesignSystem.spacingLg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeader(theme),
const SizedBox(height: DesignSystem.spacingLg),
_buildUserInfo(theme),
const SizedBox(height: DesignSystem.spacingMd),
_buildContentField(theme),
const SizedBox(height: DesignSystem.spacingLg),
MediaPicker(
onMediasChanged: (medias) {
setState(() {
_selectedMedias = medias;
});
},
initialMedias: _selectedMedias,
),
const SizedBox(height: DesignSystem.spacingLg),
_buildActions(theme),
],
),
),
),
),
);
}
Widget _buildHeader(ThemeData theme) {
return Row(
children: [
// Handle
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: theme.colorScheme.onSurface.withOpacity(0.2),
borderRadius: BorderRadius.circular(2),
),
),
const Spacer(),
// Titre
Text(
'Modifier le post',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
const Spacer(),
// Bouton fermer
IconButton(
icon: const Icon(Icons.close_rounded, size: 22),
onPressed: () => Navigator.of(context).pop(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
),
],
);
}
Widget _buildUserInfo(ThemeData theme) {
return Row(
children: [
// Avatar
CircleAvatar(
radius: 20,
backgroundColor: theme.colorScheme.primaryContainer,
backgroundImage: widget.post.userProfileImageUrl.isNotEmpty
? NetworkImage(widget.post.userProfileImageUrl)
: null,
child: widget.post.userProfileImageUrl.isEmpty
? Icon(
Icons.person_rounded,
size: 24,
color: theme.colorScheme.onPrimaryContainer,
)
: null,
),
const SizedBox(width: DesignSystem.spacingMd),
// Nom
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.post.authorFullName,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 2),
Row(
children: [
Icon(
Icons.edit_rounded,
size: 14,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
const SizedBox(width: 4),
Text(
'Édition',
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
],
),
),
],
);
}
Widget _buildContentField(ThemeData theme) {
return Form(
key: _formKey,
child: TextFormField(
controller: _contentController,
focusNode: _contentFocusNode,
decoration: InputDecoration(
hintText: 'Modifiez votre post...',
hintStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
fontSize: 16,
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 16,
height: 1.5,
),
maxLines: 8,
minLines: 3,
maxLength: 500,
validator: (value) {
if ((value == null || value.trim().isEmpty) &&
_selectedMedias.isEmpty) {
return 'Ajoutez du texte ou des médias';
}
return null;
},
),
);
}
Widget _buildActions(ThemeData theme) {
final hasChanges = _contentController.text != widget.post.content ||
_selectedMedias.isNotEmpty;
return Row(
children: [
// Indicateur de caractères
Expanded(
child: Text(
'${_contentController.text.length}/500',
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
),
const SizedBox(width: DesignSystem.spacingMd),
// Bouton Annuler
TextButton(
onPressed: _isUpdating ? null : () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
const SizedBox(width: DesignSystem.spacingSm),
// Bouton Enregistrer
FilledButton(
onPressed: (_isUpdating || !hasChanges) ? null : _handleUpdate,
child: _isUpdating
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('Enregistrer'),
),
],
);
}
Future<void> _handleUpdate() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isUpdating = true;
});
try {
await widget.onPostUpdated(
_contentController.text.trim(),
_selectedMedias,
);
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
if (mounted) {
final theme = Theme.of(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la mise à jour: ${e.toString()}'),
backgroundColor: theme.colorScheme.error,
),
);
}
} finally {
if (mounted) {
setState(() {
_isUpdating = false;
});
}
}
}
}