You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hmg-mohemm-flutter-app/lib/widgets/toast/app_toast.dart

145 lines
5.0 KiB
Dart

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, 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>[BoxShadow(color: Color(0x33000000), blurRadius: 12, offset: Offset(0, 4))],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (widget.showIcon) ...<Widget>[Icon(Icons.error_outline, color: textColor, size: 20), const SizedBox(width: 10)],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(widget.message, style: TextStyle(color: textColor, fontSize: 14, height: 1.3, fontWeight: FontWeight.w600)),
if (widget.showCountdownText) ...<Widget>[
const SizedBox(height: 4),
Text("Dismissing in ${_remaining}s", style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12, fontWeight: FontWeight.w500)),
],
],
),
),
],
),
),
),
),
),
);
}
}