import 'package:flutter/material.dart'; import '../../core/constants/design_system.dart'; import '../../domain/entities/notification.dart' as domain; import 'animated_widgets.dart'; /// Notification in-app affichée en overlay au-dessus du contenu. /// /// S'affiche en haut de l'écran avec une animation de slide down /// et disparaît automatiquement après quelques secondes. /// /// **Usage:** /// ```dart /// InAppNotification.show( /// context: context, /// notification: myNotification, /// onTap: () { /// // Naviguer vers la notification /// }, /// ); /// ``` class InAppNotification { /// Affiche une notification in-app en overlay. static void show({ required BuildContext context, required domain.Notification notification, Duration duration = const Duration(seconds: 4), VoidCallback? onTap, VoidCallback? onDismiss, }) { final overlay = Overlay.of(context); late OverlayEntry overlayEntry; overlayEntry = OverlayEntry( builder: (context) => _InAppNotificationWidget( notification: notification, onTap: () { overlayEntry.remove(); onTap?.call(); }, onDismiss: () { overlayEntry.remove(); onDismiss?.call(); }, ), ); overlay.insert(overlayEntry); // Supprime automatiquement après la durée spécifiée Future.delayed(duration, () { if (overlayEntry.mounted) { overlayEntry.remove(); onDismiss?.call(); } }); } } class _InAppNotificationWidget extends StatefulWidget { const _InAppNotificationWidget({ required this.notification, required this.onTap, required this.onDismiss, }); final domain.Notification notification; final VoidCallback onTap; final VoidCallback onDismiss; @override State<_InAppNotificationWidget> createState() => _InAppNotificationWidgetState(); } class _InAppNotificationWidgetState extends State<_InAppNotificationWidget> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _slideAnimation; late Animation _fadeAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: DesignSystem.durationNormal, vsync: this, ); _slideAnimation = Tween( begin: const Offset(0, -1), end: Offset.zero, ).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeOutCubic, ), ); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeIn, ), ); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Positioned( top: 0, left: 0, right: 0, child: SlideTransition( position: _slideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: SafeArea( child: Padding( padding: const EdgeInsets.all(DesignSystem.paddingMedium), child: Dismissible( key: ValueKey(widget.notification.id), direction: DismissDirection.up, onDismissed: (direction) { widget.onDismiss(); }, child: AnimatedScaleButton( onTap: widget.onTap, child: Container( decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(DesignSystem.radiusMedium), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: widget.onTap, borderRadius: BorderRadius.circular(DesignSystem.radiusMedium), child: Padding( padding: const EdgeInsets.all(DesignSystem.paddingMedium), child: Row( children: [ // Icône selon le type Container( padding: const EdgeInsets.all(DesignSystem.paddingSmall), decoration: BoxDecoration( color: _getNotificationColor(widget.notification.type) .withOpacity(0.2), borderRadius: BorderRadius.circular(DesignSystem.radiusSmall), ), child: Icon( _getNotificationIcon(widget.notification.type), color: _getNotificationColor(widget.notification.type), size: 24, ), ), const SizedBox(width: DesignSystem.paddingMedium), // Contenu Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( widget.notification.title, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( widget.notification.message, style: theme.textTheme.bodyMedium?.copyWith( color: theme.textTheme.bodySmall?.color, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), // Bouton fermer IconButton( icon: const Icon(Icons.close, size: 20), onPressed: widget.onDismiss, padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), ), ], ), ), ), ), ), ), ), ), ), ), ), ); } Color _getNotificationColor(domain.NotificationType type) { switch (type) { case domain.NotificationType.event: return Colors.blue; case domain.NotificationType.friend: return Colors.green; case domain.NotificationType.reminder: return Colors.orange; case domain.NotificationType.other: return Colors.grey; } } IconData _getNotificationIcon(domain.NotificationType type) { switch (type) { case domain.NotificationType.event: return Icons.event; case domain.NotificationType.friend: return Icons.person_add; case domain.NotificationType.reminder: return Icons.alarm; case domain.NotificationType.other: return Icons.notifications; } } }