域名过滤支持批量导出&编辑

This commit is contained in:
wanghongenpin
2024-01-05 07:28:44 +08:00
parent 7b224b9f61
commit 8cd5b26e2e
11 changed files with 673 additions and 258 deletions

View File

@@ -209,12 +209,14 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
'3. 抓包详情页面Headers默认展开配置\n'
'4. 请求编辑URL参数支持表单编辑\n'
'5. 增加高级重放;\n'
'6. 域名过滤支持批量导出&编辑;\n'
: 'TipsBy default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n'
'1. Increase multilingual support\n'
'2. Request Rewrite support file selection\n'
'3. Details page Headers Expanded Config\n'
'4. Request Edit URL parameter support for form editing\n'
'5. Support advanced replay\n';
'5. Support advanced replay\n'
'6. Domain name filtering supports batch export&editing\n';
showAlertDialog(isCN ? '更新内容V1.0.7' : "Update content V1.0.7", content, () {
widget.appConfiguration.upgradeNoticeV7 = false;
widget.appConfiguration.flushConfig();

View File

@@ -85,7 +85,7 @@ class _CustomRepeatState extends State<MobileCustomRepeat> {
return Row(
children: [
SizedBox(width: 90, child: Text("$label :")),
SizedBox(width: 95, child: Text("$label :")),
Expanded(
child: TextFormField(
controller: controller,

View File

@@ -1,6 +1,15 @@
import 'dart:collection';
import 'dart:convert';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/bin/configuration.dart';
import 'package:network_proxy/network/util/logger.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:share_plus/share_plus.dart';
import '../../../network/components/host_filter.dart';
@@ -67,19 +76,23 @@ class DomainFilter extends StatefulWidget {
}
class _DomainFilterState extends State<DomainFilter> {
late DomainList domainList;
bool changed = false;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
Widget build(BuildContext context) {
domainList = DomainList(widget.hostList);
void dispose() {
if (changed) {
widget.configuration.flushConfig();
}
super.dispose();
}
return ListView(
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(title: Text(widget.title), subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12))),
const SizedBox(height: 10),
ValueListenableBuilder(
valueListenable: widget.hostEnableNotifier,
builder: (_, bool enable, __) {
@@ -92,118 +105,326 @@ class _DomainFilterState extends State<DomainFilter> {
widget.hostEnableNotifier.value = !widget.hostEnableNotifier.value;
});
}),
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
FilledButton.icon(icon: const Icon(Icons.add), onPressed: add, label: Text(localizations.add)),
const SizedBox(width: 10),
TextButton.icon(
icon: const Icon(Icons.remove),
label: Text(localizations.delete),
onPressed: () {
if (domainList.selected().isEmpty) {
return;
}
changed = true;
setState(() {
widget.hostList.removeIndex(domainList.selected());
});
})
FilledButton.icon(
icon: const Icon(Icons.input_rounded), onPressed: import, label: Text(localizations.import)),
const SizedBox(width: 5),
]),
domainList
Expanded(child: DomainList(widget.hostList, onChange: () => changed = true))
],
);
}
@override
void dispose() {
if (changed) {
widget.configuration.flushConfig();
//导入
import() async {
final XFile? file = await openFile();
if (file == null) {
return;
}
try {
List json = jsonDecode(await file.readAsString());
for (var item in json) {
widget.hostList.add(item);
}
changed = true;
if (context.mounted) {
FlutterToastr.show(localizations.importSuccess, context);
}
setState(() {});
} catch (e, t) {
logger.e('导入失败 $file', error: e, stackTrace: t);
if (context.mounted) {
FlutterToastr.show("${localizations.importFailed} $e", context);
}
}
super.dispose();
}
void add() {
GlobalKey formKey = GlobalKey<FormState>();
String? host;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
scrollable: true,
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: formKey,
child: Column(children: <Widget>[
TextFormField(
decoration: const InputDecoration(labelText: 'Host', hintText: '*.example.com'),
onSaved: (val) => host = val)
]))),
actions: [
FilledButton(
child: Text(localizations.add),
onPressed: () {
(formKey.currentState as FormState).save();
if (host != null && host!.isNotEmpty) {
try {
changed = true;
widget.hostList.add(host!.trim());
setState(() {});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
Navigator.of(context).pop();
}),
ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
]);
showDialog(context: context, builder: (BuildContext context) => DomainAddDialog(hostList: widget.hostList))
.then((value) {
if (value != null) {
setState(() {
changed = true;
});
}
});
}
}
class DomainAddDialog extends StatelessWidget {
final HostList hostList;
final int? index;
const DomainAddDialog({super.key, required this.hostList, this.index});
@override
Widget build(BuildContext context) {
AppLocalizations localizations = AppLocalizations.of(context)!;
GlobalKey formKey = GlobalKey<FormState>();
String? host = index == null ? null : hostList.list.elementAt(index!).pattern.replaceAll(".*", "*");
return AlertDialog(
scrollable: true,
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: formKey,
child: Column(children: <Widget>[
TextFormField(
initialValue: host,
decoration: const InputDecoration(labelText: 'Host', hintText: '*.example.com'),
validator: (val) => val == null || val.trim().isEmpty ? localizations.cannotBeEmpty : null,
onChanged: (val) => host = val)
]))),
actions: [
FilledButton(
child: Text(localizations.add),
onPressed: () {
if (!(formKey.currentState as FormState).validate()) {
return;
}
try {
if (index != null) {
hostList.list[index!] = RegExp(host!.trim().replaceAll("*", ".*"));
} else {
hostList.add(host!.trim());
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
Navigator.of(context).pop(host);
}),
ElevatedButton(child: Text(localizations.close), onPressed: () => Navigator.of(context).pop())
]);
}
}
///域名列表
class DomainList extends StatefulWidget {
final HostList hostList;
final Function onChange;
DomainList(this.hostList) : super(key: GlobalKey<_DomainListState>());
const DomainList(this.hostList, {super.key, required this.onChange});
@override
State<StatefulWidget> createState() => _DomainListState();
List<int> selected() {
var state = (key as GlobalKey<_DomainListState>).currentState;
List<int> list = [];
state?.selected.forEach((key, value) {
if (value == true) {
list.add(key);
}
});
return list;
}
}
class _DomainListState extends State<DomainList> {
late Map<int, bool> selected = {};
Set<int> selected = HashSet<int>();
AppLocalizations get localizations => AppLocalizations.of(context)!;
bool changed = false;
bool multiple = false;
onChanged() {
changed = true;
widget.onChange.call();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 10),
child: SingleChildScrollView(
child: DataTable(
border: TableBorder.symmetric(outside: BorderSide(width: 1, color: Theme.of(context).highlightColor)),
columns: const <DataColumn>[
DataColumn(label: Text('域名')),
],
rows: List.generate(
widget.hostList.list.length,
(index) => DataRow(
cells: [DataCell(Text(widget.hostList.list[index].pattern.replaceAll(".*", "*")))],
selected: selected[index] == true,
onSelectChanged: (value) {
setState(() {
selected[index] = value!;
});
})),
)));
return Scaffold(
persistentFooterButtons: [multiple ? globalMenu() : const SizedBox()],
body: Container(
padding: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: Scrollbar(
child: ListView(children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(width: 15),
const Expanded(child: Text('Host')),
],
),
const Divider(thickness: 0.5),
Column(children: rows(widget.hostList.list))
]))));
}
globalMenu() {
return Stack(children: [
Container(
height: 50,
width: double.infinity,
margin: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.withOpacity(0.2)),
color: Colors.white,
backgroundBlendMode: BlendMode.colorBurn)),
Positioned(
top: 0,
left: 0,
right: 0,
child: Center(
child: TextButton(
onPressed: () {},
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
TextButton.icon(
onPressed: () {
export(selected.toList());
setState(() {
selected.clear();
multiple = false;
});
},
icon: const Icon(Icons.share, size: 18),
label: Text(localizations.export, style: const TextStyle(fontSize: 14))),
TextButton.icon(
onPressed: () => remove(),
icon: const Icon(Icons.delete, size: 18),
label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),
TextButton.icon(
onPressed: () {
setState(() {
multiple = false;
selected.clear();
});
},
icon: const Icon(Icons.cancel, size: 18),
label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),
]))))
]);
}
List<Widget> rows(List<RegExp> list) {
var primaryColor = Theme.of(context).colorScheme.primary;
return List.generate(list.length, (index) {
return InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
hoverColor: primaryColor.withOpacity(0.3),
onLongPress: () => showMenus(index),
onDoubleTap: () => showEdit(index),
onTap: () {
if (multiple) {
setState(() {
if (!selected.add(index)) {
selected.remove(index);
}
});
return;
}
showEdit(index);
},
child: Container(
color: selected.contains(index)
? primaryColor.withOpacity(0.8)
: index.isEven
? Colors.grey.withOpacity(0.1)
: null,
height: 38,
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
const SizedBox(width: 15),
Expanded(
child: Text(list[index].pattern.replaceAll(".*", "*"), style: const TextStyle(fontSize: 14))),
],
)));
});
}
showEdit([int? index]) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return DomainAddDialog(hostList: widget.hostList, index: index);
}).then((value) {
if (value != null) {
setState(() {
onChanged();
});
}
});
}
//点击菜单
showMenus(int index) {
setState(() {
selected.add(index);
});
showModalBottomSheet(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(10))),
context: context,
enableDrag: true,
builder: (ctx) {
return Wrap(alignment: WrapAlignment.center, children: [
BottomSheetItem(
text: localizations.multiple,
onPressed: () {
setState(() => multiple = true);
}),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(onPressed: () => export([index]), text: localizations.share),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: localizations.delete,
onPressed: () {
widget.hostList.removeIndex([index]);
onChanged();
}),
Container(color: Theme.of(context).hoverColor, height: 8),
TextButton(
child: Container(
height: 50,
width: double.infinity,
padding: const EdgeInsets.only(top: 10),
child: Text(localizations.cancel, textAlign: TextAlign.center)),
onPressed: () {
Navigator.of(context).pop();
}),
]);
}).then((value) {
if (multiple) {
return;
}
setState(() {
selected.remove(index);
});
});
}
//导出
export(List<int> indexes) async {
if (indexes.isEmpty) return;
String fileName = 'host-filters.config';
var list = [];
for (var index in indexes) {
String rule = widget.hostList.list[index].pattern;
list.add(rule);
}
final XFile file = XFile.fromData(utf8.encode(jsonEncode(list)), mimeType: 'config');
await Share.shareXFiles([file], subject: fileName);
}
//删除
Future<void> remove() async {
if (selected.isEmpty) return;
return showConfirmDialog(context, content: localizations.requestRewriteDeleteConfirm(selected.length),
onConfirm: () async {
widget.hostList.removeIndex(selected.toList());
onChanged();
setState(() {
multiple = false;
selected.clear();
});
if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);
});
}
}

