mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-04-04 08:15:09 +08:00
域名过滤支持批量导出&编辑
This commit is contained in:
@@ -4,12 +4,12 @@
|
||||
|
||||
## 开源免费抓包工具,支持Windows、Mac、Android、IOS、Linux 全平台系统
|
||||
|
||||
您可以使用它来拦截、检查和重写HTTP(S)流量,ProxyPin基于Flutter,UI美观易用。
|
||||
您可以使用它来拦截、检查和重写HTTP(S)流量,ProxyPin基于Flutter开发,UI美观易用。
|
||||
|
||||
## 核心特性
|
||||
|
||||
* 手机扫码连接: 不用手动配置Wifi代理,包括配置同步。所有终端都可以互相扫码连接转发流量。
|
||||
* 域名黑白名单过滤: 只拦截您所需要的流量,不拦截其他流量,避免干扰其他应用。
|
||||
* 域名过滤: 只拦截您所需要的流量,不拦截其他流量,避免干扰其他应用。
|
||||
* 请求重写: 支持重定向,支持替换请求或响应报文,也可以根据增则修改请求或或响应。
|
||||
* 脚本: 支持编写JavaScriot脚本来处理请求或响应。
|
||||
* 搜索:根据关键词响应类型多种条件搜索请求
|
||||
@@ -25,7 +25,7 @@ iOS国内TF下载地址(有1万名额限制,满了会清理不使用的用户)
|
||||
|
||||
TG: https://t.me/proxypin_tg
|
||||
|
||||
接下来会持续完善功能和体验,UI优化。
|
||||
**接下来会持续完善功能和体验,UI优化。**
|
||||
|
||||
<img alt="image" width="500px" height="400px" src="https://github.com/wanghongenpin/network-proxy-flutter/assets/24794200/67a2feb1-f1c3-4c0c-8737-5abe62c34794">. <img alt="image" height="500px" src="https://github.com/wanghongenpin/network_proxy_flutter/assets/24794200/1bb4b1ec-ec5c-44a7-add7-f0f94c8765b9">
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.md)
|
||||
## Open source free packet capture tool,Support Windows、Mac、Android、IOS、Linux Full platform system
|
||||
You can use it to intercept, inspect & rewrite HTTP(S) traffic, ProxyPin is based on Flutter, and the UI is beautiful
|
||||
You can use it to intercept, inspect & rewrite HTTP(S) traffic, ProxyPin is based on Flutter develop, and the UI is beautiful
|
||||
and easy to use.
|
||||
## Features
|
||||
* Mobile scan code connection: no need to manually configure WiFi proxy, including configuration synchronization. All terminals can scan codes to connect and forward traffic to each other.
|
||||
@@ -21,7 +21,7 @@ iOS TestFlight(Limited by quota): https://testflight.apple.com/join/gURGH6B4
|
||||
|
||||
TG: https://t.me/proxypin_tg
|
||||
|
||||
We will continue to improve the features and experience, as well as optimize the UI.
|
||||
**We will continue to improve the features and experience, as well as optimize the UI.**
|
||||
|
||||
<img alt="image" width="500px" height="400px" src="https://github.com/wanghongenpin/network-proxy-flutter/assets/24794200/67a2feb1-f1c3-4c0c-8737-5abe62c34794">. <img alt="image" height="500px" src="https://github.com/wanghongenpin/network_proxy_flutter/assets/24794200/1bb4b1ec-ec5c-44a7-add7-f0f94c8765b9">
|
||||
|
||||
|
||||
@@ -58,6 +58,13 @@ import NetworkExtension
|
||||
timer?.invalidate();
|
||||
timer = nil;
|
||||
}
|
||||
|
||||
if (!AudioManager.shared.openBackgroundAudioAutoplay) {
|
||||
AudioManager.shared.openBackgroundAudioAutoplay = true
|
||||
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
|
||||
self.endBackgroundUpdateTask()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -80,9 +80,6 @@ class Whites extends HostList {}
|
||||
class Blacks extends HostList {
|
||||
Blacks() {
|
||||
enabled = true;
|
||||
list.add(RegExp(".*.github.com"));
|
||||
list.add(RegExp("github.com"));
|
||||
list.add(RegExp(".*.google.com"));
|
||||
list.add(RegExp(".*.apple.com"));
|
||||
list.add(RegExp(".*.icloud.com"));
|
||||
}
|
||||
|
||||
@@ -205,13 +205,15 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
'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'
|
||||
'Click HTTPS Capture packets(Lock icon),Choose to install the root certificate and follow the prompts to proceed。\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',
|
||||
style: const TextStyle(fontSize: 14)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/components/host_filter.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.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';
|
||||
|
||||
/// @author wanghongen
|
||||
/// 2023/10/8
|
||||
@@ -38,7 +46,7 @@ class _FilterDialogState extends State<FilterDialog> {
|
||||
]),
|
||||
content: SizedBox(
|
||||
width: 680,
|
||||
height: 460,
|
||||
height: 520,
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: [
|
||||
@@ -87,62 +95,10 @@ 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);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12)),
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: widget.hostEnableNotifier,
|
||||
builder: (_, bool enable, __) {
|
||||
return SwitchListTile(
|
||||
title: Text(localizations.enable),
|
||||
dense: true,
|
||||
value: widget.hostList.enabled,
|
||||
onChanged: (value) {
|
||||
widget.hostList.enabled = value;
|
||||
changed = true;
|
||||
widget.hostEnableNotifier.value = !widget.hostEnableNotifier.value;
|
||||
});
|
||||
}),
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
onPressed: () {
|
||||
add();
|
||||
},
|
||||
label: Text(localizations.add, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.remove, size: 14),
|
||||
label: Text(localizations.delete, style: const TextStyle(fontSize: 12)),
|
||||
onPressed: () {
|
||||
if (domainList.selected().isEmpty) {
|
||||
return;
|
||||
}
|
||||
changed = true;
|
||||
setState(() {
|
||||
widget.hostList.removeIndex(domainList.selected());
|
||||
});
|
||||
})
|
||||
]),
|
||||
domainList
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (changed) {
|
||||
@@ -151,96 +107,338 @@ class _DomainFilterState extends State<DomainFilter> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(widget.title),
|
||||
isThreeLine: true,
|
||||
subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12)),
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
),
|
||||
Row(children: [
|
||||
const SizedBox(width: 8),
|
||||
Text(localizations.enable),
|
||||
const SizedBox(width: 10),
|
||||
SwitchWidget(
|
||||
scale: 0.8,
|
||||
value: widget.hostList.enabled,
|
||||
onChanged: (value) {
|
||||
widget.hostList.enabled = value;
|
||||
changed = true;
|
||||
}),
|
||||
const Expanded(child: SizedBox()),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
onPressed: add,
|
||||
label: Text(localizations.add, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.input_rounded, size: 14),
|
||||
onPressed: import,
|
||||
label: Text(localizations.import, style: const TextStyle(fontSize: 12))),
|
||||
const SizedBox(width: 5),
|
||||
]),
|
||||
DomainList(widget.hostList, onChange: () => changed = true)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//导入
|
||||
import() async {
|
||||
XTypeGroup typeGroup = const XTypeGroup(extensions: <String>['config']);
|
||||
final XFile? file = await openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add() {
|
||||
GlobalKey formKey = GlobalKey<FormState>();
|
||||
String? host;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
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();
|
||||
})
|
||||
]);
|
||||
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 = {};
|
||||
Map<int, bool> selected = {};
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
bool isPress = false;
|
||||
bool changed = false;
|
||||
|
||||
onChanged() {
|
||||
changed = true;
|
||||
widget.onChange.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
height: 300,
|
||||
child: SingleChildScrollView(
|
||||
child: DataTable(
|
||||
border: TableBorder.symmetric(outside: BorderSide(width: 1, color: Theme.of(context).highlightColor)),
|
||||
columns: <DataColumn>[
|
||||
DataColumn(label: Text(localizations.domain)),
|
||||
],
|
||||
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 GestureDetector(
|
||||
onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition),
|
||||
onTapDown: (details) {
|
||||
if (selected.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.metaLeft) ||
|
||||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.control)) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
selected.clear();
|
||||
});
|
||||
},
|
||||
child: Listener(
|
||||
onPointerUp: (details) => isPress = false,
|
||||
onPointerDown: (details) => isPress = true,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
height: 380,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(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))
|
||||
])))));
|
||||
}
|
||||
|
||||
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),
|
||||
onSecondaryTapDown: (details) => showMenus(details, index),
|
||||
onDoubleTap: () => showEdit(index),
|
||||
onHover: (hover) {
|
||||
if (isPress && selected[index] != true) {
|
||||
setState(() {
|
||||
selected[index] = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.metaLeft) ||
|
||||
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.control)) {
|
||||
setState(() {
|
||||
selected[index] = !(selected[index] ?? false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (selected.isEmpty) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
selected.clear();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: selected[index] == true
|
||||
? 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))),
|
||||
],
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
//导出
|
||||
export(List<int> indexes) async {
|
||||
if (indexes.isEmpty) return;
|
||||
|
||||
String fileName = 'host-filters.config';
|
||||
String? saveLocation = (await getSaveLocation(suggestedName: fileName))?.path;
|
||||
if (saveLocation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var list = [];
|
||||
for (var index in indexes) {
|
||||
String rule = widget.hostList.list[index].pattern;
|
||||
list.add(rule);
|
||||
}
|
||||
|
||||
final XFile xFile = XFile.fromData(utf8.encode(jsonEncode(list)), mimeType: 'json');
|
||||
await xFile.saveTo(saveLocation);
|
||||
if (context.mounted) FlutterToastr.show(localizations.exportSuccess, context);
|
||||
}
|
||||
|
||||
//删除
|
||||
Future<void> remove(List<int> indexes) async {
|
||||
if (indexes.isEmpty) return;
|
||||
return showConfirmDialog(context, content: localizations.requestRewriteDeleteConfirm(indexes.length),
|
||||
onConfirm: () async {
|
||||
widget.hostList.removeIndex(indexes);
|
||||
onChanged();
|
||||
setState(() {
|
||||
selected.clear();
|
||||
});
|
||||
if (mounted) FlutterToastr.show(localizations.deleteSuccess, context);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showGlobalMenu(Offset offset) {
|
||||
showContextMenu(context, offset, items: [
|
||||
PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
enabled: selected.isNotEmpty,
|
||||
child: Text(localizations.export),
|
||||
onTap: () => export(selected.keys.toList())),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
enabled: selected.isNotEmpty,
|
||||
child: Text(localizations.deleteSelect),
|
||||
onTap: () => remove(selected.keys.toList())),
|
||||
]);
|
||||
}
|
||||
|
||||
//点击菜单
|
||||
showMenus(TapDownDetails details, int index) {
|
||||
if (selected.length > 1) {
|
||||
showGlobalMenu(details.globalPosition);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
selected[index] = true;
|
||||
});
|
||||
|
||||
showContextMenu(context, details.globalPosition, items: [
|
||||
PopupMenuItem(height: 35, child: Text(localizations.edit), onTap: () => showEdit(index)),
|
||||
PopupMenuItem(height: 35, onTap: () => export([index]), child: Text(localizations.export)),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
height: 35,
|
||||
child: Text(localizations.delete),
|
||||
onTap: () {
|
||||
widget.hostList.removeIndex([index]);
|
||||
onChanged();
|
||||
})
|
||||
]).then((value) {
|
||||
setState(() {
|
||||
selected.remove(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ class RequestRuleList extends StatefulWidget {
|
||||
class _RequestRuleListState extends State<RequestRuleList> {
|
||||
Map<int, bool> selected = {};
|
||||
late List<RequestRewriteRule> rules;
|
||||
bool isPress = false;
|
||||
|
||||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||||
|
||||
@@ -193,7 +194,6 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
rules = widget.requestRewrites.rules;
|
||||
}
|
||||
|
||||
bool isPress = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -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