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

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

@@ -4,12 +4,12 @@
## 开源免费抓包工具支持Windows、Mac、Android、IOS、Linux 全平台系统
您可以使用它来拦截、检查和重写HTTPS流量ProxyPin基于FlutterUI美观易用。
您可以使用它来拦截、检查和重写HTTPS流量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">

View File

@@ -2,7 +2,7 @@
English | [中文](README.md)
## Open source free packet capture toolSupport 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">

View File

@@ -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()
})
}
}

View File

@@ -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"));
}

View File

@@ -205,13 +205,15 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
'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'
'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)));
});
}

View File

@@ -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);
});
});
}
}

View File

@@ -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) {

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 = [];