mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-06-01 17:15:48 +08:00
域名过滤支持批量导出&编辑
This commit is contained in:
@@ -209,12 +209,14 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
|
||||
'3. 抓包详情页面Headers默认展开配置;\n'
|
||||
'4. 请求编辑URL参数支持表单编辑;\n'
|
||||
'5. 增加高级重放;\n'
|
||||
'6. 域名过滤支持批量导出&编辑;\n'
|
||||
: 'Tips:By 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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user