import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; enum ToastType { info, success, error, warning } abstract final class AppToast { static OverlayEntry? _current; static void show( BuildContext context, String message, { ToastType type = ToastType.info, Duration duration = const Duration(seconds: 4), }) { _current?.remove(); _current = null; final overlay = Overlay.of(context, rootOverlay: true); late OverlayEntry entry; entry = OverlayEntry( builder: (_) => _ToastBanner( message: message, type: type, duration: duration, onDismiss: () { entry.remove(); if (_current == entry) _current = null; }, ), ); _current = entry; overlay.insert(entry); } } class _ToastBanner extends StatefulWidget { const _ToastBanner({ required this.message, required this.type, required this.duration, required this.onDismiss, }); final String message; final ToastType type; final Duration duration; final VoidCallback onDismiss; @override State<_ToastBanner> createState() => _ToastBannerState(); } class _ToastBannerState extends State<_ToastBanner> with SingleTickerProviderStateMixin { late final AnimationController _ctrl; late final Animation _slide; late final Animation _fade; Timer? _timer; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 280), ); _slide = Tween( begin: const Offset(0, -1.2), end: Offset.zero, ).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutCubic)); _fade = CurvedAnimation(parent: _ctrl, curve: Curves.easeOut); unawaited(_ctrl.forward()); _timer = Timer(widget.duration, _dismiss); } void _dismiss() { _timer?.cancel(); unawaited(_ctrl.reverse().then((_) => widget.onDismiss())); } @override void dispose() { _timer?.cancel(); _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final (accentColor, icon) = switch (widget.type) { ToastType.success => (const Color(0xFF34C759), Icons.check_circle_outline_rounded), ToastType.error => (const Color(0xFFFF3B30), Icons.error_outline_rounded), ToastType.warning => (const Color(0xFFFF9F0A), Icons.warning_amber_rounded), ToastType.info => (const Color(0xFF6B63D9), Icons.info_outline_rounded), }; final topPadding = MediaQuery.of(context).padding.top; return Positioned( top: topPadding + 10.h, left: 16.w, right: 16.w, child: SlideTransition( position: _slide, child: FadeTransition( opacity: _fade, child: Material( elevation: 12, shadowColor: Colors.black26, borderRadius: BorderRadius.circular(14.r), color: Colors.white, child: ClipRRect( borderRadius: BorderRadius.circular(14.r), child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container(width: 4.w, color: accentColor), Expanded( child: Padding( padding: EdgeInsets.symmetric( horizontal: 14.w, vertical: 13.h, ), child: Row( children: [ Icon(icon, color: accentColor, size: 20.sp), SizedBox(width: 10.w), Expanded( child: Text( widget.message, style: TextStyle( color: const Color(0xFF1C1C1E), fontSize: 14.sp, fontWeight: FontWeight.w500, height: 1.4, ), ), ), SizedBox(width: 8.w), GestureDetector( onTap: _dismiss, behavior: HitTestBehavior.opaque, child: Padding( padding: EdgeInsets.all(4.w), child: Icon( Icons.close_rounded, color: const Color(0xFFAEAEB2), size: 16.sp, ), ), ), ], ), ), ), ], ), ), ), ), ), ), ); } }