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; static void show(String message, {int seconds = 5, VoidCallback? onComplete, bool showIcon = true, bool showCountdownText = true}) { 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, 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; const _CountdownToast({ required this.message, required this.seconds, required this.onDismiss, this.showIcon = true, this.showCountdownText = true, }); @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) { 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: MyColors.darkTextColor, borderRadius: BorderRadius.circular(12), boxShadow: const [BoxShadow(color: Color(0x33000000), blurRadius: 12, offset: Offset(0, 4))], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showIcon) ...[const Icon(Icons.error_outline, color: Colors.white, size: 20), const SizedBox(width: 10)], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(widget.message, style: const TextStyle(color: Colors.white, fontSize: 14, height: 1.3)), if (widget.showCountdownText) ...[ const SizedBox(height: 4), Text("Dismissing in ${_remaining}s", style: const TextStyle(color: Colors.white70, fontSize: 11)), ], ], ), ), ], ), ), ), ), ), ); } }