import 'package:flutter/material.dart'; import '../../core/contracts/script_widget.dart'; import '../../shared/theme/app_theme.dart'; import '../../shared/widgets/setting_select.dart'; class ScriptWidgetParameterForm extends StatefulWidget { final List parameters; final Map initialValues; final ValueChanged> onChanged; final bool hidePagingParameters; const ScriptWidgetParameterForm({ super.key, required this.parameters, required this.initialValues, required this.onChanged, this.hidePagingParameters = false, }); @override State createState() => _ScriptWidgetParameterFormState(); } class _ScriptWidgetParameterFormState extends State { final Map _textControllers = {}; late Map _values; @override void initState() { super.initState(); _values = _buildInitialValues(); _synchronizeTextControllers(); } @override void didUpdateWidget(covariant ScriptWidgetParameterForm oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.parameters != widget.parameters || oldWidget.initialValues != widget.initialValues) { _values = _buildInitialValues(); _synchronizeTextControllers(); } } @override void dispose() { for (final controller in _textControllers.values) { controller.dispose(); } super.dispose(); } Map _buildInitialValues() { final defaultValues = buildScriptWidgetParameterDefaults(widget.parameters); return { for (final parameter in widget.parameters) parameter.name: widget.initialValues[parameter.name] ?? defaultValues[parameter.name], }; } void _synchronizeTextControllers() { final activeNames = widget.parameters .where((parameter) => !_usesOptionSelector(parameter)) .map((parameter) => parameter.name) .toSet(); final staleNames = _textControllers.keys .where((name) => !activeNames.contains(name)) .toList(growable: false); for (final name in staleNames) { _textControllers.remove(name)?.dispose(); } for (final parameter in widget.parameters) { if (_usesOptionSelector(parameter)) continue; final value = _values[parameter.name]?.toString() ?? ''; final controller = _textControllers.putIfAbsent( parameter.name, TextEditingController.new, ); if (controller.text != value) { controller.value = TextEditingValue( text: value, selection: TextSelection.collapsed(offset: value.length), ); } } } bool _usesOptionSelector(ScriptWidgetParam parameter) { return parameter.type == 'enumeration' || parameter.enumOptions.isNotEmpty || parameter.placeholders.isNotEmpty; } bool _isVisible(ScriptWidgetParam parameter) { if (parameter.type == 'constant') return false; if (widget.hidePagingParameters && const {'page', 'offset'}.contains(parameter.type)) { return false; } final condition = parameter.belongTo; if (condition == null || condition.paramName.isEmpty) return true; final parentValue = _values[condition.paramName]?.toString() ?? ''; return condition.value.contains(parentValue); } void _updateValue(String name, Object? value) { setState(() { _values[name] = value; }); widget.onChanged(Map.unmodifiable(_values)); } @override Widget build(BuildContext context) { final visibleParameters = widget.parameters .where(_isVisible) .toList(growable: false); if (visibleParameters.isEmpty) return const SizedBox.shrink(); return Wrap( spacing: 12, runSpacing: 12, children: [ for (final parameter in visibleParameters) SizedBox( width: _fieldWidth(parameter), child: _buildField(parameter), ), ], ); } double _fieldWidth(ScriptWidgetParam parameter) { if (const {'count', 'page', 'offset'}.contains(parameter.type)) { return 132; } return 240; } Widget _buildField(ScriptWidgetParam parameter) { final options = parameter.enumOptions.isNotEmpty ? parameter.enumOptions : parameter.placeholders; if (_usesOptionSelector(parameter)) { final selectedValue = _values[parameter.name]?.toString() ?? ''; final optionValues = options.map((option) => option.value).toSet(); final effectiveValue = optionValues.contains(selectedValue) ? selectedValue : null; final labelsByValue = { for (final option in options) option.value: option.title.isEmpty ? option.value : option.title, }; final theme = Theme.of(context); final tokens = context.appTokens; final helper = _helperFor(parameter); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _labelFor(parameter), style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), SettingSelect( value: effectiveValue, options: options .map((option) => option.value) .toList(growable: false), labelOf: (value) => labelsByValue[value] ?? value, onChanged: (value) => _updateValue(parameter.name, value ?? ''), ), if (helper != null) ...[ const SizedBox(height: 4), Text( helper, style: theme.textTheme.bodySmall?.copyWith( color: tokens.mutedForeground, ), ), ], ], ); } final isNumeric = const { 'count', 'page', 'offset', }.contains(parameter.type); final theme = Theme.of(context); final tokens = context.appTokens; final inputBorder = OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: tokens.mutedForeground.withValues(alpha: 0.28), ), ); return TextField( controller: _textControllers[parameter.name], keyboardType: isNumeric ? TextInputType.number : TextInputType.text, decoration: InputDecoration( labelText: _labelFor(parameter), helperText: _helperFor(parameter), filled: true, fillColor: theme.colorScheme.surface.withValues(alpha: 0.42), border: inputBorder, enabledBorder: inputBorder, focusedBorder: inputBorder.copyWith( borderSide: BorderSide(color: theme.colorScheme.primary), ), isDense: true, ), onChanged: (value) { final parsedValue = isNumeric ? int.tryParse(value) ?? value : value; _updateValue(parameter.name, parsedValue); }, ); } String _labelFor(ScriptWidgetParam parameter) { return parameter.title.isEmpty ? parameter.name : parameter.title; } String? _helperFor(ScriptWidgetParam parameter) { final description = parameter.description.trim(); return description.isEmpty ? null : description; } }