library; import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/clash_config.dart'; import 'package:fl_clash/models/state.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; final ruleItemHeight = globalState.measure.bodyLargeHeight + globalState.measure.bodyMediumHeight + 12; class RuleItem extends StatelessWidget { final bool isSelected; final bool isEditing; final Rule rule; final bool hasMatch; final void Function() onSelected; final void Function(Rule rule) onEdit; final bool Function(Rule rule)? checkInvalidHandler; const RuleItem({ super.key, required this.isSelected, required this.rule, required this.onSelected, required this.onEdit, this.checkInvalidHandler, this.isEditing = false, this.hasMatch = false, }); VM2 _checkInvalid(BuildContext context) { if (rule.ruleAction != RuleAction.SUB_RULE) { final ruleTarget = rule.ruleTarget ?? ''; if (ruleTarget.toUpperCase() == 'DIRECT') { return VM2( false, Colors.green.harmonizeWith(context.colorScheme.primary), ); } else if (ruleTarget.toUpperCase() == 'REJECT') { return VM2( false, Colors.orange.harmonizeWith(context.colorScheme.primary), ); } else if (hasMatch && ruleTarget.toUpperCase() == 'MATCH') { return VM2(false, context.colorScheme.tertiary); } } bool invalid = true; if (checkInvalidHandler != null) { invalid = checkInvalidHandler!(rule); } return VM2( invalid, invalid ? context.colorScheme.error : context.colorScheme.tertiary, ); } Widget _buildInfoWidget(BuildContext context) { return CommonMinIconButtonTheme( child: IconButton( onPressed: () { globalState.showMessage( message: TextSpan( text: rule.targetErrorTip( context.appLocalizations.invalidSubRule(rule.subRule ?? ''), context.appLocalizations.invalidPolicy(rule.ruleTarget ?? ''), ), ), ); }, icon: Icon(Icons.info, size: 16.ap, color: context.colorScheme.error), ), ); } @override Widget build(BuildContext context) { final vm2 = _checkInvalid(context); final invalid = vm2.a; return SelectedDecorationListItem( minVerticalPadding: 0, isSelected: isSelected, isEditing: isEditing, horizontalTitleGap: 0, invalid: invalid, onSelected: () { onSelected(); }, title: Center( child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Builder( builder: (context) { final style = DefaultTextStyle.of( context, ).style.toJetBrainsMono; return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( rule.ruleAction.name, style: style.copyWith( fontSize: context.textTheme.bodyLarge?.fontSize, ), ), Flexible( child: Builder( builder: (context) { return TooltipText( text: Text( rule.realContent ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: style.copyWith( fontSize: context.textTheme.bodyMedium?.fontSize, color: style.color?.opacity60, ), ), ); }, ), ), ], ); }, ), ), Row( mainAxisSize: MainAxisSize.min, children: [ if (invalid) _buildInfoWidget(context), if (rule.realTarget != null) Text( rule.realTarget!, style: context.textTheme.bodyMedium?.toJetBrainsMono .copyWith(color: vm2.b), ), ], ), ], ), ), onPressed: () { onEdit(rule); }, ); } } class RuleStatusItem extends StatelessWidget { final bool status; final Rule rule; final void Function(bool) onChange; const RuleStatusItem({ super.key, required this.status, required this.rule, required this.onChange, }); @override Widget build(BuildContext context) { return DecorationListItem( title: TooltipText( text: Text( rule.rawValue, maxLines: 2, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.toJetBrainsMono, ), ), trailing: Switch(value: status, onChanged: onChange), onPressed: () { onChange(!status); }, ); } } class AddOrEditRuleDialog extends StatefulWidget { final Rule? rule; const AddOrEditRuleDialog({super.key, this.rule}); @override State createState() => _AddOrEditRuleDialogState(); } class _AddOrEditRuleDialogState extends State { late RuleAction _ruleAction; final _ruleTargetController = TextEditingController(); final _contentController = TextEditingController(); bool _noResolve = false; bool _src = false; List _targetItems = []; final _formKey = GlobalKey(); @override void initState() { _initState(); super.initState(); } void _initState() { _targetItems = [ ...RuleTarget.values.map( (item) => DropdownMenuEntry(value: item.name, label: item.name), ), const DropdownMenuEntry(value: 'MATCH', label: 'MATCH'), ]; final rule = widget.rule; if (rule != null) { _ruleAction = rule.ruleAction; _contentController.text = rule.content ?? ''; _ruleTargetController.text = rule.ruleTarget ?? ''; _noResolve = rule.noResolve; _src = rule.src; return; } _ruleAction = RuleAction.addedRuleActions.first; if (_targetItems.isNotEmpty) { _ruleTargetController.text = _targetItems.first.value; } } @override void didUpdateWidget(AddOrEditRuleDialog oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.rule != widget.rule) { _initState(); } } void _handleSubmit() { final res = _formKey.currentState?.validate(); if (res == false) { return; } final rule = Rule( id: widget.rule?.id ?? snowflake.id, ruleAction: _ruleAction, content: _contentController.text, ruleTarget: _ruleTargetController.text, noResolve: _noResolve, src: _src, ); Navigator.of(context).pop(rule); } @override Widget build(BuildContext context) { final appLocalizations = context.appLocalizations; return CommonDialog( title: widget.rule != null ? appLocalizations.editRule : appLocalizations.addRule, actions: [ TextButton( onPressed: _handleSubmit, child: Text(appLocalizations.confirm), ), ], child: DropdownMenuTheme( data: DropdownMenuThemeData( inputDecorationTheme: InputDecorationTheme( border: const OutlineInputBorder(), labelStyle: context.textTheme.bodyLarge?.copyWith( overflow: TextOverflow.ellipsis, ), ), ), child: Form( key: _formKey, child: LayoutBuilder( builder: (_, constraints) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FilledButton.tonal( onPressed: () async { _ruleAction = await globalState.showCommonDialog( filter: false, child: OptionsDialog( title: appLocalizations.ruleName, options: RuleAction.addedRuleActions, textBuilder: (item) => item.value, value: _ruleAction, ), ) ?? _ruleAction; setState(() {}); }, child: Text(_ruleAction.value), ), const SizedBox(height: 24), TextFormField( keyboardType: TextInputType.text, inputFormatters: TextInputLimits.limit( TextInputLimits.rule, ), onFieldSubmitted: (_) { _handleSubmit(); }, controller: _contentController, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: appLocalizations.content, ), validator: (_) { if (_contentController.text.isEmpty) { return appLocalizations.emptyTip( appLocalizations.content, ); } return null; }, ), const SizedBox(height: 24), FormField( validator: (_) { if (_ruleTargetController.text.isEmpty) { return appLocalizations.emptyTip( appLocalizations.ruleTarget, ); } return null; }, builder: (filed) { return DropdownMenu( controller: _ruleTargetController, label: Text(appLocalizations.ruleTarget), width: 200, menuHeight: 250, enableFilter: false, enableSearch: false, dropdownMenuEntries: _targetItems, errorText: filed.errorText, ); }, ), if (_ruleAction.hasParams) ...[ const SizedBox(height: 20), Wrap( spacing: 8, children: [ CommonCard( radius: 8, isSelected: _src, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 8, ), child: Text( appLocalizations.sourceIp, style: context.textTheme.bodyMedium, ), ), onPressed: () { setState(() { _src = !_src; }); }, ), CommonCard( radius: 8, isSelected: _noResolve, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 8, ), child: Text( appLocalizations.noResolve, style: context.textTheme.bodyMedium, ), ), onPressed: () { setState(() { _noResolve = !_noResolve; }); }, ), ], ), ], const SizedBox(height: 20), ], ); }, ), ), ), ); } }