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