Feat: Fügt einen dynamisch erstell- und anzeigbaren Dialog hinzu

This commit is contained in:
2025-12-23 01:00:23 +01:00
parent 016ba85416
commit 246c0401cc
4 changed files with 267 additions and 0 deletions

View File

@@ -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<String, String> inputValues)? onPressed;
}

View File

@@ -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<String>? onChanged;
}

View File

@@ -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<DialogInputField>? inputFields,
final List<DialogAction>? 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<DialogInputField> inputFields;
/// Die Aktionen des Dialogs
final List<DialogAction> actions;
/// Der Typ des Dialogs
final DialogTypeEnum dialogType;
Map<String, TextEditingController>? _controllers;
Map<String, FocusNode>? _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<void> 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<DialogInputField?>()
.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<String, String> 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<String, String> 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();
}
}