304 lines
9.4 KiB
Dart
304 lines
9.4 KiB
Dart
|
|
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';
|
||
|
|
import 'package:fl_clash/widgets/card.dart';
|
||
|
|
import 'package:fl_clash/widgets/dialog.dart';
|
||
|
|
import 'package:fl_clash/widgets/input.dart';
|
||
|
|
import 'package:fl_clash/widgets/list.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
|
||
|
|
class RuleItem extends StatelessWidget {
|
||
|
|
final bool isSelected;
|
||
|
|
final bool isEditing;
|
||
|
|
final Rule rule;
|
||
|
|
final void Function(String id) onSelected;
|
||
|
|
final void Function(Rule rule) onEdit;
|
||
|
|
|
||
|
|
const RuleItem({
|
||
|
|
super.key,
|
||
|
|
required this.isSelected,
|
||
|
|
required this.rule,
|
||
|
|
required this.onSelected,
|
||
|
|
required this.onEdit,
|
||
|
|
this.isEditing = false,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return CommonSelectedListItem(
|
||
|
|
isSelected: isSelected,
|
||
|
|
onSelected: () {
|
||
|
|
onSelected(rule.id);
|
||
|
|
},
|
||
|
|
title: Text(
|
||
|
|
rule.value,
|
||
|
|
style: context.textTheme.bodyMedium?.toJetBrainsMono,
|
||
|
|
),
|
||
|
|
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 Material(
|
||
|
|
color: Colors.transparent,
|
||
|
|
child: Container(
|
||
|
|
margin: EdgeInsets.symmetric(vertical: 4),
|
||
|
|
child: CommonCard(
|
||
|
|
padding: EdgeInsets.zero,
|
||
|
|
radius: 18,
|
||
|
|
type: CommonCardType.filled,
|
||
|
|
onPressed: () {
|
||
|
|
onChange(!status);
|
||
|
|
},
|
||
|
|
child: ListTile(
|
||
|
|
minTileHeight: 0,
|
||
|
|
minVerticalPadding: 0,
|
||
|
|
titleTextStyle: context.textTheme.bodyMedium?.toJetBrainsMono,
|
||
|
|
contentPadding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: 16,
|
||
|
|
vertical: 16,
|
||
|
|
),
|
||
|
|
trailing: Switch(value: status, onChanged: onChange),
|
||
|
|
title: Text(rule.value),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class AddOrEditRuleDialog extends StatefulWidget {
|
||
|
|
final Rule? rule;
|
||
|
|
|
||
|
|
const AddOrEditRuleDialog({super.key, this.rule});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<AddOrEditRuleDialog> createState() => _AddOrEditRuleDialogState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _AddOrEditRuleDialogState extends State<AddOrEditRuleDialog> {
|
||
|
|
late RuleAction _ruleAction;
|
||
|
|
final _ruleTargetController = TextEditingController();
|
||
|
|
final _contentController = TextEditingController();
|
||
|
|
bool _noResolve = false;
|
||
|
|
bool _src = false;
|
||
|
|
List<DropdownMenuEntry> _targetItems = [];
|
||
|
|
final _formKey = GlobalKey<FormState>();
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
_initState();
|
||
|
|
super.initState();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _initState() {
|
||
|
|
_targetItems = [
|
||
|
|
...RuleTarget.values.map(
|
||
|
|
(item) => DropdownMenuEntry(value: item.name, label: item.name),
|
||
|
|
),
|
||
|
|
];
|
||
|
|
if (widget.rule != null) {
|
||
|
|
final parsedRule = ParsedRule.parseString(widget.rule!.value);
|
||
|
|
_ruleAction = parsedRule.ruleAction;
|
||
|
|
_contentController.text = parsedRule.content ?? '';
|
||
|
|
_ruleTargetController.text = parsedRule.ruleTarget ?? '';
|
||
|
|
_noResolve = parsedRule.noResolve;
|
||
|
|
_src = parsedRule.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 parsedRule = ParsedRule(
|
||
|
|
ruleAction: _ruleAction,
|
||
|
|
content: _contentController.text,
|
||
|
|
ruleTarget: _ruleTargetController.text,
|
||
|
|
noResolve: _noResolve,
|
||
|
|
src: _src,
|
||
|
|
);
|
||
|
|
final rule = widget.rule != null
|
||
|
|
? widget.rule!.copyWith(value: parsedRule.value)
|
||
|
|
: Rule.value(parsedRule.value);
|
||
|
|
Navigator.of(context).pop(rule);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
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: 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<RuleAction>(
|
||
|
|
filter: false,
|
||
|
|
child: OptionsDialog<RuleAction>(
|
||
|
|
title: appLocalizations.ruleName,
|
||
|
|
options: RuleAction.addedRuleActions,
|
||
|
|
textBuilder: (item) => item.value,
|
||
|
|
value: _ruleAction,
|
||
|
|
),
|
||
|
|
) ??
|
||
|
|
_ruleAction;
|
||
|
|
setState(() {});
|
||
|
|
},
|
||
|
|
child: Text(_ruleAction.value),
|
||
|
|
),
|
||
|
|
SizedBox(height: 24),
|
||
|
|
TextFormField(
|
||
|
|
keyboardType: TextInputType.text,
|
||
|
|
onFieldSubmitted: (_) {
|
||
|
|
_handleSubmit();
|
||
|
|
},
|
||
|
|
controller: _contentController,
|
||
|
|
decoration: InputDecoration(
|
||
|
|
border: const OutlineInputBorder(),
|
||
|
|
labelText: appLocalizations.content,
|
||
|
|
),
|
||
|
|
validator: (_) {
|
||
|
|
if (_contentController.text.isEmpty) {
|
||
|
|
return appLocalizations.emptyTip(
|
||
|
|
appLocalizations.content,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
),
|
||
|
|
SizedBox(height: 24),
|
||
|
|
FormField<String>(
|
||
|
|
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) ...[
|
||
|
|
SizedBox(height: 20),
|
||
|
|
Wrap(
|
||
|
|
spacing: 8,
|
||
|
|
children: [
|
||
|
|
CommonCard(
|
||
|
|
radius: 8,
|
||
|
|
isSelected: _src,
|
||
|
|
child: Padding(
|
||
|
|
padding: 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: EdgeInsets.symmetric(
|
||
|
|
horizontal: 8,
|
||
|
|
vertical: 8,
|
||
|
|
),
|
||
|
|
child: Text(
|
||
|
|
appLocalizations.noResolve,
|
||
|
|
style: context.textTheme.bodyMedium,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
onPressed: () {
|
||
|
|
setState(() {
|
||
|
|
_noResolve = !_noResolve;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
],
|
||
|
|
SizedBox(height: 20),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|