import 'dart:async'; import 'package:flutter/material.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/config/routes.dart'; /// Self-dismissing toast with a visible countdown, replacing the generic "just closes" /// ConfirmDialog for pure-info messages. Mounts via the root navigator's Overlay, so callers /// don't need to pass a BuildContext (avoids the stale-context issues the dialog path has). class AppToast { static OverlayEntry? _entry; // Single place to retune how long an action-performing toast waits before firing its // onComplete (either naturally, or immediately if the user taps it early - see _dismiss). static const int actionSeconds = 4; // Duration for the plain toast that replaces Utils.showToast's old Fluttertoast call - // change this single value to retune it everywhere that uses it. static const int plainSeconds = 3; // Shared success/error colors - same combo as dependent_details_screen.dart's // "Active"/"Access Off" badges. Any direct AppToast.show(...) call (not going through // Utils.showToast) must pass these explicitly, otherwise it silently falls back to the // plain dark/white style below regardless of whether it's reporting success or failure. static const Color errorBgColor = Color(0xFFF1CECE); static const Color errorTextColor = Color(0xFFC53030); static const Color successBgColor = Color(0xFFECFDF5); static const Color successTextColor = Color(0xFF059669); static void show(String message, {int seconds = 5, VoidCallback? onComplete, bool showIcon = true, bool showCountdownText = true, Color? bgColor, Color? textColor}) { OverlayState? overlay = AppRoutes.navigatorKey.currentState?.overlay; if (overlay == null) return; // Only one toast on screen at a time - a new call replaces whatever is currently showing. // Its onComplete (if any) never fires in that case - _dismiss only acts on the entry // that's still current. _entry?.remove(); _entry = null; late OverlayEntry entry; entry = OverlayEntry( builder: (BuildContext context) => _CountdownToast( message: message, seconds: seconds, showIcon: showIcon, showCountdownText: showCountdownText, bgColor: bgColor, textColor: textColor, onDismiss: () => _dismiss(entry, onComplete), ), ); _entry = entry; overlay.insert(entry); } // Fires on both the timer completing and an early tap - a tap just means "don't make me // wait", not "cancel this". Guarded so a toast that's already been replaced never fires its // (now stale) action. static void _dismiss(OverlayEntry entry, VoidCallback? onComplete) { if (_entry == entry) { entry.remove(); _entry = null; onComplete?.call(); } } } class _CountdownToast extends StatefulWidget { final String message; final int seconds; final VoidCallback onDismiss; final bool showIcon; final bool showCountdownText; final Color? bgColor; final Color? textColor; const _CountdownToast({required this.message, required this.seconds, required this.onDismiss, this.showIcon = true, this.showCountdownText = true, this.bgColor, this.textColor}); @override State<_CountdownToast> createState() => _CountdownToastState(); } class _CountdownToastState extends State<_CountdownToast> { late int _remaining = widget.seconds; Timer? _timer; @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { setState(() => _remaining--); if (_remaining <= 0) { t.cancel(); widget.onDismiss(); } }); } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { Color bgColor = widget.bgColor ?? MyColors.darkTextColor; Color textColor = widget.textColor ?? Colors.white; return Positioned( left: 16, right: 16, top: 24, child: SafeArea( bottom: false, child: Material( color: Colors.transparent, child: GestureDetector( onTap: widget.onDismiss, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(10), boxShadow: const [BoxShadow(color: Color(0x33000000), blurRadius: 12, offset: Offset(0, 4))], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showIcon) ...[Icon(Icons.error_outline, color: textColor, size: 20), const SizedBox(width: 10)], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(widget.message, style: TextStyle(color: textColor, fontSize: 14, height: 1.3, fontWeight: FontWeight.w600)), if (widget.showCountdownText) ...[ const SizedBox(height: 4), Text("Dismissing in ${_remaining}s", style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12, fontWeight: FontWeight.w500)), ], ], ), ), ], ), ), ), ), ), ); } }