283 lines
7.8 KiB
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;
|
|
}
|