diff --git a/lib/Entities/dialog_type_enum.dart b/lib/Entities/dialog_type_enum.dart new file mode 100644 index 0000000..7978ae1 --- /dev/null +++ b/lib/Entities/dialog_type_enum.dart @@ -0,0 +1,11 @@ +/// Eine Enum, um den Typ eines Dialogs festzulegen +enum DialogTypeEnum { + /// Der Standarddialog + info, + + /// Der Errordialog + error, + + /// Der Erfolgreichdialog + success, +} diff --git a/lib/Pages/Dialog/dialog_action.dart b/lib/Pages/Dialog/dialog_action.dart new file mode 100644 index 0000000..e953dc6 --- /dev/null +++ b/lib/Pages/Dialog/dialog_action.dart @@ -0,0 +1,14 @@ +/// Action/Knopf für den Dialog +class DialogAction { + /// Erstellt eine neue Aktion für den Dialog + DialogAction({required this.label, this.isPrimary = false, this.onPressed}); + + /// Das Label der Aktion + final String label; + + /// Ob es die primäre Aktion ist + final bool isPrimary; + + /// Was bei einem Knopfdruck passieren soll + final void Function(Map inputValues)? onPressed; +} diff --git a/lib/Pages/Dialog/dialog_input_field.dart b/lib/Pages/Dialog/dialog_input_field.dart new file mode 100644 index 0000000..3e5813c --- /dev/null +++ b/lib/Pages/Dialog/dialog_input_field.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +/// Ein Input-Feld für den dynamischen Dialog +class DialogInputField { + /// Erstellt ein neues Input-Feld + const DialogInputField({ + required this.id, + this.label, + this.initialValue, + this.keyboardType = TextInputType.text, + this.obscureText = false, + this.autoFocus = false, + this.onChanged, + }); + + /// Die Id des InputFelds + final String id; + + /// Der Label des InputFelds + final String? label; + + /// Der initiale Wert des InputFelds + final String? initialValue; + + /// Der InputTyp des InputFeld + final TextInputType keyboardType; + + /// Ob der Text obskur angezeigt werden soll + final bool obscureText; + + /// Ob das Textfeld automatisch fokussiert werden soll + final bool autoFocus; + + /// Was bei Veränderung des Textfeldes geschehen soll + final ValueChanged? onChanged; +} diff --git a/lib/Pages/Dialog/dynamic_dialog.dart b/lib/Pages/Dialog/dynamic_dialog.dart new file mode 100644 index 0000000..1ecd0b2 --- /dev/null +++ b/lib/Pages/Dialog/dynamic_dialog.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; + +import '../../Entities/dialog_type_enum.dart'; +import '../../Services/theme_service.dart'; +import 'dialog_action.dart'; +import 'dialog_input_field.dart'; + +/// Erstellt einen neuen dynamischen Dialog +class DynamicDialog { + /// Erstellt eine neue Instanz dieser Klasse + DynamicDialog({ + this.title, + this.icon, + this.content, + final List? inputFields, + final List? actions, + this.backgroundColor, + this.borderRadius = 16, + this.barrierDismissible = true, + this.dialogType = DialogTypeEnum.info, + }) : inputFields = inputFields ?? const [], + actions = actions ?? [DialogAction(label: 'Schließen')]; + + /// Der Titel des Dialogs + final String? title; + + /// Der Icon des Dialogs + final IconData? icon; + + /// Der Inhalt des Dialogs + final Widget? content; + + /// Die Hintergrundfarbe des Dialogs, Standard wenn nicht gesetzt + final Color? backgroundColor; + + /// Der BorderRadius des Dialogs + final double borderRadius; + + /// Ob der Dialog bei einem Klick auf die Barriere geschlossen werden kann + final bool barrierDismissible; + + /// Die InputFelder des Dialogs + final List inputFields; + + /// Die Aktionen des Dialogs + final List actions; + + /// Der Typ des Dialogs + final DialogTypeEnum dialogType; + + Map? _controllers; + Map? _focusNodes; + + BuildContext? _dialogContext; + + void _prepareControllers() { + _controllers = { + for (final field in inputFields) + field.id: TextEditingController(text: field.initialValue ?? ''), + }; + } + + void _prepareFocusNodes() { + _focusNodes = {for (final field in inputFields) field.id: FocusNode()}; + } + + void _disposeControllers() { + for (final TextEditingController controller in _controllers!.values) { + controller.dispose(); + } + _controllers = null; + } + + void _disposeFocusNodes() { + for (final FocusNode node in _focusNodes!.values) { + node.dispose(); + } + _focusNodes = null; + } + + /// Zeigt den vorher zusammengebauten Dialog an + Future show(final BuildContext context) async { + final ThemeData theme = Theme.of(context); + + _prepareControllers(); + _prepareFocusNodes(); + + await showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (final BuildContext ctx) { + _dialogContext = ctx; + WidgetsBinding.instance.addPostFrameCallback((_) { + final DialogInputField? autoFocusField = inputFields + .where((final DialogInputField f) => f.autoFocus) + .cast() + .firstWhere( + (final DialogInputField? f) => f != null, + orElse: () => null, + ); + + if (autoFocusField != null) { + _focusNodes![autoFocusField.id]!.requestFocus(); + } + }); + + final DialogAction primaryAction = actions.firstWhere( + (final a) => a.isPrimary, + orElse: () => actions.first, + ); + + Color backgroundColor = + this.backgroundColor ?? theme.colorScheme.surface; + + if (dialogType == DialogTypeEnum.error) { + backgroundColor = theme.colorScheme.errorContainer; + } else if (dialogType == DialogTypeEnum.success) { + backgroundColor = ThemeService.getSuccessColor( + brightness: theme.brightness, + ); + } + + return AlertDialog( + backgroundColor: backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius), + ), + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) + Icon(icon, size: 48, color: theme.colorScheme.primary), + if (title != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + title!, + style: theme.textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ?content, + ...inputFields.map( + (final DialogInputField field) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: TextField( + controller: _controllers![field.id], + focusNode: _focusNodes![field.id], + keyboardType: field.keyboardType, + obscureText: field.obscureText, + onChanged: field.onChanged, + decoration: InputDecoration( + labelText: field.label, + border: const OutlineInputBorder(), + isDense: true, + ), + onSubmitted: (_) { + final Map values = { + for (final entry in _controllers!.entries) + entry.key: entry.value.text, + }; + + close(); + primaryAction.onPressed?.call(values); + }, + ), + ), + ), + ], + ), + actions: actions + .map( + (final action) => TextButton( + onPressed: () { + final Map values = { + for (final entry in _controllers!.entries) + entry.key: entry.value.text, + }; + + close(); + action.onPressed?.call(values); + }, + child: Text(action.label), + ), + ) + .toList(), + ); + }, + ); + } + + /// Schließt den dynamischen Dialog + void close() { + if (_dialogContext != null) { + Navigator.of(_dialogContext!).pop(); + } + + _disposeControllers(); + _disposeFocusNodes(); + } +}