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,501 @@
import 'package:flutter/material.dart';
import '../../core/constants/design_system.dart';
/// Types de SnackBar selon le contexte
enum SnackBarType {
success,
error,
warning,
info,
}
/// Affiche un SnackBar personnalisé avec style moderne
///
/// **Usage:**
/// ```dart
/// showCustomSnackBar(
/// context,
/// message: 'Événement créé avec succès',
/// type: SnackBarType.success,
/// );
/// ```
void showCustomSnackBar(
BuildContext context, {
required String message,
SnackBarType type = SnackBarType.info,
Duration duration = const Duration(seconds: 3),
String? actionLabel,
VoidCallback? onActionPressed,
}) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Couleurs selon le type
Color backgroundColor;
Color textColor;
IconData icon;
switch (type) {
case SnackBarType.success:
backgroundColor = const Color(0xFF4CAF50);
textColor = Colors.white;
icon = Icons.check_circle;
break;
case SnackBarType.error:
backgroundColor = const Color(0xFFF44336);
textColor = Colors.white;
icon = Icons.error;
break;
case SnackBarType.warning:
backgroundColor = const Color(0xFFFF9800);
textColor = Colors.white;
icon = Icons.warning;
break;
case SnackBarType.info:
backgroundColor = isDark ? theme.colorScheme.surface : Colors.grey[800]!;
textColor = Colors.white;
icon = Icons.info;
break;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(icon, color: textColor, size: DesignSystem.iconSizeMd),
DesignSystem.horizontalSpace(DesignSystem.spacingMd),
Expanded(
child: Text(
message,
style: TextStyle(
color: textColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
backgroundColor: backgroundColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: DesignSystem.borderRadiusMd,
),
margin: DesignSystem.paddingAll(DesignSystem.spacingLg),
duration: duration,
action: actionLabel != null
? SnackBarAction(
label: actionLabel,
textColor: textColor,
onPressed: onActionPressed ?? () {},
)
: null,
),
);
}
/// Toast flottant avec animation
///
/// Alternative élégante au SnackBar, s'affiche en haut de l'écran.
class CustomToast {
static OverlayEntry? _overlayEntry;
static bool _isVisible = false;
/// Affiche un toast personnalisé
static void show(
BuildContext context, {
required String message,
SnackBarType type = SnackBarType.info,
Duration duration = const Duration(seconds: 2),
ToastPosition position = ToastPosition.top,
}) {
if (_isVisible) {
hide();
}
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Couleurs selon le type
Color backgroundColor;
Color textColor;
IconData icon;
switch (type) {
case SnackBarType.success:
backgroundColor = const Color(0xFF4CAF50);
textColor = Colors.white;
icon = Icons.check_circle;
break;
case SnackBarType.error:
backgroundColor = const Color(0xFFF44336);
textColor = Colors.white;
icon = Icons.error;
break;
case SnackBarType.warning:
backgroundColor = const Color(0xFFFF9800);
textColor = Colors.white;
icon = Icons.warning;
break;
case SnackBarType.info:
backgroundColor =
isDark ? theme.colorScheme.surface : Colors.grey[800]!;
textColor = Colors.white;
icon = Icons.info;
break;
}
_overlayEntry = OverlayEntry(
builder: (context) => _ToastWidget(
message: message,
backgroundColor: backgroundColor,
textColor: textColor,
icon: icon,
position: position,
onDismiss: hide,
),
);
final overlay = Overlay.of(context);
overlay.insert(_overlayEntry!);
_isVisible = true;
// Auto-dismiss après la durée spécifiée
Future.delayed(duration, () {
hide();
});
}
/// Cache le toast
static void hide() {
if (_isVisible && _overlayEntry != null) {
_overlayEntry!.remove();
_overlayEntry = null;
_isVisible = false;
}
}
}
/// Position du toast
enum ToastPosition {
top,
center,
bottom,
}
/// Widget interne pour afficher le toast avec animation
class _ToastWidget extends StatefulWidget {
const _ToastWidget({
required this.message,
required this.backgroundColor,
required this.textColor,
required this.icon,
required this.position,
required this.onDismiss,
});
final String message;
final Color backgroundColor;
final Color textColor;
final IconData icon;
final ToastPosition position;
final VoidCallback onDismiss;
@override
State<_ToastWidget> createState() => _ToastWidgetState();
}
class _ToastWidgetState extends State<_ToastWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: DesignSystem.durationMedium,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: DesignSystem.curveDecelerate,
),
);
// Animation de slide selon la position
Offset beginOffset;
switch (widget.position) {
case ToastPosition.top:
beginOffset = const Offset(0, -1);
break;
case ToastPosition.center:
beginOffset = const Offset(0, 0);
break;
case ToastPosition.bottom:
beginOffset = const Offset(0, 1);
break;
}
_slideAnimation = Tween<Offset>(
begin: beginOffset,
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: DesignSystem.curveDecelerate,
),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Position selon l'énumération
Alignment alignment;
EdgeInsets margin;
switch (widget.position) {
case ToastPosition.top:
alignment = Alignment.topCenter;
margin = EdgeInsets.only(
top: MediaQuery.of(context).padding.top + DesignSystem.spacingLg,
);
break;
case ToastPosition.center:
alignment = Alignment.center;
margin = EdgeInsets.zero;
break;
case ToastPosition.bottom:
alignment = Alignment.bottomCenter;
margin = EdgeInsets.only(
bottom:
MediaQuery.of(context).padding.bottom + DesignSystem.spacingLg,
);
break;
}
return Positioned.fill(
child: Align(
alignment: alignment,
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: GestureDetector(
onTap: () {
_controller.reverse().then((_) => widget.onDismiss());
},
child: Container(
margin: margin +
DesignSystem.paddingHorizontal(DesignSystem.spacingLg),
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: DesignSystem.borderRadiusMd,
boxShadow: DesignSystem.shadowLg,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
widget.icon,
color: widget.textColor,
size: DesignSystem.iconSizeMd,
),
DesignSystem.horizontalSpace(DesignSystem.spacingMd),
Flexible(
child: Text(
widget.message,
style: TextStyle(
color: widget.textColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
),
),
),
);
}
}
/// Bottom sheet personnalisé avec design moderne
Future<T?> showCustomBottomSheet<T>({
required BuildContext context,
required Widget child,
bool isDismissible = true,
bool enableDrag = true,
Color? backgroundColor,
double? initialChildSize,
double? minChildSize,
double? maxChildSize,
}) {
final theme = Theme.of(context);
return showModalBottomSheet<T>(
context: context,
isScrollControlled: true,
isDismissible: isDismissible,
enableDrag: enableDrag,
backgroundColor: Colors.transparent,
builder: (context) => DraggableScrollableSheet(
initialChildSize: initialChildSize ?? 0.6,
minChildSize: minChildSize ?? 0.3,
maxChildSize: maxChildSize ?? 0.9,
builder: (context, scrollController) => Container(
decoration: BoxDecoration(
color: backgroundColor ?? theme.scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(DesignSystem.radiusXl),
),
boxShadow: DesignSystem.shadowXl,
),
child: Column(
children: [
// Handle pour indiquer qu'on peut glisser
Container(
margin: DesignSystem.paddingVertical(DesignSystem.spacingMd),
width: 40,
height: 4,
decoration: BoxDecoration(
color: theme.dividerColor,
borderRadius: DesignSystem.borderRadiusRound,
),
),
Expanded(
child: SingleChildScrollView(
controller: scrollController,
child: child,
),
),
],
),
),
),
);
}
/// Dialog personnalisé avec animation élégante
Future<T?> showCustomDialog<T>({
required BuildContext context,
required Widget child,
bool barrierDismissible = true,
}) {
return showGeneralDialog<T>(
context: context,
barrierDismissible: barrierDismissible,
barrierColor: Colors.black54,
transitionDuration: DesignSystem.durationMedium,
transitionBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: Tween<double>(
begin: 0.8,
end: 1.0,
).animate(
CurvedAnimation(
parent: animation,
curve: DesignSystem.curveDecelerate,
),
),
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return Center(
child: Container(
margin: DesignSystem.paddingAll(DesignSystem.spacing2xl),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: DesignSystem.borderRadiusXl,
boxShadow: DesignSystem.shadowXl,
),
child: Material(
color: Colors.transparent,
child: child,
),
),
);
},
);
}
/// Extensions pour faciliter l'utilisation
extension SnackBarExtensions on BuildContext {
/// Affiche un snackbar de succès
void showSuccess(String message) {
showCustomSnackBar(
this,
message: message,
type: SnackBarType.success,
);
}
/// Affiche un snackbar d'erreur
void showError(String message) {
showCustomSnackBar(
this,
message: message,
type: SnackBarType.error,
);
}
/// Affiche un snackbar d'avertissement
void showWarning(String message) {
showCustomSnackBar(
this,
message: message,
type: SnackBarType.warning,
);
}
/// Affiche un snackbar d'information
void showInfo(String message) {
showCustomSnackBar(
this,
message: message,
type: SnackBarType.info,
);
}
/// Affiche un toast de succès
void toastSuccess(String message) {
CustomToast.show(
this,
message: message,
type: SnackBarType.success,
);
}
/// Affiche un toast d'erreur
void toastError(String message) {
CustomToast.show(
this,
message: message,
type: SnackBarType.error,
);
}
}