Files
venera/lib/components/message.dart
2025-01-20 15:02:36 +08:00

461 lines
12 KiB
Dart

part of "components.dart";
void showToast({
required String message,
required BuildContext context,
Widget? icon,
Widget? trailing,
int? seconds,
}) {
var newEntry = OverlayEntry(
builder: (context) => _ToastOverlay(
message: message,
icon: icon,
trailing: trailing,
));
var state = context.findAncestorStateOfType<OverlayWidgetState>();
state?.addOverlay(newEntry);
Timer(Duration(seconds: 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<OverlayWidget> createState() => OverlayWidgetState();
}
class OverlayWidgetState extends State<OverlayWidget> {
final overlayKey = GlobalKey<OverlayState>();
var entries = <OverlayEntry>[];
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<void> 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,
this.title, // 如果不传 title 将不会展示
required this.content,
this.dismissible = true,
this.actions = const [],
});
final String? title;
final Widget content;
final List<Widget> actions;
final bool dismissible;
@override
Widget build(BuildContext context) {
var content = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title != null
? Appbar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: dismissible ? context.pop : null,
),
title: Text(title!),
backgroundColor: Colors.transparent,
)
: const SizedBox.shrink(),
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<void> showInputDialog({
required BuildContext context,
required String title,
String? hintText,
required FutureOr<Object?> 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),
),
],
);
},
);
}
Future<int?> showSelectDialog({
required String title,
required List<String> options,
int? initialIndex,
}) async {
int? current = initialIndex;
await showDialog(
context: App.rootContext,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return ContentDialog(
title: title,
content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Select(
current: current == null ? "" : options[current!],
values: options,
minWidth: 156,
onTap: (i) {
setState(() {
current = i;
});
},
)
],
),
),
actions: [
TextButton(
onPressed: () {
current = null;
context.pop();
},
child: Text('Cancel'.tl),
),
FilledButton(
onPressed: current == null
? null
: context.pop,
child: Text('Confirm'.tl),
),
],
);
},
);
},
);
return current;
}