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 _fadeAnimation; late Animation _slideAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: DesignSystem.durationMedium, ); _fadeAnimation = Tween( 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( 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 showCustomBottomSheet({ 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( 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 showCustomDialog({ required BuildContext context, required Widget child, bool barrierDismissible = true, }) { return showGeneralDialog( context: context, barrierDismissible: barrierDismissible, barrierColor: Colors.black54, transitionDuration: DesignSystem.durationMedium, transitionBuilder: (context, animation, secondaryAnimation, child) { return ScaleTransition( scale: Tween( 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, ); } }