View File

@@ -242,16 +242,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
});
return;
}
var rule = widget.requestRewrites.rules[index];
var rewriteItems = await widget.requestRewrites.getRewriteItems(rule);
if (!mounted) return;
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => RewriteRule(rule: rule, items: rewriteItems)))
.then((value) {
if (value != null) {
setState(() {});
}
});
showEdit(index);
},
child: Container(
color: selected.contains(index)
@@ -287,6 +278,20 @@ class _RequestRuleListState extends State<RequestRuleList> {
});
}
showEdit(int index) async {
var rule = widget.requestRewrites.rules[index];
var rewriteItems = await widget.requestRewrites.getRewriteItems(rule);
if (!mounted) return;
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => RewriteRule(rule: rule, items: rewriteItems)))
.then((value) {
if (value != null) {
setState(() {});
}
});
}
//点击菜单
showMenus(int index) {
setState(() {
@@ -302,36 +307,20 @@ class _RequestRuleListState extends State<RequestRuleList> {
BottomSheetItem(
text: localizations.multiple,
onPressed: () {
setState(() {
multiple = true;
});
setState(() => multiple = true);
}),
const Divider(thickness: 0.5),
BottomSheetItem(
text: localizations.edit,
onPressed: () async {
var rule = widget.requestRewrites.rules[index];
var rewriteItems = await widget.requestRewrites.getRewriteItems(rule);
if (!mounted) return;
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => RewriteRule(rule: rule, items: rewriteItems)))
.then((value) {
if (value != null) {
setState(() {});
}
});
}),
const Divider(thickness: 0.5),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(text: localizations.edit, onPressed: () => showEdit(index)),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(text: localizations.share, onPressed: () => export([index])),
const Divider(thickness: 0.5, height: 1),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: rules[index].enabled ? localizations.disabled : localizations.enable,
onPressed: () {
rules[index].enabled = !rules[index].enabled;
changed = true;
}),
const Divider(thickness: 0.5),
const Divider(thickness: 0.5, height: 5),
BottomSheetItem(
text: localizations.delete,
onPressed: () async {
@@ -363,7 +352,6 @@ class _RequestRuleListState extends State<RequestRuleList> {
//导出js
Future<void> export(List<int> indexes) async {
if (indexes.isEmpty) return;
String fileName = 'proxypin-rewrites.config';
var list = [];