import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../shared/theme/app_theme.dart'; import '../animations/loading_animations.dart'; /// Service de feedback utilisateur avec différents types de notifications class UserFeedback { /// Affiche un message de succès static void showSuccess( BuildContext context, String message, { Duration duration = const Duration(seconds: 3), VoidCallback? onAction, String? actionLabel, }) { HapticFeedback.lightImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon( Icons.check_circle, color: Colors.white, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( message, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), backgroundColor: AppTheme.successColor, duration: duration, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), action: onAction != null && actionLabel != null ? SnackBarAction( label: actionLabel, textColor: Colors.white, onPressed: onAction, ) : null, ), ); } /// Affiche un message d'information static void showInfo( BuildContext context, String message, { Duration duration = const Duration(seconds: 3), VoidCallback? onAction, String? actionLabel, }) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon( Icons.info, color: Colors.white, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( message, style: const TextStyle( color: Colors.white, fontSize: 14, ), ), ), ], ), backgroundColor: AppTheme.infoColor, duration: duration, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), action: onAction != null && actionLabel != null ? SnackBarAction( label: actionLabel, textColor: Colors.white, onPressed: onAction, ) : null, ), ); } /// Affiche un message d'avertissement static void showWarning( BuildContext context, String message, { Duration duration = const Duration(seconds: 4), VoidCallback? onAction, String? actionLabel, }) { HapticFeedback.mediumImpact(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon( Icons.warning, color: Colors.white, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( message, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), backgroundColor: AppTheme.warningColor, duration: duration, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), action: onAction != null && actionLabel != null ? SnackBarAction( label: actionLabel, textColor: Colors.white, onPressed: onAction, ) : null, ), ); } /// Affiche une boîte de dialogue de confirmation static Future showConfirmation( BuildContext context, { required String title, required String message, String confirmText = 'Confirmer', String cancelText = 'Annuler', Color? confirmColor, IconData? icon, bool isDangerous = false, }) async { HapticFeedback.mediumImpact(); final result = await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Row( children: [ if (icon != null) ...[ Icon( icon, color: isDangerous ? AppTheme.errorColor : AppTheme.primaryColor, size: 24, ), const SizedBox(width: 12), ], Expanded( child: Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), ), ], ), content: Text( message, style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text( cancelText, style: const TextStyle( color: AppTheme.textSecondary, ), ), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: confirmColor ?? (isDangerous ? AppTheme.errorColor : AppTheme.primaryColor), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text(confirmText), ), ], ); }, ); return result ?? false; } /// Affiche une boîte de dialogue de saisie static Future showInputDialog( BuildContext context, { required String title, required String label, String? initialValue, String? hintText, String confirmText = 'OK', String cancelText = 'Annuler', TextInputType? keyboardType, String? Function(String?)? validator, int maxLines = 1, }) async { final controller = TextEditingController(text: initialValue); final formKey = GlobalKey(); final result = await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), content: Form( key: formKey, child: TextFormField( controller: controller, decoration: InputDecoration( labelText: label, hintText: hintText, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), keyboardType: keyboardType, maxLines: maxLines, validator: validator, autofocus: true, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( cancelText, style: const TextStyle( color: AppTheme.textSecondary, ), ), ), ElevatedButton( onPressed: () { if (formKey.currentState?.validate() ?? false) { Navigator.of(context).pop(controller.text); } }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text(confirmText), ), ], ); }, ); controller.dispose(); return result; } /// Affiche un indicateur de chargement avec message et animation personnalisée static void showLoading( BuildContext context, { String message = 'Chargement...', bool barrierDismissible = false, Widget? customLoader, }) { showDialog( context: context, barrierDismissible: barrierDismissible, builder: (BuildContext context) { return PopScope( canPop: barrierDismissible, child: AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ customLoader ?? LoadingAnimations.waves( color: AppTheme.primaryColor, size: 50, ), const SizedBox(height: 16), Text( message, style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), textAlign: TextAlign.center, ), ], ), ), ); }, ); } /// Affiche un indicateur de chargement avec animation de points static void showLoadingDots( BuildContext context, { String message = 'Chargement...', bool barrierDismissible = false, }) { showLoading( context, message: message, barrierDismissible: barrierDismissible, customLoader: LoadingAnimations.dots( color: AppTheme.primaryColor, size: 12, ), ); } /// Affiche un indicateur de chargement avec animation de spinner static void showLoadingSpinner( BuildContext context, { String message = 'Chargement...', bool barrierDismissible = false, }) { showLoading( context, message: message, barrierDismissible: barrierDismissible, customLoader: LoadingAnimations.spinner( color: AppTheme.primaryColor, size: 50, ), ); } /// Ferme l'indicateur de chargement static void hideLoading(BuildContext context) { Navigator.of(context).pop(); } /// Affiche un toast personnalisé static void showToast( BuildContext context, String message, { Duration duration = const Duration(seconds: 2), Color? backgroundColor, Color? textColor, IconData? icon, }) { final overlay = Overlay.of(context); late OverlayEntry overlayEntry; overlayEntry = OverlayEntry( builder: (context) => Positioned( bottom: 100, left: 20, right: 20, child: Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: backgroundColor ?? AppTheme.textPrimary.withOpacity(0.9), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon( icon, color: textColor ?? Colors.white, size: 20, ), const SizedBox(width: 8), ], Expanded( child: Text( message, style: TextStyle( color: textColor ?? Colors.white, fontSize: 14, ), textAlign: TextAlign.center, ), ), ], ), ), ), ), ); overlay.insert(overlayEntry); Future.delayed(duration, () { overlayEntry.remove(); }); } }