mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
490 lines
12 KiB
Dart
490 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 {
|
|
double? _progress;
|
|
|
|
String? _message;
|
|
|
|
void Function()? _closeDialog;
|
|
|
|
void Function(double? value)? _serProgress;
|
|
|
|
void Function(String message)? _setMessage;
|
|
|
|
bool closed = false;
|
|
|
|
void close() {
|
|
if (closed) {
|
|
return;
|
|
}
|
|
closed = true;
|
|
if (_closeDialog == null) {
|
|
Future.microtask(_closeDialog!);
|
|
} else {
|
|
_closeDialog!();
|
|
}
|
|
}
|
|
|
|
void setProgress(double? value) {
|
|
if (closed) {
|
|
return;
|
|
}
|
|
_serProgress?.call(value);
|
|
}
|
|
|
|
void setMessage(String message) {
|
|
if (closed) {
|
|
return;
|
|
}
|
|
_setMessage?.call(message);
|
|
}
|
|
}
|
|
|
|
LoadingDialogController showLoadingDialog(
|
|
BuildContext context, {
|
|
void Function()? onCancel,
|
|
bool barrierDismissible = true,
|
|
bool allowCancel = true,
|
|
String? message,
|
|
String cancelButtonText = "Cancel",
|
|
bool withProgress = false,
|
|
}) {
|
|
var controller = LoadingDialogController();
|
|
controller._message = message;
|
|
|
|
if (withProgress) {
|
|
controller._progress = 0;
|
|
}
|
|
|
|
var loadingDialogRoute = DialogRoute(
|
|
context: context,
|
|
barrierDismissible: barrierDismissible,
|
|
builder: (BuildContext context) {
|
|
return StatefulBuilder(builder: (context, setState) {
|
|
controller._serProgress = (value) {
|
|
setState(() {
|
|
controller._progress = value;
|
|
});
|
|
};
|
|
controller._setMessage = (message) {
|
|
setState(() {
|
|
controller._message = message;
|
|
});
|
|
};
|
|
return ContentDialog(
|
|
title: controller._message ?? 'Loading',
|
|
content: LinearProgressIndicator(
|
|
value: controller._progress,
|
|
backgroundColor: context.colorScheme.surfaceContainer,
|
|
).paddingHorizontal(16).paddingVertical(16),
|
|
actions: [
|
|
FilledButton(
|
|
onPressed: allowCancel
|
|
? () {
|
|
controller.close();
|
|
onCancel?.call();
|
|
}
|
|
: null,
|
|
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;
|
|
}
|