import 'dart:math'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:obtainium/components/generated_form_modal.dart'; abstract class GeneratedFormItem { late String key; late String label; late List belowWidgets; late dynamic defaultValue; List additionalValidators; dynamic ensureType(dynamic val); GeneratedFormItem(this.key, {this.label = 'Input', this.belowWidgets = const [], this.defaultValue, this.additionalValidators = const []}); } class GeneratedFormTextField extends GeneratedFormItem { late bool required; late int max; late String? hint; late bool password; GeneratedFormTextField(String key, {String label = 'Input', List belowWidgets = const [], String defaultValue = '', List additionalValidators = const [], this.required = true, this.max = 1, this.hint, this.password = false}) : super(key, label: label, belowWidgets: belowWidgets, defaultValue: defaultValue, additionalValidators: additionalValidators); @override String ensureType(val) { return val.toString(); } } class GeneratedFormDropdown extends GeneratedFormItem { late List>? opts; List? disabledOptKeys; GeneratedFormDropdown( String key, this.opts, { String label = 'Input', List belowWidgets = const [], String defaultValue = '', this.disabledOptKeys, List additionalValidators = const [], }) : super(key, label: label, belowWidgets: belowWidgets, defaultValue: defaultValue, additionalValidators: additionalValidators); @override String ensureType(val) { return val.toString(); } } class GeneratedFormSwitch extends GeneratedFormItem { GeneratedFormSwitch( String key, { String label = 'Input', List belowWidgets = const [], bool defaultValue = false, List additionalValidators = const [], }) : super(key, label: label, belowWidgets: belowWidgets, defaultValue: defaultValue, additionalValidators: additionalValidators); @override bool ensureType(val) { return val == true || val == 'true'; } } class GeneratedFormTagInput extends GeneratedFormItem { late MapEntry? deleteConfirmationMessage; late bool singleSelect; late WrapAlignment alignment; late String emptyMessage; late bool showLabelWhenNotEmpty; GeneratedFormTagInput(String key, {String label = 'Input', List belowWidgets = const [], Map> defaultValue = const {}, List> value)> additionalValidators = const [], this.deleteConfirmationMessage, this.singleSelect = false, this.alignment = WrapAlignment.start, this.emptyMessage = 'Input', this.showLabelWhenNotEmpty = true}) : super(key, label: label, belowWidgets: belowWidgets, defaultValue: defaultValue, additionalValidators: additionalValidators); @override Map> ensureType(val) { return val is Map> ? val : {}; } } typedef OnValueChanges = void Function( Map values, bool valid, bool isBuilding); class GeneratedForm extends StatefulWidget { const GeneratedForm( {super.key, required this.items, required this.onValueChanges}); final List> items; final OnValueChanges onValueChanges; @override State createState() => _GeneratedFormState(); } // Generates a random light color // Courtesy of ChatGPT 😭 (with a bugfix 🥳) Color generateRandomLightColor() { // Create a random number generator final Random random = Random(); // Generate random hue, saturation, and value values final double hue = random.nextDouble() * 360; final double saturation = 0.5 + random.nextDouble() * 0.5; final double value = 0.9 + random.nextDouble() * 0.1; // Create a HSV color with the random values return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor(); } class _GeneratedFormState extends State { final _formKey = GlobalKey(); Map values = {}; late List> formInputs; List> rows = []; String? initKey; // If any value changes, call this to update the parent with value and validity void someValueChanged({bool isBuilding = false}) { Map returnValues = values; var valid = true; for (int r = 0; r < widget.items.length; r++) { for (int i = 0; i < widget.items[r].length; i++) { if (formInputs[r][i] is TextFormField) { var fieldState = (formInputs[r][i].key as GlobalKey).currentState; if (fieldState != null) { valid = valid && fieldState.isValid; } } } } widget.onValueChanges(returnValues, valid, isBuilding); } initForm() { initKey = widget.key.toString(); // Initialize form values as all empty values.clear(); for (var row in widget.items) { for (var e in row) { values[e.key] = e.defaultValue; } } // Dynamically create form inputs formInputs = widget.items.asMap().entries.map((row) { return row.value.asMap().entries.map((e) { var formItem = e.value; if (formItem is GeneratedFormTextField) { final formFieldKey = GlobalKey(); return TextFormField( obscureText: formItem.password, autocorrect: !formItem.password, enableSuggestions: !formItem.password, key: formFieldKey, initialValue: values[formItem.key], autovalidateMode: AutovalidateMode.onUserInteraction, onChanged: (value) { setState(() { values[formItem.key] = value; someValueChanged(); }); }, decoration: InputDecoration( helperText: formItem.label + (formItem.required ? ' *' : ''), hintText: formItem.hint), minLines: formItem.max <= 1 ? null : formItem.max, maxLines: formItem.max <= 1 ? 1 : formItem.max, validator: (value) { if (formItem.required && (value == null || value.trim().isEmpty)) { return '${formItem.label} ${tr('requiredInBrackets')}'; } for (var validator in formItem.additionalValidators) { String? result = validator(value); if (result != null) { return result; } } return null; }, ); } else if (formItem is GeneratedFormDropdown) { if (formItem.opts!.isEmpty) { return Text(tr('dropdownNoOptsError')); } return DropdownButtonFormField( decoration: InputDecoration(labelText: formItem.label), value: values[formItem.key], items: formItem.opts!.map((e2) { var enabled = formItem.disabledOptKeys?.contains(e2.key) != true; return DropdownMenuItem( value: e2.key, enabled: enabled, child: Opacity( opacity: enabled ? 1 : 0.5, child: Text(e2.value))); }).toList(), onChanged: (value) { setState(() { values[formItem.key] = value ?? formItem.opts!.first.key; someValueChanged(); }); }); } else { return Container(); // Some input types added in build } }).toList(); }).toList(); someValueChanged(isBuilding: true); } @override void initState() { super.initState(); initForm(); } @override Widget build(BuildContext context) { if (widget.key.toString() != initKey) { initForm(); } for (var r = 0; r < formInputs.length; r++) { for (var e = 0; e < formInputs[r].length; e++) { if (widget.items[r][e] is GeneratedFormSwitch) { formInputs[r][e] = Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(widget.items[r][e].label), Switch( value: values[widget.items[r][e].key], onChanged: (value) { setState(() { values[widget.items[r][e].key] = value; someValueChanged(); }); }) ], ); } else if (widget.items[r][e] is GeneratedFormTagInput) { formInputs[r][e] = Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if ((values[widget.items[r][e].key] as Map>?) ?.isNotEmpty == true && (widget.items[r][e] as GeneratedFormTagInput) .showLabelWhenNotEmpty) Column( crossAxisAlignment: (widget.items[r][e] as GeneratedFormTagInput).alignment == WrapAlignment.center ? CrossAxisAlignment.center : CrossAxisAlignment.stretch, children: [ Text(widget.items[r][e].label), const SizedBox( height: 8, ), ], ), Wrap( alignment: (widget.items[r][e] as GeneratedFormTagInput).alignment, crossAxisAlignment: WrapCrossAlignment.center, children: [ (values[widget.items[r][e].key] as Map>?) ?.isEmpty == true ? Text( (widget.items[r][e] as GeneratedFormTagInput) .emptyMessage, ) : const SizedBox.shrink(), ...(values[widget.items[r][e].key] as Map>?) ?.entries .map((e2) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ChoiceChip( label: Text(e2.key), backgroundColor: Color(e2.value.key).withAlpha(50), selectedColor: Color(e2.value.key), visualDensity: VisualDensity.compact, selected: e2.value.value, onSelected: (value) { setState(() { (values[widget.items[r][e].key] as Map>)[e2.key] = MapEntry( (values[widget.items[r][e].key] as Map< String, MapEntry>)[e2.key]! .key, value); if ((widget.items[r][e] as GeneratedFormTagInput) .singleSelect && value == true) { for (var key in (values[ widget.items[r][e].key] as Map>) .keys) { if (key != e2.key) { (values[widget.items[r][e].key] as Map< String, MapEntry>)[key] = MapEntry( (values[widget.items[r][e].key] as Map< String, MapEntry>)[key]! .key, false); } } } someValueChanged(); }); }, )); }) ?? [const SizedBox.shrink()], (values[widget.items[r][e].key] as Map>?) ?.values .where((e) => e.value) .isNotEmpty == true ? Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: IconButton( onPressed: () { fn() { setState(() { var temp = values[widget.items[r][e].key] as Map>; temp.removeWhere((key, value) => value.value); values[widget.items[r][e].key] = temp; someValueChanged(); }); } if ((widget.items[r][e] as GeneratedFormTagInput) .deleteConfirmationMessage != null) { var message = (widget.items[r][e] as GeneratedFormTagInput) .deleteConfirmationMessage!; showDialog?>( context: context, builder: (BuildContext ctx) { return GeneratedFormModal( title: message.key, message: message.value, items: const []); }).then((value) { if (value != null) { fn(); } }); } else { fn(); } }, icon: const Icon(Icons.remove), visualDensity: VisualDensity.compact, tooltip: tr('remove'), )) : const SizedBox.shrink(), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: IconButton( onPressed: () { showDialog?>( context: context, builder: (BuildContext ctx) { return GeneratedFormModal( title: widget.items[r][e].label, items: [ [ GeneratedFormTextField('label', label: tr('label')) ] ]); }).then((value) { String? label = value?['label']; if (label != null) { setState(() { var temp = values[widget.items[r][e].key] as Map>?; temp ??= {}; if (temp[label] == null) { var singleSelect = (widget.items[r][e] as GeneratedFormTagInput) .singleSelect; var someSelected = temp.entries .where((element) => element.value.value) .isNotEmpty; temp[label] = MapEntry( generateRandomLightColor().value, !(someSelected && singleSelect)); values[widget.items[r][e].key] = temp; someValueChanged(); } }); } }); }, icon: const Icon(Icons.add), visualDensity: VisualDensity.compact, tooltip: tr('add'), )), ], ) ]); } } } rows.clear(); formInputs.asMap().entries.forEach((rowInputs) { if (rowInputs.key > 0) { rows.add([ SizedBox( height: widget.items[rowInputs.key - 1][0] is GeneratedFormSwitch ? 8 : 25, ) ]); } List rowItems = []; rowInputs.value.asMap().entries.forEach((rowInput) { if (rowInput.key > 0) { rowItems.add(const SizedBox( width: 20, )); } rowItems.add(Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ rowInput.value, ...widget.items[rowInputs.key][rowInput.key].belowWidgets ]))); }); rows.add(rowItems); }); return Form( key: _formKey, child: Column( children: [ ...rows.map((row) => Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [...row.map((e) => e)], )) ], )); } }