Files
sm-emby-share/lib/features/modules/script_widget_parameter_form.dart
T
2026-07-14 11:11:36 +08:00

237 lines
7.3 KiB
Dart

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<ScriptWidgetParam> parameters;
final Map<String, Object?> initialValues;
final ValueChanged<Map<String, Object?>> onChanged;
final bool hidePagingParameters;
const ScriptWidgetParameterForm({
super.key,
required this.parameters,
required this.initialValues,
required this.onChanged,
this.hidePagingParameters = false,
});
@override
State<ScriptWidgetParameterForm> createState() =>
_ScriptWidgetParameterFormState();
}
class _ScriptWidgetParameterFormState extends State<ScriptWidgetParameterForm> {
final Map<String, TextEditingController> _textControllers =
<String, TextEditingController>{};
late Map<String, Object?> _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<String, Object?> _buildInitialValues() {
final defaultValues = buildScriptWidgetParameterDefaults(widget.parameters);
return <String, Object?>{
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 <String>{'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<String, Object?>.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 <String>{'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 = <String, String>{
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<String>(
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 <String>{
'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;
}
}