2025-10-14 15:13:52 +08:00
|
|
|
library;
|
|
|
|
|
|
|
|
|
|
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/state.dart';
|
2026-10-16 14:55:07 +08:00
|
|
|
import 'package:fl_clash/ui/icons/icons.dart';
|
2026-02-02 10:14:13 +08:00
|
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
2026-09-05 20:09:44 +08:00
|
|
|
import 'package:material_ui/material_ui.dart';
|
2025-10-14 15:13:52 +08:00
|
|
|
|
2026-10-16 14:55:07 +08:00
|
|
|
import 'form_page.dart';
|
|
|
|
|
import 'form_row.dart';
|
|
|
|
|
|
2026-10-08 17:58:43 +08:00
|
|
|
const _ruleRowVerticalPadding = 7.0;
|
|
|
|
|
|
|
|
|
|
/// Rule lists run to thousands of rows, so a rule row is padded tighter than
|
|
|
|
|
/// other list rows.
|
|
|
|
|
double get ruleItemHeight =>
|
|
|
|
|
globalState.measure.listTitleHeight +
|
|
|
|
|
globalState.measure.listSubtitleHeight +
|
|
|
|
|
2 * _ruleRowVerticalPadding;
|
2026-02-02 10:14:13 +08:00
|
|
|
|
2025-10-14 15:13:52 +08:00
|
|
|
class RuleItem extends StatelessWidget {
|
2026-09-05 20:09:44 +08:00
|
|
|
static const _targetMaxWidthFactor = 0.5;
|
2026-10-08 17:58:43 +08:00
|
|
|
static const _targetMinWidthFactor = 0.3;
|
|
|
|
|
static const _labelTargetGap = 16.0;
|
2026-09-05 20:09:44 +08:00
|
|
|
|
2025-10-14 15:13:52 +08:00
|
|
|
final bool isSelected;
|
|
|
|
|
final bool isEditing;
|
|
|
|
|
final Rule rule;
|
2026-02-02 10:14:13 +08:00
|
|
|
final bool hasMatch;
|
2025-12-16 11:23:09 +08:00
|
|
|
final void Function() onSelected;
|
2025-10-14 15:13:52 +08:00
|
|
|
final void Function(Rule rule) onEdit;
|
2026-09-25 03:56:59 +08:00
|
|
|
final String? Function(Rule rule)? invalidMessageOf;
|
2025-10-14 15:13:52 +08:00
|
|
|
|
|
|
|
|
const RuleItem({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.isSelected,
|
|
|
|
|
required this.rule,
|
|
|
|
|
required this.onSelected,
|
|
|
|
|
required this.onEdit,
|
2026-09-25 03:56:59 +08:00
|
|
|
this.invalidMessageOf,
|
2025-10-14 15:13:52 +08:00
|
|
|
this.isEditing = false,
|
2026-02-02 10:14:13 +08:00
|
|
|
this.hasMatch = false,
|
2025-10-14 15:13:52 +08:00
|
|
|
});
|
|
|
|
|
|
2026-09-25 03:56:59 +08:00
|
|
|
({bool invalid, Color? color, String? message}) _checkInvalid(
|
|
|
|
|
BuildContext context,
|
|
|
|
|
) {
|
|
|
|
|
final message = invalidMessageOf?.call(rule);
|
|
|
|
|
if (message == null && rule.ruleAction != RuleAction.SUB_RULE) {
|
|
|
|
|
final ruleTarget = (rule.ruleTarget ?? '').toUpperCase();
|
|
|
|
|
final color = switch (ruleTarget) {
|
2026-10-08 17:58:43 +08:00
|
|
|
'DIRECT' || 'COMPATIBLE' => context.colorScheme.success,
|
2026-09-25 03:56:59 +08:00
|
|
|
'REJECT' || 'REJECT-DROP' => context.colorScheme.warning,
|
2026-10-08 17:58:43 +08:00
|
|
|
'PASS' || 'PASS-RULE' || 'GLOBAL' => context.colorScheme.tertiary,
|
2026-09-25 03:56:59 +08:00
|
|
|
'MATCH' when hasMatch => context.colorScheme.tertiary,
|
|
|
|
|
_ => null,
|
|
|
|
|
};
|
|
|
|
|
if (color != null) {
|
|
|
|
|
return (invalid: false, color: color, message: null);
|
2026-02-02 10:14:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-25 03:56:59 +08:00
|
|
|
final invalid = invalidMessageOf == null || message != null;
|
2026-09-05 20:09:44 +08:00
|
|
|
return (
|
|
|
|
|
invalid: invalid,
|
|
|
|
|
color: invalid ? context.colorScheme.error : context.colorScheme.tertiary,
|
2026-09-25 03:56:59 +08:00
|
|
|
message: message,
|
2026-02-02 10:14:13 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 03:56:59 +08:00
|
|
|
Widget _buildInfoWidget(BuildContext context, String? message) {
|
2026-02-02 10:14:13 +08:00
|
|
|
return CommonMinIconButtonTheme(
|
|
|
|
|
child: IconButton(
|
2026-09-05 20:09:44 +08:00
|
|
|
tooltip: context.appLocalizations.tip,
|
2026-02-02 10:14:13 +08:00
|
|
|
onPressed: () {
|
2026-09-05 20:09:44 +08:00
|
|
|
dialogs.showMessage(
|
2026-02-02 10:14:13 +08:00
|
|
|
message: TextSpan(
|
2026-09-25 03:56:59 +08:00
|
|
|
text:
|
|
|
|
|
message ??
|
|
|
|
|
rule.targetErrorTip(
|
|
|
|
|
context.appLocalizations.invalidSubRule(rule.subRule ?? ''),
|
|
|
|
|
context.appLocalizations.invalidPolicy(
|
|
|
|
|
rule.ruleTarget ?? '',
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-02-02 10:14:13 +08:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
2026-09-25 03:56:59 +08:00
|
|
|
icon: GlyphIcon(
|
|
|
|
|
AppGlyphs.info,
|
|
|
|
|
fill: 1,
|
2026-10-16 14:55:07 +08:00
|
|
|
size: 16.ap(context),
|
2026-09-25 03:56:59 +08:00
|
|
|
color: context.colorScheme.error,
|
|
|
|
|
),
|
2026-02-02 10:14:13 +08:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 15:13:52 +08:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-09-05 20:09:44 +08:00
|
|
|
final checkResult = _checkInvalid(context);
|
|
|
|
|
final invalid = checkResult.invalid;
|
2026-02-02 10:14:13 +08:00
|
|
|
return SelectedDecorationListItem(
|
|
|
|
|
minVerticalPadding: 0,
|
2025-10-14 15:13:52 +08:00
|
|
|
isSelected: isSelected,
|
2026-02-02 10:14:13 +08:00
|
|
|
isEditing: isEditing,
|
|
|
|
|
horizontalTitleGap: 0,
|
|
|
|
|
invalid: invalid,
|
2025-10-14 15:13:52 +08:00
|
|
|
onSelected: () {
|
2025-12-16 11:23:09 +08:00
|
|
|
onSelected();
|
2025-10-14 15:13:52 +08:00
|
|
|
},
|
2026-02-02 10:14:13 +08:00
|
|
|
title: Center(
|
2026-09-05 20:09:44 +08:00
|
|
|
child: LayoutBuilder(
|
|
|
|
|
builder: (context, constraints) {
|
2026-10-08 17:58:43 +08:00
|
|
|
final labelStyle = DefaultTextStyle.of(
|
|
|
|
|
context,
|
|
|
|
|
).style.toJetBrainsMono;
|
|
|
|
|
final actionWidth = globalState.measure
|
|
|
|
|
.computeTextSize(Text(rule.ruleAction.name, style: labelStyle))
|
|
|
|
|
.width;
|
|
|
|
|
final maxWidth = constraints.maxWidth;
|
|
|
|
|
final targetMaxWidth = (maxWidth - _labelTargetGap - actionWidth)
|
|
|
|
|
.clamp(
|
|
|
|
|
maxWidth * _targetMinWidthFactor,
|
|
|
|
|
maxWidth * _targetMaxWidthFactor,
|
|
|
|
|
);
|
2026-09-05 20:09:44 +08:00
|
|
|
return Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.max,
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
2026-10-08 17:58:43 +08:00
|
|
|
Expanded(
|
|
|
|
|
child: _RuleItemLabel(rule: rule, style: labelStyle),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: _labelTargetGap),
|
2026-09-05 20:09:44 +08:00
|
|
|
ConstrainedBox(
|
2026-10-08 17:58:43 +08:00
|
|
|
constraints: BoxConstraints(maxWidth: targetMaxWidth),
|
2026-09-05 20:09:44 +08:00
|
|
|
child: Row(
|
2026-02-02 10:14:13 +08:00
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
2026-09-25 03:56:59 +08:00
|
|
|
if (invalid)
|
|
|
|
|
_buildInfoWidget(context, checkResult.message),
|
2026-09-05 20:09:44 +08:00
|
|
|
if (rule.realTarget != null)
|
|
|
|
|
Flexible(
|
|
|
|
|
child: TooltipText(
|
|
|
|
|
text: Text(
|
|
|
|
|
hasMatch &&
|
|
|
|
|
rule.realTarget!.toUpperCase() ==
|
|
|
|
|
RuleAction.MATCH.value
|
|
|
|
|
? context.appLocalizations.matchTarget
|
|
|
|
|
: rule.realTarget!,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-10-16 14:55:07 +08:00
|
|
|
style: AppTextStyle.subheadline
|
|
|
|
|
.of(context)
|
|
|
|
|
.toJetBrainsMono
|
2026-09-05 20:09:44 +08:00
|
|
|
.copyWith(color: checkResult.color),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-02-02 10:14:13 +08:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-09-05 20:09:44 +08:00
|
|
|
),
|
2026-02-02 10:14:13 +08:00
|
|
|
],
|
2026-09-05 20:09:44 +08:00
|
|
|
);
|
|
|
|
|
},
|
2026-02-02 10:14:13 +08:00
|
|
|
),
|
2025-10-14 15:13:52 +08:00
|
|
|
),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
onEdit(rule);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:44 +08:00
|
|
|
class _RuleItemLabel extends StatelessWidget {
|
2026-10-08 17:58:43 +08:00
|
|
|
const _RuleItemLabel({required this.rule, required this.style});
|
2026-09-05 20:09:44 +08:00
|
|
|
|
|
|
|
|
final Rule rule;
|
2026-10-08 17:58:43 +08:00
|
|
|
final TextStyle style;
|
2026-09-05 20:09:44 +08:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-10-08 17:58:43 +08:00
|
|
|
final content = rule.realContent ?? '';
|
2026-09-05 20:09:44 +08:00
|
|
|
return Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
rule.ruleAction.name,
|
2026-10-08 17:58:43 +08:00
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
style: style,
|
2026-09-05 20:09:44 +08:00
|
|
|
),
|
2026-10-08 17:58:43 +08:00
|
|
|
if (content.isNotEmpty)
|
|
|
|
|
Flexible(
|
|
|
|
|
child: TooltipText(
|
|
|
|
|
text: Text(
|
|
|
|
|
content,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-10-16 14:55:07 +08:00
|
|
|
style: context.listSubtitleStyle.toJetBrainsMono.copyWith(
|
2026-10-08 17:58:43 +08:00
|
|
|
color: style.color?.opacity60,
|
|
|
|
|
),
|
2026-09-05 20:09:44 +08:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 15:13:52 +08:00
|
|
|
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) {
|
2026-02-02 10:14:13 +08:00
|
|
|
return DecorationListItem(
|
|
|
|
|
title: TooltipText(
|
|
|
|
|
text: Text(
|
|
|
|
|
rule.rawValue,
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-10-16 14:55:07 +08:00
|
|
|
style: AppTextStyle.subheadline.of(context).toJetBrainsMono,
|
2025-10-14 15:13:52 +08:00
|
|
|
),
|
|
|
|
|
),
|
2026-02-02 10:14:13 +08:00
|
|
|
trailing: Switch(value: status, onChanged: onChange),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
onChange(!status);
|
|
|
|
|
},
|
2025-10-14 15:13:52 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 14:55:07 +08:00
|
|
|
enum _RuleField { content, target }
|
|
|
|
|
|
|
|
|
|
class RuleForm extends StatefulWidget {
|
2025-10-14 15:13:52 +08:00
|
|
|
final Rule? rule;
|
|
|
|
|
|
2026-10-16 14:55:07 +08:00
|
|
|
const RuleForm({super.key, this.rule});
|
2025-10-14 15:13:52 +08:00
|
|
|
|
|
|
|
|
@override
|
2026-10-16 14:55:07 +08:00
|
|
|
State<RuleForm> createState() => _RuleFormState();
|
2025-10-14 15:13:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-10-16 14:55:07 +08:00
|
|
|
class _RuleFormState extends State<RuleForm> {
|
2025-10-14 15:13:52 +08:00
|
|
|
late RuleAction _ruleAction;
|
2026-10-16 14:55:07 +08:00
|
|
|
late String _ruleTarget;
|
|
|
|
|
late final TextEditingController _contentController;
|
|
|
|
|
late bool _noResolve;
|
|
|
|
|
late bool _src;
|
|
|
|
|
bool _attempted = false;
|
2026-09-05 20:09:44 +08:00
|
|
|
|
2025-10-14 15:13:52 +08:00
|
|
|
@override
|
2026-10-16 14:55:07 +08:00
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2026-02-02 10:14:13 +08:00
|
|
|
final rule = widget.rule;
|
2026-10-16 14:55:07 +08:00
|
|
|
_ruleAction = rule?.ruleAction ?? RuleAction.addedRuleActions.first;
|
|
|
|
|
_ruleTarget = rule?.ruleTarget ?? RuleTarget.ruleTargetNames.first;
|
|
|
|
|
_contentController = TextEditingController(text: rule?.content);
|
|
|
|
|
_noResolve = rule?.noResolve ?? false;
|
|
|
|
|
_src = rule?.src ?? false;
|
2025-10-14 15:13:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:44 +08:00
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_contentController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 14:55:07 +08:00
|
|
|
({_RuleField field, String message})? _issue() {
|
|
|
|
|
final appLocalizations = context.appLocalizations;
|
|
|
|
|
final content = _contentController.text;
|
|
|
|
|
if (content.isEmpty) {
|
|
|
|
|
return (
|
|
|
|
|
field: _RuleField.content,
|
|
|
|
|
message: appLocalizations.emptyTip(appLocalizations.content),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
final payloadError = Rule(
|
|
|
|
|
ruleAction: _ruleAction,
|
|
|
|
|
content: content,
|
|
|
|
|
).payloadError;
|
|
|
|
|
if (payloadError != null) {
|
|
|
|
|
return (
|
|
|
|
|
field: _RuleField.content,
|
|
|
|
|
message: payloadError.getMessage(context),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (_ruleTarget.isEmpty) {
|
|
|
|
|
return (
|
|
|
|
|
field: _RuleField.target,
|
|
|
|
|
message: appLocalizations.splitStrategyNotEmpty,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 15:13:52 +08:00
|
|
|
void _handleSubmit() {
|
2026-10-16 14:55:07 +08:00
|
|
|
if (_issue() != null) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_attempted = true;
|
|
|
|
|
});
|
2025-10-14 15:13:52 +08:00
|
|
|
return;
|
|
|
|
|
}
|
2026-10-16 14:55:07 +08:00
|
|
|
Navigator.of(context).pop(
|
2026-10-16 20:56:11 +08:00
|
|
|
(widget.rule ?? Rule(id: snowflake.id)).copyWith(
|
2026-10-16 14:55:07 +08:00
|
|
|
ruleAction: _ruleAction,
|
|
|
|
|
content: _contentController.text,
|
|
|
|
|
ruleTarget: _ruleTarget,
|
|
|
|
|
noResolve: _noResolve,
|
|
|
|
|
src: _src,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _targetLabel(String target) => target == RuleAction.MATCH.value
|
|
|
|
|
? context.appLocalizations.matchTarget
|
|
|
|
|
: target;
|
|
|
|
|
|
|
|
|
|
Widget _buildSwitchRow({
|
|
|
|
|
required String title,
|
|
|
|
|
required bool value,
|
|
|
|
|
required ValueChanged<bool> onChanged,
|
|
|
|
|
}) {
|
|
|
|
|
return FormRow(
|
|
|
|
|
title: title,
|
|
|
|
|
onPressed: () => onChanged(!value),
|
|
|
|
|
trailing: Switch(value: value, onChanged: onChanged),
|
2025-10-14 15:13:52 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-02-02 10:14:13 +08:00
|
|
|
final appLocalizations = context.appLocalizations;
|
2026-10-16 14:55:07 +08:00
|
|
|
final issue = _attempted ? _issue() : null;
|
|
|
|
|
final targets = [...RuleTarget.ruleTargetNames, RuleAction.MATCH.value];
|
|
|
|
|
return FormPage(
|
2025-10-14 15:13:52 +08:00
|
|
|
title: widget.rule != null
|
|
|
|
|
? appLocalizations.editRule
|
|
|
|
|
: appLocalizations.addRule,
|
2026-10-16 14:55:07 +08:00
|
|
|
onSave: _handleSubmit,
|
|
|
|
|
error: issue?.message,
|
|
|
|
|
children: [
|
|
|
|
|
generateSection(
|
|
|
|
|
title: appLocalizations.basicInfo,
|
|
|
|
|
items: [
|
|
|
|
|
ListItem<RuleAction>.options(
|
|
|
|
|
title: Text(appLocalizations.proxyType),
|
|
|
|
|
options: RuleAction.addedRuleActions,
|
|
|
|
|
value: _ruleAction,
|
|
|
|
|
textBuilder: (action) => action.value,
|
|
|
|
|
onChanged: (action) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_ruleAction = action;
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-10-14 15:13:52 +08:00
|
|
|
),
|
2026-10-16 14:55:07 +08:00
|
|
|
FormRow(
|
|
|
|
|
title: appLocalizations.content,
|
|
|
|
|
invalid: issue?.field == _RuleField.content,
|
|
|
|
|
trailing: FormRowTextField(
|
|
|
|
|
controller: _contentController,
|
|
|
|
|
hintText: appLocalizations.inputRuleContent,
|
|
|
|
|
autocorrect: false,
|
|
|
|
|
inputFormatters: TextInputLimits.limit(TextInputLimits.rule),
|
|
|
|
|
onChanged: (_) {
|
|
|
|
|
if (_attempted) {
|
|
|
|
|
setState(() {});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onFieldSubmitted: (_) => _handleSubmit(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ListItem<String>.options(
|
|
|
|
|
title: Text(appLocalizations.splitStrategy),
|
|
|
|
|
options: [
|
|
|
|
|
if (!targets.contains(_ruleTarget)) _ruleTarget,
|
|
|
|
|
...targets,
|
|
|
|
|
],
|
|
|
|
|
value: _ruleTarget,
|
|
|
|
|
textBuilder: _targetLabel,
|
|
|
|
|
onChanged: (target) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_ruleTarget = target;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
2025-10-14 15:13:52 +08:00
|
|
|
),
|
2026-10-16 14:55:07 +08:00
|
|
|
if (_ruleAction.hasParams)
|
|
|
|
|
generateSection(
|
|
|
|
|
title: appLocalizations.additionalParameters,
|
|
|
|
|
items: [
|
|
|
|
|
_buildSwitchRow(
|
|
|
|
|
title: appLocalizations.noResolveHostname,
|
|
|
|
|
value: _noResolve,
|
|
|
|
|
onChanged: (value) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_noResolve = value;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
_buildSwitchRow(
|
|
|
|
|
title: appLocalizations.matchSourceIp,
|
|
|
|
|
value: _src,
|
|
|
|
|
onChanged: (value) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_src = value;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
2025-10-14 15:13:52 +08:00
|
|
|
),
|
2026-10-16 14:55:07 +08:00
|
|
|
],
|
2026-09-05 20:09:44 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|