import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; enum _SnackVariant { success, error, info, warning } extension _VariantProps on _SnackVariant { Color get baseColor => switch (this) { _SnackVariant.success => const Color(0xFF1A9E5C), _SnackVariant.error => const Color(0xFFD93025), _SnackVariant.info => const Color(0xFF1A73E8), _SnackVariant.warning => const Color(0xFFF29900), }; Color get surfaceColor => switch (this) { _SnackVariant.success => const Color(0xFFF0FBF5), _SnackVariant.error => const Color(0xFFFEF2F1), _SnackVariant.info => const Color(0xFFF0F6FF), _SnackVariant.warning => const Color(0xFFFFF8E6), }; IconData get icon => switch (this) { _SnackVariant.success => Icons.check_circle_rounded, _SnackVariant.error => Icons.error_rounded, _SnackVariant.info => Icons.info_rounded, _SnackVariant.warning => Icons.warning_amber_rounded, }; String get label => switch (this) { _SnackVariant.success => 'Success', _SnackVariant.error => 'Error', _SnackVariant.info => 'Info', _SnackVariant.warning => 'Warning', }; } class _SnackContent extends StatefulWidget { final String message; final _SnackVariant variant; const _SnackContent({required this.message, required this.variant}); @override State<_SnackContent> createState() => _SnackContentState(); } class _SnackContentState extends State<_SnackContent> with TickerProviderStateMixin { late final AnimationController _ctrl; late final AnimationController _scaleCtrl; late final Animation _scaleAnim; late final Animation _progressAnim; static const Duration _displayDuration = Duration(seconds: 4); @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: _displayDuration); _scaleCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); _scaleAnim = CurvedAnimation( parent: _scaleCtrl, curve: Curves.elasticOut, ); _progressAnim = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _ctrl, curve: Curves.linear), ); _scaleCtrl.forward(); _ctrl.forward(); } @override void dispose() { _ctrl.dispose(); _scaleCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final v = widget.variant; final accent = v.baseColor; final surface = v.surfaceColor; return Container( margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: surface, borderRadius: BorderRadius.circular(18), border: Border.all(color: accent.withAlpha(46), width: 1.2), boxShadow: [ BoxShadow( color: accent.withAlpha(31), blurRadius: 20, spreadRadius: -2, offset: const Offset(0, 6), ), BoxShadow( color: Colors.black.withAlpha(15), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(18), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(14, 14, 10, 12), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ ScaleTransition( scale: _scaleAnim, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: accent.withAlpha(31), shape: BoxShape.circle, ), child: Icon(v.icon, color: accent, size: 22), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( v.label.tr, style: TextStyle( color: accent, fontSize: 13, fontWeight: FontWeight.w700, letterSpacing: 0.2, ), ), const SizedBox(height: 3), Text( widget.message, style: const TextStyle( color: Color(0xFF424242), fontSize: 13.5, height: 1.4, fontWeight: FontWeight.w400, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), GestureDetector( onTap: () { HapticFeedback.lightImpact(); _closeSnackbar(context); }, child: Container( width: 30, height: 30, margin: const EdgeInsets.only(left: 6), decoration: BoxDecoration( color: Colors.grey.withAlpha(25), shape: BoxShape.circle, ), child: Icon( Icons.close_rounded, size: 16, color: Colors.grey[500], ), ), ), ], ), ), AnimatedBuilder( animation: _progressAnim, builder: (_, __) => Stack( children: [ Container(height: 3, color: accent.withAlpha(20)), FractionallySizedBox( widthFactor: _progressAnim.value, child: Container( height: 3, decoration: BoxDecoration( color: accent.withAlpha(115), borderRadius: const BorderRadius.only( topRight: Radius.circular(4), bottomRight: Radius.circular(4), ), ), ), ), ], ), ), ], ), ), ); } void _closeSnackbar(BuildContext context) { ScaffoldMessenger.maybeOf(context)?.hideCurrentSnackBar(); } } int _retryCount = 0; void _show(_SnackVariant variant, String message) { if (Get.context == null) { if (_retryCount < 3) { _retryCount++; WidgetsBinding.instance.addPostFrameCallback((_) => _show(variant, message)); } return; } _retryCount = 0; final context = Get.context; if (context == null) return; final messenger = ScaffoldMessenger.maybeOf(context); if (messenger == null) return; messenger.clearSnackBars(); switch (variant) { case _SnackVariant.error: case _SnackVariant.warning: HapticFeedback.mediumImpact(); case _SnackVariant.success: HapticFeedback.lightImpact(); case _SnackVariant.info: HapticFeedback.selectionClick(); } messenger.showSnackBar( SnackBar( content: _SnackContent(message: message, variant: variant), backgroundColor: Colors.transparent, elevation: 0, margin: EdgeInsets.zero, padding: EdgeInsets.zero, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 4), dismissDirection: DismissDirection.up, ), ); } void mySnackbarSuccess(String message) => _show(_SnackVariant.success, message); void mySnackbarError(String message) => _show(_SnackVariant.error, message); void mySnackbarInfo(String message) => _show(_SnackVariant.info, message); void mySnackbarWarning(String message) => _show(_SnackVariant.warning, message);