Files
App/lib/widgets/alert.dart

283 lines
7.8 KiB
Dart

import 'dart:async';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/main.dart';
import 'package:island/talker.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
void showSnackBar(String message, {SnackBarAction? action}) {
final context = globalOverlay.currentState!.context;
final screenWidth = MediaQuery.of(context).size.width;
final padding = 40.0;
final availableWidth = screenWidth - padding;
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: availableWidth.clamp(0, 400),
maxWidth: availableWidth.clamp(0, 600),
),
child: Card(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainer,
child: Text(message).padding(horizontal: 20, vertical: 16),
),
),
),
curve: Curves.easeInOut,
snackBarPosition: SnackBarPosition.bottom,
);
}
OverlayEntry? _loadingOverlay;
GlobalKey<_FadeOverlayState> _loadingOverlayKey = GlobalKey();
class _FadeOverlay extends StatefulWidget {
const _FadeOverlay({
super.key,
this.child,
this.builder,
this.duration = const Duration(milliseconds: 200),
this.curve = Curves.linear,
}) : assert(child != null || builder != null);
final Widget? child;
final Widget Function(BuildContext, Animation<double>)? builder;
final Duration duration;
final Curve curve;
@override
State<_FadeOverlay> createState() => _FadeOverlayState();
}
class _FadeOverlayState extends State<_FadeOverlay>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> animateOut() async {
await _controller.reverse();
}
@override
Widget build(BuildContext context) {
final animation = CurvedAnimation(parent: _controller, curve: widget.curve);
if (widget.builder != null) {
return widget.builder!(context, animation);
}
return FadeTransition(opacity: animation, child: widget.child);
}
}
void showLoadingModal(BuildContext context) {
if (_loadingOverlay != null) return;
_loadingOverlay = OverlayEntry(
builder:
(context) => _FadeOverlay(
key: _loadingOverlayKey,
child: Material(
color: Colors.black54,
child: Center(
child: Material(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
elevation: 4,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(year2023: false),
const Gap(24),
Text('loading'.tr()),
],
).padding(all: 32),
),
),
),
),
);
Overlay.of(context).insert(_loadingOverlay!);
}
void hideLoadingModal(BuildContext context) async {
if (_loadingOverlay == null) return;
final entry = _loadingOverlay!;
_loadingOverlay = null;
final state = entry.mounted ? _loadingOverlayKey.currentState : null;
if (state != null) {
await state.animateOut();
}
entry.remove();
}
String _parseRemoteError(DioException err) {
String? message;
if (err.response?.data is String) {
message = err.response?.data;
} else if (err.response?.data?['message'] != null) {
message = <String?>[
err.response?.data?['message']?.toString(),
err.response?.data?['detail']?.toString(),
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
} else if (err.response?.data?['errors'] != null) {
final errors = err.response?.data['errors'] as Map<String, dynamic>;
message = errors.values
.map(
(ele) =>
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
)
.join('\n');
}
if (message == null || message.isEmpty) message = err.response?.statusMessage;
message ??= err.message;
return message ?? err.toString();
}
Future<T?> showOverlayDialog<T>({
required Widget Function(BuildContext context, void Function(T? result) close)
builder,
bool barrierDismissible = true,
}) {
final completer = Completer<T?>();
final key = GlobalKey<_FadeOverlayState>();
late OverlayEntry entry;
void close(T? result) async {
if (completer.isCompleted) return;
final state = key.currentState;
if (state != null) {
await state.animateOut();
}
entry.remove();
completer.complete(result);
}
entry = OverlayEntry(
builder:
(context) => _FadeOverlay(
key: key,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
builder: (context, animation) {
return Stack(
children: [
Positioned.fill(
child: FadeTransition(
opacity: animation,
child: GestureDetector(
onTap: barrierDismissible ? () => close(null) : null,
behavior: HitTestBehavior.opaque,
child: const ColoredBox(color: Colors.black54),
),
),
),
Center(
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: builder(context, close),
),
),
),
],
);
},
),
);
globalOverlay.currentState!.insert(entry);
return completer.future;
}
void showErrorAlert(dynamic err) {
if (err is Error) {
talker.error('Something went wrong...', err, err.stackTrace);
}
final text = switch (err) {
String _ => err,
DioException _ => _parseRemoteError(err),
Exception _ => err.toString(),
_ => err.toString(),
};
showOverlayDialog<void>(
builder:
(context, close) => AlertDialog(
title: Text('somethingWentWrong'.tr()),
content: Text(text),
actions: [
TextButton(
onPressed: () => close(null),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
);
}
void showInfoAlert(String message, String title) {
showOverlayDialog<void>(
builder:
(context, close) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => close(null),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
);
}
Future<bool> showConfirmAlert(String message, String title) async {
final result = await showOverlayDialog<bool>(
builder:
(context, close) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => close(false),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
onPressed: () => close(true),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
);
return result ?? false;
}