part of "components.dart"; void showToast({ required String message, required BuildContext context, Widget? icon, Widget? trailing, }) { var newEntry = OverlayEntry( builder: (context) => _ToastOverlay( message: message, icon: icon, trailing: trailing, )); var state = context.findAncestorStateOfType(); state?.addOverlay(newEntry); Timer(const Duration(seconds: 2), () => state?.remove(newEntry)); } class _ToastOverlay extends StatelessWidget { const _ToastOverlay({required this.message, this.icon, this.trailing}); final String message; final Widget? icon; final Widget? trailing; @override Widget build(BuildContext context) { return Positioned( bottom: 24 + MediaQuery.of(context).viewInsets.bottom, left: 0, right: 0, child: Align( alignment: Alignment.bottomCenter, child: Material( color: Theme.of(context).colorScheme.inverseSurface, borderRadius: BorderRadius.circular(8), elevation: 2, textStyle: ts.withColor(Theme.of(context).colorScheme.onInverseSurface), child: IconTheme( data: IconThemeData( color: Theme.of(context).colorScheme.onInverseSurface), child: IntrinsicWidth( child: Container( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), constraints: BoxConstraints( maxWidth: context.width - 32, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) icon!.paddingRight(8), Expanded( child: Text( message, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500), maxLines: 3, overflow: TextOverflow.ellipsis, ), ), if (trailing != null) trailing!.paddingLeft(8) ], ), ), ), ), ), ), ); } } class OverlayWidget extends StatefulWidget { const OverlayWidget(this.child, {super.key}); final Widget child; @override State createState() => OverlayWidgetState(); } class OverlayWidgetState extends State { final overlayKey = GlobalKey(); var entries = []; void addOverlay(OverlayEntry entry) { if (overlayKey.currentState != null) { overlayKey.currentState!.insert(entry); entries.add(entry); } } void remove(OverlayEntry entry) { if (entries.remove(entry)) { entry.remove(); } } void removeAll() { for (var entry in entries) { entry.remove(); } entries.clear(); } @override Widget build(BuildContext context) { return Overlay( key: overlayKey, initialEntries: [OverlayEntry(builder: (context) => widget.child)], ); } } void showDialogMessage(BuildContext context, String title, String message) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: Text(message), actions: [ TextButton( onPressed: context.pop, child: Text("OK".tl), ) ], ), ); } Future showConfirmDialog({ required BuildContext context, required String title, required String content, required void Function() onConfirm, String confirmText = "Confirm", Color? btnColor, }) { return showDialog( context: context, builder: (context) => ContentDialog( title: title, content: Text(content).paddingHorizontal(16).paddingVertical(8), actions: [ FilledButton( onPressed: () { context.pop(); onConfirm(); }, style: FilledButton.styleFrom( backgroundColor: btnColor, ), child: Text(confirmText.tl), ), ], ), ); } class LoadingDialogController { void Function()? closeDialog; bool closed = false; void close() { if (closed) { return; } closed = true; if (closeDialog == null) { Future.microtask(closeDialog!); } else { closeDialog!(); } } } LoadingDialogController showLoadingDialog(BuildContext context, {void Function()? onCancel, bool barrierDismissible = true, bool allowCancel = true, String? message, String cancelButtonText = "Cancel"}) { var controller = LoadingDialogController(); var loadingDialogRoute = DialogRoute( context: context, barrierDismissible: barrierDismissible, builder: (BuildContext context) { return Dialog( child: Container( width: 100, padding: const EdgeInsets.all(16.0), child: Row( children: [ const SizedBox( width: 30, height: 30, child: CircularProgressIndicator(), ), const SizedBox( width: 16, ), Text( message ?? 'Loading', style: const TextStyle(fontSize: 16), ), const Spacer(), if (allowCancel) TextButton( onPressed: () { controller.close(); onCancel?.call(); }, child: Text(cancelButtonText.tl)) ], ), ), ); }); var navigator = Navigator.of(context, rootNavigator: true); navigator.push(loadingDialogRoute).then((value) => controller.closed = true); controller.closeDialog = () { navigator.removeRoute(loadingDialogRoute); }; return controller; } class ContentDialog extends StatelessWidget { const ContentDialog({ super.key, required this.title, required this.content, this.dismissible = true, this.actions = const [], }); final String title; final Widget content; final List actions; final bool dismissible; @override Widget build(BuildContext context) { var content = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Appbar( leading: IconButton( icon: const Icon(Icons.close), onPressed: dismissible ? context.pop : null, ), title: Text(title), backgroundColor: Colors.transparent, ), this.content, const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.end, children: actions, ).paddingRight(12), const SizedBox(height: 16), ], ); return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: context.brightness == Brightness.dark ? BorderSide(color: context.colorScheme.outlineVariant) : BorderSide.none, ), insetPadding: context.width < 400 ? const EdgeInsets.symmetric(horizontal: 4) : const EdgeInsets.symmetric(horizontal: 16), elevation: 2, shadowColor: context.colorScheme.shadow, backgroundColor: context.colorScheme.surface, child: AnimatedSize( duration: const Duration(milliseconds: 200), alignment: Alignment.topCenter, child: IntrinsicWidth( child: ConstrainedBox( constraints: BoxConstraints( maxWidth: 600, minWidth: math.min(400, context.width - 16), ), child: MediaQuery.removePadding( removeTop: true, removeBottom: true, context: context, child: content, ), ), ), ), ); } } Future showInputDialog({ required BuildContext context, required String title, String? hintText, required FutureOr Function(String) onConfirm, String? initialValue, String confirmText = "Confirm", String cancelText = "Cancel", RegExp? inputValidator, }) { var controller = TextEditingController(text: initialValue); bool isLoading = false; String? error; return showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setState) { return ContentDialog( title: title, content: TextField( controller: controller, decoration: InputDecoration( hintText: hintText, border: const OutlineInputBorder(), errorText: error, ), ).paddingHorizontal(12), actions: [ Button.filled( isLoading: isLoading, onPressed: () async { if (inputValidator != null && !inputValidator.hasMatch(controller.text)) { setState(() => error = "Invalid input"); return; } var futureOr = onConfirm(controller.text); Object? result; if (futureOr is Future) { setState(() => isLoading = true); result = await futureOr; setState(() => isLoading = false); } else { result = futureOr; } if(result == null) { context.pop(); } else { setState(() => error = result.toString()); } }, child: Text(confirmText.tl), ), ], ); }, ); }, ); } void showInfoDialog({ required BuildContext context, required String title, required String content, String confirmText = "OK", }) { showDialog( context: context, builder: (context) { return ContentDialog( title: title, content: Text(content).paddingHorizontal(16).paddingVertical(8), actions: [ Button.filled( onPressed: context.pop, child: Text(confirmText.tl), ), ], ); }, ); }