Files
superport/lib/widgets/shadcn/shad_dialog.dart

398 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
enum DialogType {
info,
warning,
error,
success,
confirm,
custom,
}
class SuperportShadDialog extends StatelessWidget {
final String title;
final String? description;
final Widget? content;
final List<Widget>? actions;
final DialogType type;
final bool dismissible;
final double? width;
final double? maxHeight;
final VoidCallback? onClose;
final bool showCloseButton;
final bool loading;
final String? loadingMessage;
const SuperportShadDialog({
super.key,
required this.title,
this.description,
this.content,
this.actions,
this.type = DialogType.custom,
this.dismissible = true,
this.width,
this.maxHeight,
this.onClose,
this.showCloseButton = true,
this.loading = false,
this.loadingMessage,
});
static Future<T?> show<T>({
required BuildContext context,
required String title,
String? description,
Widget? content,
List<Widget>? actions,
DialogType type = DialogType.custom,
bool dismissible = true,
double? width,
double? maxHeight,
bool showCloseButton = true,
}) {
return showDialog<T>(
context: context,
barrierDismissible: dismissible,
builder: (context) => SuperportShadDialog(
title: title,
description: description,
content: content,
actions: actions,
type: type,
dismissible: dismissible,
width: width,
maxHeight: maxHeight,
showCloseButton: showCloseButton,
),
);
}
static Future<bool?> confirm({
required BuildContext context,
required String title,
required String message,
String confirmText = '확인',
String cancelText = '취소',
DialogType type = DialogType.confirm,
}) {
return showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => SuperportShadDialog(
title: title,
description: message,
type: type,
dismissible: false,
showCloseButton: false,
actions: [
ShadButton.outline(
onPressed: () => Navigator.of(context).pop(false),
child: Text(cancelText),
),
ShadButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(confirmText),
),
],
),
);
}
static Future<void> alert({
required BuildContext context,
required String title,
required String message,
String confirmText = '확인',
DialogType type = DialogType.info,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => SuperportShadDialog(
title: title,
description: message,
type: type,
dismissible: false,
showCloseButton: false,
actions: [
ShadButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(confirmText),
),
],
),
);
}
static Future<T?> form<T>({
required BuildContext context,
required String title,
required Widget form,
required Future<T?> Function() onSubmit,
String submitText = '저장',
String cancelText = '취소',
double? width,
double? maxHeight,
}) async {
bool isLoading = false;
String? errorMessage;
return await showDialog<T>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setState) {
return SuperportShadDialog(
title: title,
width: width ?? 500,
maxHeight: maxHeight,
dismissible: false,
showCloseButton: !isLoading,
loading: isLoading,
loadingMessage: '처리 중...',
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (errorMessage != null) ...[
ShadAlert.destructive(
title: const Text('오류'),
description: Text(errorMessage!),
),
const SizedBox(height: 16),
],
form,
],
),
actions: isLoading
? null
: [
ShadButton.outline(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(cancelText),
),
ShadButton(
onPressed: () async {
setState(() {
isLoading = true;
errorMessage = null;
});
try {
final result = await onSubmit();
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop(result);
}
} catch (e) {
setState(() {
isLoading = false;
errorMessage = e.toString();
});
}
},
child: Text(submitText),
),
],
);
},
),
);
}
IconData _getIconForType() {
switch (type) {
case DialogType.info:
return Icons.info_outline;
case DialogType.warning:
return Icons.warning_amber_outlined;
case DialogType.error:
return Icons.error_outline;
case DialogType.success:
return Icons.check_circle_outline;
case DialogType.confirm:
return Icons.help_outline;
case DialogType.custom:
default:
return Icons.message_outlined;
}
}
Color _getColorForType(ShadColorScheme colorScheme) {
switch (type) {
case DialogType.info:
return colorScheme.primary;
case DialogType.warning:
return const Color(0xFFFFC107);
case DialogType.error:
return colorScheme.destructive;
case DialogType.success:
return const Color(0xFF2E8B57);
case DialogType.confirm:
return colorScheme.primary;
case DialogType.custom:
default:
return colorScheme.foreground;
}
}
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final typeColor = _getColorForType(theme.colorScheme);
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
width: width ?? 480,
constraints: BoxConstraints(
maxWidth: width ?? 480,
maxHeight: maxHeight ?? MediaQuery.of(context).size.height * 0.8,
),
decoration: BoxDecoration(
color: theme.colorScheme.background,
borderRadius: theme.radius,
border: Border.all(
color: theme.colorScheme.border,
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.colorScheme.border,
width: 1,
),
),
),
child: Row(
children: [
if (type != DialogType.custom) ...[
Icon(
_getIconForType(),
color: typeColor,
size: 24,
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.h4.copyWith(
fontWeight: FontWeight.w600,
),
),
if (description != null && content == null) ...[
const SizedBox(height: 8),
Text(
description!,
style: theme.textTheme.p.copyWith(
color: theme.colorScheme.mutedForeground,
),
),
],
],
),
),
if (showCloseButton && !loading) ...[
const SizedBox(width: 12),
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: onClose ?? () => Navigator.of(context).pop(),
style: IconButton.styleFrom(
foregroundColor: theme.colorScheme.mutedForeground,
),
),
],
],
),
),
if (loading) ...[
Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
if (loadingMessage != null) ...[
const SizedBox(height: 16),
Text(
loadingMessage!,
style: theme.textTheme.muted,
),
],
],
),
),
] else ...[
if (content != null) ...[
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: content!,
),
),
],
if (actions != null && actions!.isNotEmpty) ...[
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: theme.colorScheme.border,
width: 1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
for (int i = 0; i < actions!.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
actions![i],
],
],
),
),
],
],
],
),
),
);
}
}
class FormDialog extends StatefulWidget {
final Widget child;
final GlobalKey<FormState> formKey;
const FormDialog({
super.key,
required this.child,
required this.formKey,
});
@override
State<FormDialog> createState() => _FormDialogState();
}
class _FormDialogState extends State<FormDialog> {
@override
Widget build(BuildContext context) {
return Form(
key: widget.formKey,
child: widget.child,
);
}
}