353 lines
9.5 KiB
Dart
353 lines
9.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../shared/theme/app_theme.dart';
|
|
|
|
/// Service de notifications animées
|
|
class AnimatedNotifications {
|
|
static OverlayEntry? _currentOverlay;
|
|
|
|
/// Affiche une notification de succès
|
|
static void showSuccess(
|
|
BuildContext context,
|
|
String message, {
|
|
Duration duration = const Duration(seconds: 3),
|
|
}) {
|
|
_showNotification(
|
|
context,
|
|
message,
|
|
NotificationType.success,
|
|
duration,
|
|
);
|
|
}
|
|
|
|
/// Affiche une notification d'erreur
|
|
static void showError(
|
|
BuildContext context,
|
|
String message, {
|
|
Duration duration = const Duration(seconds: 4),
|
|
}) {
|
|
_showNotification(
|
|
context,
|
|
message,
|
|
NotificationType.error,
|
|
duration,
|
|
);
|
|
}
|
|
|
|
/// Affiche une notification d'information
|
|
static void showInfo(
|
|
BuildContext context,
|
|
String message, {
|
|
Duration duration = const Duration(seconds: 3),
|
|
}) {
|
|
_showNotification(
|
|
context,
|
|
message,
|
|
NotificationType.info,
|
|
duration,
|
|
);
|
|
}
|
|
|
|
/// Affiche une notification d'avertissement
|
|
static void showWarning(
|
|
BuildContext context,
|
|
String message, {
|
|
Duration duration = const Duration(seconds: 3),
|
|
}) {
|
|
_showNotification(
|
|
context,
|
|
message,
|
|
NotificationType.warning,
|
|
duration,
|
|
);
|
|
}
|
|
|
|
static void _showNotification(
|
|
BuildContext context,
|
|
String message,
|
|
NotificationType type,
|
|
Duration duration,
|
|
) {
|
|
// Supprimer la notification précédente si elle existe
|
|
_currentOverlay?.remove();
|
|
|
|
final overlay = Overlay.of(context);
|
|
late OverlayEntry overlayEntry;
|
|
|
|
overlayEntry = OverlayEntry(
|
|
builder: (context) => AnimatedNotificationWidget(
|
|
message: message,
|
|
type: type,
|
|
onDismiss: () {
|
|
overlayEntry.remove();
|
|
_currentOverlay = null;
|
|
},
|
|
),
|
|
);
|
|
|
|
_currentOverlay = overlayEntry;
|
|
overlay.insert(overlayEntry);
|
|
|
|
// Auto-dismiss après la durée spécifiée
|
|
Future.delayed(duration, () {
|
|
if (_currentOverlay == overlayEntry) {
|
|
overlayEntry.remove();
|
|
_currentOverlay = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Masque la notification actuelle
|
|
static void dismiss() {
|
|
_currentOverlay?.remove();
|
|
_currentOverlay = null;
|
|
}
|
|
}
|
|
|
|
/// Widget de notification animée
|
|
class AnimatedNotificationWidget extends StatefulWidget {
|
|
final String message;
|
|
final NotificationType type;
|
|
final VoidCallback onDismiss;
|
|
|
|
const AnimatedNotificationWidget({
|
|
super.key,
|
|
required this.message,
|
|
required this.type,
|
|
required this.onDismiss,
|
|
});
|
|
|
|
@override
|
|
State<AnimatedNotificationWidget> createState() => _AnimatedNotificationWidgetState();
|
|
}
|
|
|
|
class _AnimatedNotificationWidgetState extends State<AnimatedNotificationWidget>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _slideController;
|
|
late AnimationController _fadeController;
|
|
late AnimationController _scaleController;
|
|
|
|
late Animation<Offset> _slideAnimation;
|
|
late Animation<double> _fadeAnimation;
|
|
late Animation<double> _scaleAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_slideController = AnimationController(
|
|
duration: const Duration(milliseconds: 500),
|
|
vsync: this,
|
|
);
|
|
|
|
_fadeController = AnimationController(
|
|
duration: const Duration(milliseconds: 300),
|
|
vsync: this,
|
|
);
|
|
|
|
_scaleController = AnimationController(
|
|
duration: const Duration(milliseconds: 200),
|
|
vsync: this,
|
|
);
|
|
|
|
_slideAnimation = Tween<Offset>(
|
|
begin: const Offset(0, -1),
|
|
end: Offset.zero,
|
|
).animate(CurvedAnimation(
|
|
parent: _slideController,
|
|
curve: Curves.elasticOut,
|
|
));
|
|
|
|
_fadeAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _fadeController,
|
|
curve: Curves.easeOut,
|
|
));
|
|
|
|
_scaleAnimation = Tween<double>(
|
|
begin: 1.0,
|
|
end: 1.05,
|
|
).animate(CurvedAnimation(
|
|
parent: _scaleController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
|
|
// Démarrer les animations d'entrée
|
|
_fadeController.forward();
|
|
_slideController.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_slideController.dispose();
|
|
_fadeController.dispose();
|
|
_scaleController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _dismiss() async {
|
|
await _fadeController.reverse();
|
|
widget.onDismiss();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = _getColors();
|
|
|
|
return Positioned(
|
|
top: MediaQuery.of(context).padding.top + 16,
|
|
left: 16,
|
|
right: 16,
|
|
child: AnimatedBuilder(
|
|
animation: Listenable.merge([
|
|
_slideAnimation,
|
|
_fadeAnimation,
|
|
_scaleAnimation,
|
|
]),
|
|
builder: (context, child) {
|
|
return SlideTransition(
|
|
position: _slideAnimation,
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: GestureDetector(
|
|
onTap: () => _scaleController.forward().then((_) {
|
|
_scaleController.reverse();
|
|
}),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
colors.backgroundColor,
|
|
colors.backgroundColor.withOpacity(0.9),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: colors.backgroundColor.withOpacity(0.3),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Icône
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: colors.iconBackgroundColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
colors.icon,
|
|
color: colors.iconColor,
|
|
size: 24,
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
// Message
|
|
Expanded(
|
|
child: Text(
|
|
widget.message,
|
|
style: TextStyle(
|
|
color: colors.textColor,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Bouton de fermeture
|
|
GestureDetector(
|
|
onTap: _dismiss,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4),
|
|
child: Icon(
|
|
Icons.close,
|
|
color: colors.textColor.withOpacity(0.7),
|
|
size: 20,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
_NotificationColors _getColors() {
|
|
switch (widget.type) {
|
|
case NotificationType.success:
|
|
return _NotificationColors(
|
|
backgroundColor: AppTheme.successColor,
|
|
textColor: Colors.white,
|
|
icon: Icons.check_circle,
|
|
iconColor: Colors.white,
|
|
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
|
);
|
|
case NotificationType.error:
|
|
return _NotificationColors(
|
|
backgroundColor: AppTheme.errorColor,
|
|
textColor: Colors.white,
|
|
icon: Icons.error,
|
|
iconColor: Colors.white,
|
|
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
|
);
|
|
case NotificationType.warning:
|
|
return _NotificationColors(
|
|
backgroundColor: AppTheme.warningColor,
|
|
textColor: Colors.white,
|
|
icon: Icons.warning,
|
|
iconColor: Colors.white,
|
|
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
|
);
|
|
case NotificationType.info:
|
|
return _NotificationColors(
|
|
backgroundColor: AppTheme.primaryColor,
|
|
textColor: Colors.white,
|
|
icon: Icons.info,
|
|
iconColor: Colors.white,
|
|
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _NotificationColors {
|
|
final Color backgroundColor;
|
|
final Color textColor;
|
|
final IconData icon;
|
|
final Color iconColor;
|
|
final Color iconBackgroundColor;
|
|
|
|
_NotificationColors({
|
|
required this.backgroundColor,
|
|
required this.textColor,
|
|
required this.icon,
|
|
required this.iconColor,
|
|
required this.iconBackgroundColor,
|
|
});
|
|
}
|
|
|
|
enum NotificationType {
|
|
success,
|
|
error,
|
|
warning,
|
|
info,
|
|
}
|