diff --git a/.gitignore b/.gitignore
index daa5ac0..87b2f4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,6 @@ app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
-/android/app/release
\ No newline at end of file
+/android/app/release
+
+l10n_errors.txt
\ No newline at end of file
diff --git a/README.md b/README.md
index e3dbdd7..b6acc87 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,21 @@
-## [免费开源抓包工具](https://github.com/wanghongenpin/network_proxy_flutter),支持Windows、Mac、Android、IOS、Linux 全平台系统
-支持手机扫码连接,不用手动配置Wifi代理,包括配置同步。所有终端都可以互相扫码连接转发流量。
+# ProxyPin
-**Mac首次打开会提示已损坏,需要到系统偏好设置-安全性与隐私-允许任何来源。**
+[English](README_EN.md) | 中文
+
+## 开源免费抓包工具,支持Windows、Mac、Android、IOS、Linux 全平台系统
+
+您可以使用它来拦截、检查和重写HTTP(S)流量,ProxyPin基于Flutter,UI美观易用。
+
+## 核心特性
+
+* 手机扫码连接: 不用手动配置Wifi代理,包括配置同步。所有终端都可以互相扫码连接转发流量。
+* 域名黑白名单过滤: 只拦截您所需要的流量,不拦截其他流量,避免干扰其他应用。
+* 请求重写: 支持重定向,支持替换请求或响应报文,也可以根据增则修改请求或或响应。
+* 脚本: 支持编写JavaScriot脚本来处理请求或响应。
+* 搜索:根据关键词响应类型多种条件搜索请求
+* 其他:收藏、历史记录、工具箱等
+
+**Mac首次打开会提示不受信任开发者,需要到系统偏好设置-安全性与隐私-允许任何来源。**
国内下载地址: https://gitee.com/wanghongenpin/network-proxy-flutter/releases
@@ -11,9 +25,7 @@ iOS国内TF下载地址(有1万名额限制,满了会清理不使用的用户)
TG: https://t.me/proxypin_tg
-- [ ] 接下来会持续完善功能和体验,请求重写功能增强、模拟慢请求、请求debug, UI优化。
-- [ ] 支持安卓微信小程序抓包,安卓分为系统证书和用户证书,下载的自签名根证书安装都是用户证书,微信不信任用户证书,不Root导致Https抓不了了, 目前市场上所有抓包软件抓不了微信的包,后面单独做个运行空间插件,动态反编译修改配置,信任用户证书来解决。
-- [ ] WebSocket、HTTP2协议支持。
+接下来会持续完善功能和体验,UI优化。
.
diff --git a/README_EN.md b/README_EN.md
new file mode 100644
index 0000000..de3f257
--- /dev/null
+++ b/README_EN.md
@@ -0,0 +1,28 @@
+# ProxyPin
+
+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
+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.
+* Domain name filtering: Only intercept the traffic you need, and do not intercept other traffic to avoid interference with other applications.
+* Request rewrite: Support redirection, support replacement of request or response message, and can also modify request or response according to the increase.
+* Script: Support writing JavaScript scripts to process requests or responses.
+* Search: Search requests according to keywords, response types and other conditions
+* Others: Favorites, history, toolbox, etc.
+
+**Mac will prompt untrusted developers when first opened, you need to go to System Preferences-Security & Privacy-Allow any source.**
+Download: https://github.com/wanghongenpin/network_proxy_flutter/releases
+
+iOS AppStore ProxyPin: https://apps.apple.com/app/proxypin/id6450932949
+
+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.
+
+
.
+
+
diff --git a/l10n.yaml b/l10n.yaml
index 4e6692e..afa2d41 100644
--- a/l10n.yaml
+++ b/l10n.yaml
@@ -1,3 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
-output-localization-file: app_localizations.dart
\ No newline at end of file
+output-localization-file: app_localizations.dart
+untranslated-messages-file: l10n_errors.txt
\ No newline at end of file
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 6f9ce48..4373acd 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -97,6 +97,11 @@
"disableSelect": "Disable Select",
"deleteSelect": "Delete Select",
+ "modifyRequestHeader": "Modify Header",
+ "headerName": "Header Name",
+ "headerValue": "Header Value",
+ "deleteHeaderConfirm": "Do you want to delete the request header?",
+
"sequence": "All Requests",
"domainList": "Domain List",
"domainWhitelist": "Domain Whitelist",
@@ -160,7 +165,6 @@
"androidUserCAInstall": "Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate",
"androidUserXposed": "It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki",
"configWifiProxy": "Configure mobile Wi-Fi proxy",
- "installGuide": "Installation Guide",
"caInstallGuide": "Certificate Installation Guide",
"caAndroidBrowser": "Open Google Browser on Android devices:",
"caIosBrowser": "Open Safari on iOS devices:",
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index e7553e2..33aee8b 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -48,6 +48,7 @@
"deleteSuccess": "删除成功",
"send": "发送",
"fail": "失败",
+ "success": "成功",
"emptyData": "无数据",
"requestSuccess": "请求成功",
"add": "添加",
@@ -96,12 +97,17 @@
"disableSelect": "禁用选择",
"deleteSelect": "删除选择",
+ "modifyRequestHeader": "修改请求头",
+ "headerName": "请求头名称",
+ "headerValue": "请求头值",
+ "deleteHeaderConfirm": "是否删除该请求头",
+
"sequence": "全部请求",
"domainList": "域名列表",
"domainWhitelist": "域名白名单",
"domainBlacklist" :"域名黑名单",
- "appWhitelist":"应用白名单",
- "domainFilter":"域名过滤",
+ "appWhitelist": "应用白名单",
+ "domainFilter": "域名过滤",
"scanCode": "扫码连接",
"addBlacklist": "添加黑名单",
"addWhitelist": "添加白名单",
diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart
index ba810f0..40218a5 100644
--- a/lib/network/http/http_headers.dart
+++ b/lib/network/http/http_headers.dart
@@ -171,6 +171,10 @@ class HttpHeaders {
return json;
}
+ Map> getHeaders() {
+ return _headers;
+ }
+
///从json解析
factory HttpHeaders.fromJson(Map json) {
HttpHeaders headers = HttpHeaders();
diff --git a/lib/ui/configuration.dart b/lib/ui/configuration.dart
index c0c9e96..1ef5e24 100644
--- a/lib/ui/configuration.dart
+++ b/lib/ui/configuration.dart
@@ -5,14 +5,15 @@ import 'package:flutter/material.dart';
import 'package:network_proxy/utils/platform.dart';
import 'package:path_provider/path_provider.dart';
+/// @author wanghongen
+/// 2024/1/1
class ThemeModel {
ThemeMode mode;
bool useMaterial3;
ThemeModel({this.mode = ThemeMode.system, this.useMaterial3 = true});
- ThemeModel copy({ThemeMode? mode, bool? useMaterial3}) =>
- ThemeModel(
+ ThemeModel copy({ThemeMode? mode, bool? useMaterial3}) => ThemeModel(
mode: mode ?? this.mode,
useMaterial3: useMaterial3 ?? this.useMaterial3,
);
@@ -109,7 +110,7 @@ class AppConfiguration {
try {
Map config = jsonDecode(json);
var mode =
- ThemeMode.values.firstWhere((element) => element.name == config['mode'], orElse: () => ThemeMode.system);
+ ThemeMode.values.firstWhere((element) => element.name == config['mode'], orElse: () => ThemeMode.system);
_theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true);
upgradeNoticeV7 = config['upgradeNoticeV7'] ?? true;
_language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']);
diff --git a/lib/ui/content/body.dart b/lib/ui/content/body.dart
index 0b81e52..3830589 100644
--- a/lib/ui/content/body.dart
+++ b/lib/ui/content/body.dart
@@ -86,9 +86,11 @@ class HttpBodyState extends State {
List list = [
widget.inNewWindow ? const SizedBox() : titleWidget(),
+ const SizedBox(height: 3),
SizedBox(
height: 36,
child: TabBar(
+ labelPadding: const EdgeInsets.only(left: 3, right: 5),
tabs: tabs.tabList(),
onTap: (index) {
tabIndex = index;
@@ -133,14 +135,17 @@ class HttpBodyState extends State {
if (body == null) {
return;
}
- Clipboard.setData(ClipboardData(text: body)).then((value) => FlutterToastr.show(localizations.copied, context));
+ Clipboard.setData(ClipboardData(text: body))
+ .then((value) => FlutterToastr.show(localizations.copied, context));
}),
];
if (!widget.hideRequestRewrite) {
list.add(const SizedBox(width: 3));
- list.add(
- IconButton(icon: const Icon(Icons.edit_document, size: 18), tooltip: localizations.requestRewrite, onPressed: showRequestRewrite));
+ list.add(IconButton(
+ icon: const Icon(Icons.edit_document, size: 18),
+ tooltip: localizations.requestRewrite,
+ onPressed: showRequestRewrite));
}
list.add(const SizedBox(width: 3));
@@ -152,7 +157,8 @@ class HttpBodyState extends State {
}));
if (!inNewWindow) {
list.add(const SizedBox(width: 3));
- list.add(IconButton(icon: const Icon(Icons.open_in_new, size: 18), tooltip: localizations.newWindow, onPressed: () => openNew()));
+ list.add(IconButton(
+ icon: const Icon(Icons.open_in_new, size: 18), tooltip: localizations.newWindow, onPressed: () => openNew()));
}
return Wrap(
diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart
index 8d353cd..3def7dd 100644
--- a/lib/ui/desktop/desktop.dart
+++ b/lib/ui/desktop/desktop.dart
@@ -19,6 +19,8 @@ import 'package:url_launcher/url_launcher.dart';
import '../component/split_view.dart';
+/// @author wanghongen
+/// 2023/10/8
class DesktopHomePage extends StatefulWidget {
final Configuration configuration;
final AppConfiguration appConfiguration;
diff --git a/lib/ui/desktop/left/favorite.dart b/lib/ui/desktop/left/favorite.dart
index 9a7b678..a0cfd91 100644
--- a/lib/ui/desktop/left/favorite.dart
+++ b/lib/ui/desktop/left/favorite.dart
@@ -19,6 +19,8 @@ import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/utils/curl.dart';
import 'package:window_manager/window_manager.dart';
+/// @author wanghongen
+/// 2023/10/8
class Favorites extends StatefulWidget {
final NetworkTabController panel;
diff --git a/lib/ui/desktop/left/history.dart b/lib/ui/desktop/left/history.dart
index f35d9c2..068b82f 100644
--- a/lib/ui/desktop/left/history.dart
+++ b/lib/ui/desktop/left/history.dart
@@ -22,7 +22,9 @@ import 'package:network_proxy/utils/har.dart';
import '../../content/panel.dart';
import 'list.dart';
-///历史记录
+/// 历史记录
+/// @author wanghongen
+/// 2023/10/8
class HistoryPageWidget extends StatelessWidget {
final ProxyServer proxyServer;
final GlobalKey domainWidgetState;
diff --git a/lib/ui/desktop/left/list.dart b/lib/ui/desktop/left/list.dart
index bf4da2c..b8846a5 100644
--- a/lib/ui/desktop/left/list.dart
+++ b/lib/ui/desktop/left/list.dart
@@ -16,7 +16,9 @@ import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
import 'package:network_proxy/ui/desktop/left/request.dart';
import 'package:network_proxy/ui/desktop/left/search.dart';
-///左侧域名
+/// 左侧域名
+/// @author wanghongen
+/// 2023/10/8
class DomainList extends StatefulWidget {
final NetworkTabController panel;
final ProxyServer proxyServer;
diff --git a/lib/ui/desktop/left/request.dart b/lib/ui/desktop/left/request.dart
index f19aba2..a4c381a 100644
--- a/lib/ui/desktop/left/request.dart
+++ b/lib/ui/desktop/left/request.dart
@@ -19,7 +19,9 @@ import 'package:network_proxy/utils/lang.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-///请求 URI
+/// 请求 URI
+/// @author wanghongen
+/// 2023/10/8
class RequestWidget extends StatefulWidget {
final Color? color;
final HttpRequest request;
@@ -93,7 +95,8 @@ class _RequestWidgetState extends State {
items: [
popupItem(localizations.copyUrl, onTap: () {
var requestUrl = widget.request.requestUrl;
- Clipboard.setData(ClipboardData(text: requestUrl)).then((value) => FlutterToastr.show(localizations.copied, context));
+ Clipboard.setData(ClipboardData(text: requestUrl))
+ .then((value) => FlutterToastr.show(localizations.copied, context));
}),
popupItem(localizations.copyRequestResponse, onTap: () {
Clipboard.setData(ClipboardData(text: copyRequest(widget.request, widget.response.get())))
diff --git a/lib/ui/desktop/left/request_editor.dart b/lib/ui/desktop/left/request_editor.dart
index 5325be8..ec1d145 100644
--- a/lib/ui/desktop/left/request_editor.dart
+++ b/lib/ui/desktop/left/request_editor.dart
@@ -20,6 +20,7 @@ import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.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/http/http.dart';
import 'package:network_proxy/network/http/http_headers.dart';
@@ -28,8 +29,9 @@ import 'package:network_proxy/ui/component/split_view.dart';
import 'package:network_proxy/ui/component/state_component.dart';
import 'package:network_proxy/ui/content/body.dart';
import 'package:network_proxy/utils/curl.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:network_proxy/utils/lang.dart';
+/// @author wanghongen
class RequestEditor extends StatefulWidget {
final WindowController? windowController;
final HttpRequest? request;
@@ -43,8 +45,11 @@ class RequestEditor extends StatefulWidget {
}
class RequestEditorState extends State {
+ final UrlQueryNotifier _queryNotifier = UrlQueryNotifier();
final requestLineKey = GlobalKey<_RequestLineState>();
final requestKey = GlobalKey<_HttpState>();
+ final responseKey = GlobalKey<_HttpState>();
+
ValueNotifier responseChange = ValueNotifier(false);
HttpRequest? request;
HttpResponse? response;
@@ -102,17 +107,20 @@ class RequestEditorState extends State {
],
),
body: Column(children: [
- _RequestLine(key: requestLineKey, request: request),
+ _RequestLine(key: requestLineKey, request: request, urlQueryNotifier: _queryNotifier),
Expanded(
child: VerticalSplitView(
ratio: 0.53,
left: _HttpWidget(
- key: requestKey,
- title: const Text("Request", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
- message: request),
+ key: requestKey,
+ title: const Text("Request", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
+ message: request,
+ urlQueryNotifier: _queryNotifier,
+ ),
right: ValueListenableBuilder(
valueListenable: responseChange,
builder: (_, value, __) => _HttpWidget(
+ key: responseKey,
title: Row(children: [
const Text("Response", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const Spacer(),
@@ -131,15 +139,15 @@ class RequestEditorState extends State {
var requestBody = requestKey.currentState?.getBody();
HttpRequest request =
- HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.encodeFull(currentState.requestUrl));
+ HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.encodeFull(currentState.requestUrl.text));
request.headers.addAll(headers);
-
request.body = requestBody == null ? null : utf8.encode(requestBody);
HttpClients.proxyRequest(request).then((response) {
FlutterToastr.show(localizations.requestSuccess, context);
this.response = response;
responseChange.value = !responseChange.value;
+ responseKey.currentState?.change(response);
}).catchError((e) {
FlutterToastr.show('${localizations.fail}$e', context);
});
@@ -168,6 +176,7 @@ class RequestEditorState extends State {
try {
setState(() {
request = parseCurl(text!);
+ requestKey.currentState?.change(request!);
requestLineKey.currentState?.change(request?.requestUrl, request?.method.name);
});
} catch (e) {
@@ -182,12 +191,28 @@ class RequestEditorState extends State {
}
}
+typedef ParamCallback = void Function(String param);
+
+class UrlQueryNotifier {
+ ParamCallback? _urlNotifier;
+ ParamCallback? _paramNotifier;
+
+ urlListener(ParamCallback listener) => _urlNotifier = listener;
+
+ paramListener(ParamCallback listener) => _paramNotifier = listener;
+
+ onUrlChange(String url) => _urlNotifier?.call(url);
+
+ onParamChange(String param) => _paramNotifier?.call(param);
+}
+
class _HttpWidget extends StatefulWidget {
final HttpMessage? message;
final bool readOnly;
final Widget title;
+ final UrlQueryNotifier? urlQueryNotifier;
- const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title});
+ const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title, this.urlQueryNotifier});
@override
State createState() {
@@ -196,25 +221,46 @@ class _HttpWidget extends StatefulWidget {
}
class _HttpState extends State<_HttpWidget> {
- final tabs = ['Header', 'Body'];
- final headerKey = GlobalKey();
- String? body;
+ List tabs = ['Header', 'Body'];
+ final headerKey = GlobalKey();
+ Map> initHeader = {};
+ HttpMessage? message;
+ TextEditingController? body;
AppLocalizations get localizations => AppLocalizations.of(context)!;
String? getBody() {
- return body;
+ return body?.text;
}
HttpHeaders? getHeaders() {
- return headerKey.currentState?.getHeaders();
+ return HttpHeaders.fromJson(headerKey.currentState?.getParams() ?? {});
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.urlQueryNotifier != null) {
+ tabs.insert(0, "URL Params");
+ }
+
+ message = widget.message;
+ body = TextEditingController(text: widget.message?.bodyAsString);
+ if (widget.message?.headers == null && !widget.readOnly) {
+ initHeader["User-Agent"] = ["ProxyPin/1.0.8"];
+ initHeader["Accept"] = ["*/*"];
+ return;
+ }
+ }
+
+ change(HttpMessage message) {
+ this.message = message;
+ body?.text = message.bodyAsString;
+ headerKey.currentState?.refreshParam(message.headers.getHeaders());
}
@override
Widget build(BuildContext context) {
- body = widget.message?.bodyAsString;
- headerKey.currentState?.refreshHeader(widget.message?.headers);
-
if (widget.message == null && widget.readOnly) {
return Scaffold(appBar: AppBar(title: widget.title), body: Center(child: Text(localizations.emptyData)));
}
@@ -236,7 +282,16 @@ class _HttpState extends State<_HttpWidget> {
padding: const EdgeInsets.only(left: 10),
child: TabBarView(
children: [
- Headers(key: headerKey, headers: widget.message?.headers, readOnly: widget.readOnly),
+ if (tabs.length == 3)
+ KeyValWidget(
+ paramNotifier: widget.urlQueryNotifier,
+ params: message is HttpRequest
+ ? (message as HttpRequest).requestUri?.queryParametersAll
+ : null),
+ KeyValWidget(
+ key: headerKey,
+ params: message?.headers.getHeaders() ?? initHeader,
+ readOnly: widget.readOnly),
_body()
],
)),
@@ -246,26 +301,19 @@ class _HttpState extends State<_HttpWidget> {
Widget _body() {
if (widget.readOnly) {
return KeepAliveWrapper(
- child: SingleChildScrollView(child: HttpBodyWidget(httpMessage: widget.message, hideRequestRewrite: true)));
+ child: SingleChildScrollView(child: HttpBodyWidget(httpMessage: message, hideRequestRewrite: true)));
}
- return TextField(
- autofocus: true,
- controller: TextEditingController(text: body),
- readOnly: widget.readOnly,
- onChanged: (value) {
- body = value;
- },
- minLines: 20,
- maxLines: 20);
+ return TextFormField(autofocus: true, controller: body, readOnly: widget.readOnly, minLines: 20, maxLines: 20);
}
}
///请求行
class _RequestLine extends StatefulWidget {
final HttpRequest? request;
+ final UrlQueryNotifier? urlQueryNotifier;
- const _RequestLine({super.key, this.request});
+ const _RequestLine({super.key, this.request, this.urlQueryNotifier});
@override
State createState() {
@@ -274,30 +322,56 @@ class _RequestLine extends StatefulWidget {
}
class _RequestLineState extends State<_RequestLine> {
- String requestUrl = "";
String requestMethod = HttpMethod.get.name;
+ TextEditingController requestUrl = TextEditingController(text: "");
@override
void initState() {
super.initState();
+ widget.urlQueryNotifier?.paramListener((param) => onQueryChange(param));
if (widget.request == null) {
- requestUrl = 'https://';
+ requestUrl.text = 'https://';
return;
}
var request = widget.request!;
- requestUrl = request.requestUrl;
+ requestUrl.text = request.requestUrl;
requestMethod = request.method.name;
}
+ @override
+ dispose() {
+ requestUrl.dispose();
+ super.dispose();
+ }
+
change(String? requestUrl, String? requestMethod) {
- this.requestUrl = requestUrl ?? this.requestUrl;
+ this.requestUrl.text = requestUrl ?? this.requestUrl.text;
this.requestMethod = requestMethod ?? this.requestMethod;
+
+ urlNotifier();
+ }
+
+ urlNotifier() {
+ var splitFirst = requestUrl.text.splitFirst("?".codeUnits.first);
+ widget.urlQueryNotifier?.onUrlChange(splitFirst.length > 1 ? splitFirst.last : '');
+ }
+
+ onQueryChange(String query) {
+ var url = requestUrl.text;
+ var indexOf = url.indexOf("?");
+ if (indexOf == -1) {
+ requestUrl.text = "$url?$query";
+ } else {
+ requestUrl.text = "${url.substring(0, indexOf)}?$query";
+ }
+ setState(() {});
}
@override
Widget build(BuildContext context) {
return TextField(
+ controller: requestUrl,
decoration: InputDecoration(
prefix: DropdownButton(
padding: const EdgeInsets.only(right: 10),
@@ -315,28 +389,34 @@ class _RequestLineState extends State<_RequestLine> {
isDense: true,
border: const OutlineInputBorder(borderSide: BorderSide()),
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.grey, width: 0.3))),
- controller: TextEditingController(text: requestUrl),
onChanged: (value) {
- requestUrl = value;
+ urlNotifier();
});
}
}
-///请求头
-class Headers extends StatefulWidget {
- final HttpHeaders? headers;
- final bool readOnly; //只读
+class KeyVal {
+ bool enabled = true;
+ TextEditingController key;
+ TextEditingController value;
- const Headers({super.key, this.headers, this.readOnly = false});
-
- @override
- State createState() {
- return HeadersState();
- }
+ KeyVal(this.key, this.value);
}
-class HeadersState extends State with AutomaticKeepAliveClientMixin {
- final Map> _headers = {};
+///key value
+class KeyValWidget extends StatefulWidget {
+ final Map>? params;
+ final bool readOnly; //只读
+ final UrlQueryNotifier? paramNotifier;
+
+ const KeyValWidget({super.key, this.params, this.readOnly = false, this.paramNotifier});
+
+ @override
+ State createState() => KeyValState();
+}
+
+class KeyValState extends State with AutomaticKeepAliveClientMixin {
+ final List _params = [];
AppLocalizations get localizations => AppLocalizations.of(context)!;
@@ -346,40 +426,91 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
- if (widget.headers == null && !widget.readOnly) {
- _headers[TextEditingController(text: "User-Agent")] = [TextEditingController(text: "ProxyPin/1.0.8")];
- _headers[TextEditingController(text: "Accept")] = [TextEditingController(text: "*/*")];
+ widget.paramNotifier?.urlListener((url) => onChange(url));
+ if (widget.params == null) {
+ var keyVal = KeyVal(TextEditingController(), TextEditingController());
+ _params.add(keyVal);
return;
}
- widget.headers?.forEach((name, values) {
- _headers[TextEditingController(text: name)] = values.map((it) => TextEditingController(text: it)).toList();
+
+ widget.params?.forEach((name, values) {
+ for (var val in values) {
+ var keyVal = KeyVal(TextEditingController(text: name), TextEditingController(text: val));
+ _params.add(keyVal);
+ }
});
}
- //刷新header
- refreshHeader(HttpHeaders? headers) {
- _headers.clear();
+ @override
+ dispose() {
+ clear();
+ super.dispose();
+ }
+
+ //监听url发生变化 更改表单
+ onChange(String value) {
+ var query = value.split("&");
+ int index = 0;
+ while (index < query.length) {
+ var splitFirst = query[index].splitFirst('='.codeUnits.first);
+ String key = splitFirst.first;
+ String? val = splitFirst.length == 1 ? null : splitFirst.last;
+ if (_params.length <= index) {
+ _params.add(KeyVal(TextEditingController(text: key), TextEditingController(text: val)));
+ continue;
+ }
+
+ var keyVal = _params[index++];
+ keyVal.key.text = key;
+ keyVal.value.text = val ?? '';
+ }
+
+ _params.length = index;
+ setState(() {});
+ }
+
+ notifierChange() {
+ if (widget.paramNotifier == null) return;
+ String query = _params
+ .where((e) => e.enabled && e.key.text.isNotEmpty)
+ .map((e) => "${e.key.text}=${e.value.text}".replaceAll("&", "%26"))
+ .join("&");
+ widget.paramNotifier?.onParamChange(query);
+ }
+
+ clear() {
+ for (var element in _params) {
+ element.key.dispose();
+ element.value.dispose();
+ }
+ _params.clear();
+ }
+
+ //刷新param
+ refreshParam(Map>? headers) {
+ clear();
setState(() {
headers?.forEach((name, values) {
- _headers[TextEditingController(text: name)] = values.map((it) => TextEditingController(text: it)).toList();
+ for (var val in values) {
+ var keyVal = KeyVal(TextEditingController(text: name), TextEditingController(text: val));
+ _params.add(keyVal);
+ }
});
});
}
///获取所有请求头
- HttpHeaders getHeaders() {
- var headers = HttpHeaders();
- _headers.forEach((name, values) {
- if (name.text.isEmpty) {
- return;
+ Map> getParams() {
+ Map> map = {};
+ for (var keVal in _params) {
+ if (keVal.key.text.isEmpty || !keVal.enabled) {
+ continue;
}
- for (var element in values) {
- if (element.text.isNotEmpty) {
- headers.add(name.text, element.text);
- }
- }
- });
- return headers;
+ map[keVal.key.text] ??= [];
+ map[keVal.key.text]!.add(keVal.value.text);
+ }
+
+ return map;
}
@override
@@ -387,16 +518,20 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin {
super.build(context);
var list = [
- _row(const Text('Key'), const Text('Value'), const Text('')),
+ const Row(children: [
+ SizedBox(width: 38),
+ Expanded(flex: 4, child: Text('Key')),
+ Expanded(flex: 5, child: Text('Value'))
+ ]),
..._buildRows(),
];
if (!widget.readOnly) {
list.add(TextButton(
- child: Text("${localizations.add}Header", textAlign: TextAlign.center),
+ child: Text(localizations.add, textAlign: TextAlign.center),
onPressed: () {
setState(() {
- _headers[TextEditingController()] = [TextEditingController()];
+ _params.add(KeyVal(TextEditingController(), TextEditingController()));
});
},
));
@@ -413,25 +548,24 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin {
List _buildRows() {
List list = [];
-
- _headers.forEach((key, values) {
- for (var val in values) {
- list.add(_row(
- _cell(key, isKey: true),
- _cell(val),
- widget.readOnly
- ? null
- : Padding(
- padding: const EdgeInsets.only(right: 15),
- child: InkWell(
- onTap: () {
- setState(() {
- _headers.remove(key);
- });
- },
- child: const Icon(Icons.remove_circle, size: 16)))));
- }
- });
+ for (var keyVal in _params) {
+ list.add(_row(
+ keyVal,
+ widget.readOnly
+ ? null
+ : Padding(
+ padding: const EdgeInsets.only(right: 15),
+ child: InkWell(
+ onTap: () {
+ setState(() {
+ _params.remove(keyVal);
+ keyVal.key.dispose();
+ keyVal.value.dispose();
+ });
+ notifierChange();
+ },
+ child: const Icon(Icons.remove_circle, size: 16)))));
+ }
return list;
}
@@ -441,14 +575,39 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin {
padding: const EdgeInsets.only(right: 5),
child: TextFormField(
readOnly: widget.readOnly,
- style: TextStyle(fontSize: 12, fontWeight: isKey ? FontWeight.w500 : null),
+ style: TextStyle(fontSize: 13, fontWeight: isKey ? FontWeight.w500 : null),
controller: val,
+ onChanged: (val) => notifierChange(),
minLines: 1,
maxLines: 3,
- decoration: InputDecoration(isDense: true, border: InputBorder.none, hintText: isKey ? "Key" : "Value")));
+ decoration: InputDecoration(
+ isDense: true,
+ hintStyle: const TextStyle(color: Colors.grey),
+ contentPadding: const EdgeInsets.fromLTRB(5, 13, 5, 13),
+ focusedBorder: widget.readOnly
+ ? null
+ : OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 1.5)),
+ border: InputBorder.none,
+ hintText: isKey ? "Key" : "Value")));
}
- Widget _row(Widget key, Widget val, Widget? op) {
- return Row(children: [Expanded(flex: 4, child: key), Expanded(flex: 6, child: val), op ?? const SizedBox()]);
+ Widget _row(KeyVal keyVal, Widget? op) {
+ return Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
+ if (op != null)
+ Checkbox(
+ value: keyVal.enabled,
+ onChanged: (val) {
+ setState(() {
+ keyVal.enabled = val!;
+ });
+ notifierChange();
+ }),
+ Container(width: 5),
+ Expanded(flex: 4, child: _cell(keyVal.key, isKey: true)),
+ const Text(":", style: TextStyle(color: Colors.deepOrangeAccent)),
+ const SizedBox(width: 8),
+ Expanded(flex: 6, child: _cell(keyVal.value)),
+ op ?? const SizedBox()
+ ]);
}
}
diff --git a/lib/ui/desktop/left/search.dart b/lib/ui/desktop/left/search.dart
index d45bfc3..7848b92 100644
--- a/lib/ui/desktop/left/search.dart
+++ b/lib/ui/desktop/left/search.dart
@@ -4,6 +4,8 @@ import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/ui/desktop/left/model/search_model.dart';
import 'package:network_proxy/ui/desktop/left/search_condition.dart';
+/// @author wanghongen
+/// 2023/10/8
class Search extends StatefulWidget {
final Function(SearchModel searchModel)? onSearch;
diff --git a/lib/ui/desktop/preference.dart b/lib/ui/desktop/preference.dart
index a741d16..e26f5b6 100644
--- a/lib/ui/desktop/preference.dart
+++ b/lib/ui/desktop/preference.dart
@@ -3,6 +3,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/ui/configuration.dart';
+/// @author wanghongen
+/// 2024/1/2
class Preference extends StatelessWidget {
final AppConfiguration appConfiguration;
diff --git a/lib/ui/desktop/toolbar/phone_connect.dart b/lib/ui/desktop/toolbar/phone_connect.dart
index e12b990..4fe4c68 100644
--- a/lib/ui/desktop/toolbar/phone_connect.dart
+++ b/lib/ui/desktop/toolbar/phone_connect.dart
@@ -3,6 +3,8 @@ import 'package:network_proxy/network/bin/server.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+/// @author wanghongen
+/// 2023/10/8
class PhoneConnect extends StatefulWidget {
final ProxyServer proxyServer;
final List hosts;
diff --git a/lib/ui/desktop/toolbar/setting/external_proxy.dart b/lib/ui/desktop/toolbar/setting/external_proxy.dart
index e5b9657..4a964c8 100644
--- a/lib/ui/desktop/toolbar/setting/external_proxy.dart
+++ b/lib/ui/desktop/toolbar/setting/external_proxy.dart
@@ -6,6 +6,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:network_proxy/network/bin/configuration.dart';
import 'package:network_proxy/network/host_port.dart';
+/// @author wanghongen
+/// 2023/10/8
class ExternalProxyDialog extends StatefulWidget {
final Configuration configuration;
diff --git a/lib/ui/desktop/toolbar/setting/filter.dart b/lib/ui/desktop/toolbar/setting/filter.dart
index 9ca54f2..942e25f 100644
--- a/lib/ui/desktop/toolbar/setting/filter.dart
+++ b/lib/ui/desktop/toolbar/setting/filter.dart
@@ -3,6 +3,8 @@ 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';
+/// @author wanghongen
+/// 2023/10/8
class FilterDialog extends StatefulWidget {
final Configuration configuration;
diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart
index 6930534..150b22e 100644
--- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart
+++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart
@@ -16,6 +16,8 @@ import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart';
import 'package:network_proxy/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart';
+/// @author wanghongen
+/// 2023/10/8
class RequestRewriteWidget extends StatefulWidget {
final int windowId;
final RequestRewrites requestRewrites;
@@ -263,7 +265,7 @@ class _RequestRuleListState extends State {
}
List rows(List list) {
- var primaryColor = Theme.of(context).primaryColor;
+ var primaryColor = Theme.of(context).colorScheme.primary;
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
return List.generate(list.length, (index) {
diff --git a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart
index 213b6a2..4bdfbf4 100644
--- a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart
+++ b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart
@@ -10,6 +10,8 @@ import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/utils/lang.dart';
/// 重写替换
+/// @author wanghongen
+/// 2023/10/8
class RewriteReplaceDialog extends StatefulWidget {
final String subtitle;
final RuleType ruleType;
diff --git a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart
index 495fa3c..c8e3ddb 100644
--- a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart
+++ b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart
@@ -5,6 +5,8 @@ import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+/// @author wanghongen
+/// 2023/10/8
class RewriteUpdateDialog extends StatefulWidget {
final String subtitle;
final RuleType ruleType;
diff --git a/lib/ui/desktop/toolbar/setting/script.dart b/lib/ui/desktop/toolbar/setting/script.dart
index 3d7b9ad..13bdd18 100644
--- a/lib/ui/desktop/toolbar/setting/script.dart
+++ b/lib/ui/desktop/toolbar/setting/script.dart
@@ -47,6 +47,8 @@ void _refreshScript() {
});
}
+/// @author wanghongen
+/// 2023/10/8
class ScriptWidget extends StatefulWidget {
final int windowId;
diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart
index 00b9c70..dd8d6d5 100644
--- a/lib/ui/desktop/toolbar/setting/setting.dart
+++ b/lib/ui/desktop/toolbar/setting/setting.dart
@@ -15,6 +15,8 @@ import 'package:url_launcher/url_launcher.dart';
import 'filter.dart';
///设置菜单
+/// @author wanghongen
+/// 2023/10/8
class Setting extends StatefulWidget {
final ProxyServer proxyServer;
diff --git a/lib/ui/desktop/toolbar/setting/theme.dart b/lib/ui/desktop/toolbar/setting/theme.dart
index 9571055..ae4a550 100644
--- a/lib/ui/desktop/toolbar/setting/theme.dart
+++ b/lib/ui/desktop/toolbar/setting/theme.dart
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:network_proxy/ui/configuration.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+/// @author wanghongen
+/// 2023/6/17
class ThemeSetting extends StatelessWidget {
final AppConfiguration appConfiguration;
diff --git a/lib/ui/desktop/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart
index 777a676..2fa2850 100644
--- a/lib/ui/desktop/toolbar/toolbar.dart
+++ b/lib/ui/desktop/toolbar/toolbar.dart
@@ -12,7 +12,8 @@ import 'package:window_manager/window_manager.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../left/list.dart';
-
+/// @author wanghongen
+/// 2023/10/8
class Toolbar extends StatefulWidget {
final ProxyServer proxyServer;
final GlobalKey domainStateKey;
diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart
index 8ff9c81..90308e2 100644
--- a/lib/ui/mobile/mobile.dart
+++ b/lib/ui/mobile/mobile.dart
@@ -207,10 +207,12 @@ class MobileHomeState extends State implements EventListener, Li
'1. 增加多语言支持;\n'
'2. 请求重写支持文件选择;\n'
'3. 抓包详情页面Headers默认展开配置;\n'
+ '4. 请求编辑URL参数支持表单编辑;\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';
+ '3. Request Edit URL parameter support for form editing;\n';
showAlertDialog(isCN ? '更新内容V1.0.7' : "Update content V1.0.7", content, () {
widget.appConfiguration.upgradeNoticeV7 = false;
widget.appConfiguration.flushConfig();
diff --git a/lib/ui/mobile/request/request_editor.dart b/lib/ui/mobile/request/request_editor.dart
index 051374c..f6cf099 100644
--- a/lib/ui/mobile/request/request_editor.dart
+++ b/lib/ui/mobile/request/request_editor.dart
@@ -12,7 +12,9 @@ import 'package:network_proxy/network/http/http_headers.dart';
import 'package:network_proxy/network/http_client.dart';
import 'package:network_proxy/ui/content/body.dart';
import 'package:network_proxy/utils/curl.dart';
+import 'package:network_proxy/utils/lang.dart';
+/// @author wanghongen
class MobileRequestEditor extends StatefulWidget {
final HttpRequest? request;
final ProxyServer? proxyServer;
@@ -26,6 +28,7 @@ class MobileRequestEditor extends StatefulWidget {
}
class RequestEditorState extends State with SingleTickerProviderStateMixin {
+ final UrlQueryNotifier _queryNotifier = UrlQueryNotifier();
final requestLineKey = GlobalKey<_RequestLineState>();
final requestKey = GlobalKey<_HttpState>();
final responseKey = GlobalKey<_HttpState>();
@@ -120,23 +123,30 @@ class RequestEditorState extends State with SingleTickerPro
TextButton.icon(icon: const Icon(Icons.send), label: Text(localizations.send), onPressed: sendRequest)
],
bottom: TabBar(controller: tabController, tabs: tabs)),
- body: TabBarView(
- controller: tabController,
- children: [
- _HttpWidget(title: _RequestLine(request: request, key: requestLineKey), message: request, key: requestKey),
- ValueListenableBuilder(
- valueListenable: responseChange,
- builder: (_, value, __) => _HttpWidget(
- key: responseKey,
- title: Row(children: [
- Text("${localizations.statusCode}: ", style: const TextStyle(fontWeight: FontWeight.w500)),
- const SizedBox(width: 10),
- Text(response?.status.toString() ?? "", style: const TextStyle(color: Colors.blue))
- ]),
- readOnly: true,
- message: response)),
- ],
- ));
+ body: GestureDetector(
+ onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
+ child: TabBarView(
+ controller: tabController,
+ children: [
+ _HttpWidget(
+ title: _RequestLine(request: request, key: requestLineKey, urlQueryNotifier: _queryNotifier),
+ message: request,
+ key: requestKey,
+ urlQueryNotifier: _queryNotifier,
+ ),
+ ValueListenableBuilder(
+ valueListenable: responseChange,
+ builder: (_, value, __) => _HttpWidget(
+ key: responseKey,
+ title: Row(children: [
+ Text("${localizations.statusCode}: ", style: const TextStyle(fontWeight: FontWeight.w500)),
+ const SizedBox(width: 10),
+ Text(response?.status.toString() ?? "", style: const TextStyle(color: Colors.blue))
+ ]),
+ readOnly: true,
+ message: response)),
+ ],
+ )));
}
///发送请求
@@ -146,7 +156,7 @@ class RequestEditorState extends State with SingleTickerPro
var requestBody = requestKey.currentState?.getBody();
HttpRequest request =
- HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.encodeFull(currentState.requestUrl));
+ HttpRequest(HttpMethod.valueOf(currentState.requestMethod), Uri.encodeFull(currentState.requestUrl.text));
request.headers.addAll(headers);
request.body = requestBody == null ? null : utf8.encode(requestBody);
@@ -165,12 +175,28 @@ class RequestEditorState extends State with SingleTickerPro
}
}
+typedef ParamCallback = void Function(String param);
+
+class UrlQueryNotifier {
+ ParamCallback? _urlNotifier;
+ ParamCallback? _paramNotifier;
+
+ urlListener(ParamCallback listener) => _urlNotifier = listener;
+
+ paramListener(ParamCallback listener) => _paramNotifier = listener;
+
+ onUrlChange(String url) => _urlNotifier?.call(url);
+
+ onParamChange(String param) => _paramNotifier?.call(param);
+}
+
class _HttpWidget extends StatefulWidget {
final HttpMessage? message;
final bool readOnly;
final Widget title;
+ final UrlQueryNotifier? urlQueryNotifier;
- const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title});
+ const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title, this.urlQueryNotifier});
@override
State createState() {
@@ -179,7 +205,8 @@ class _HttpWidget extends StatefulWidget {
}
class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
- final headerKey = GlobalKey();
+ final headerKey = GlobalKey();
+ Map> initHeader = {};
HttpMessage? message;
String? body;
@@ -197,16 +224,21 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
super.initState();
message = widget.message;
body = widget.message?.bodyAsString;
+ if (widget.message?.headers == null && !widget.readOnly) {
+ initHeader["User-Agent"] = ["ProxyPin/1.0.8"];
+ initHeader["Accept"] = ["*/*"];
+ return;
+ }
}
change(HttpMessage message) {
this.message = message;
body = message.bodyAsString;
- headerKey.currentState?.refreshHeader(message.headers);
+ headerKey.currentState?.refreshParam(message.headers.getHeaders());
}
HttpHeaders? getHeaders() {
- return headerKey.currentState?.getHeaders();
+ return HttpHeaders.fromJson(headerKey.currentState?.getParams() ?? {});
}
@override
@@ -221,7 +253,17 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
padding: const EdgeInsets.all(15),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
widget.title,
- Headers(headers: message?.headers, key: headerKey, readOnly: widget.readOnly), // 请求头
+ if (widget.urlQueryNotifier != null)
+ KeyValWidget(
+ title: 'URL${localizations.param}',
+ paramNotifier: widget.urlQueryNotifier,
+ params: message is HttpRequest ? (message as HttpRequest).requestUri?.queryParametersAll : null),
+ KeyValWidget(
+ title: "Headers",
+ params: message?.headers.getHeaders() ?? initHeader,
+ key: headerKey,
+ readOnly: widget.readOnly),
+ // 请求头
const SizedBox(height: 10),
const Text("Body", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),
_body(),
@@ -237,9 +279,7 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
return TextField(
controller: TextEditingController(text: body),
readOnly: widget.readOnly,
- onChanged: (value) {
- body = value;
- },
+ onChanged: (value) => body = value,
minLines: 3,
maxLines: 15);
}
@@ -247,8 +287,9 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
class _RequestLine extends StatefulWidget {
final HttpRequest? request;
+ final UrlQueryNotifier? urlQueryNotifier;
- const _RequestLine({this.request, super.key});
+ const _RequestLine({this.request, super.key, this.urlQueryNotifier});
@override
State createState() {
@@ -257,32 +298,60 @@ class _RequestLine extends StatefulWidget {
}
class _RequestLineState extends State<_RequestLine> {
- String requestUrl = "";
+ TextEditingController requestUrl = TextEditingController(text: "");
String requestMethod = HttpMethod.get.name;
@override
void initState() {
super.initState();
+ widget.urlQueryNotifier?.paramListener((param) => onQueryChange(param));
if (widget.request == null) {
- requestUrl = 'https://';
+ requestUrl.text = 'https://';
return;
}
var request = widget.request!;
- requestUrl = request.requestUrl;
+ requestUrl.text = request.requestUrl;
requestMethod = request.method.name;
}
+ @override
+ dispose() {
+ requestUrl.dispose();
+ super.dispose();
+ }
+
change(String? requestUrl, String? requestMethod) {
- this.requestUrl = requestUrl ?? this.requestUrl;
+ this.requestUrl.text = requestUrl ?? this.requestUrl.text;
this.requestMethod = requestMethod ?? this.requestMethod;
+
+ urlNotifier();
+ }
+
+ urlNotifier() {
+ var splitFirst = requestUrl.text.splitFirst("?".codeUnits.first);
+ widget.urlQueryNotifier?.onUrlChange(splitFirst.length > 1 ? splitFirst.last : '');
+ }
+
+ onQueryChange(String query) {
+ var url = requestUrl.text;
+ var indexOf = url.indexOf("?");
+ if (indexOf == -1) {
+ requestUrl.text = "$url?$query";
+ } else {
+ requestUrl.text = "${url.substring(0, indexOf)}?$query";
+ }
+ setState(() {});
}
@override
Widget build(BuildContext context) {
+ TextInput;
return TextField(
style: const TextStyle(fontSize: 14),
minLines: 1,
maxLines: 5,
+ autofocus: false,
+ controller: requestUrl,
decoration: InputDecoration(
prefix: DropdownButton(
padding: const EdgeInsets.only(right: 10),
@@ -295,160 +364,206 @@ class _RequestLineState extends State<_RequestLine> {
DropdownMenuItem(value: it.name, child: Text(it.name, style: const TextStyle(fontSize: 12))))
.toList(),
onChanged: (String? value) {
- setState(() {
- requestMethod = value!;
- });
+ setState(() => requestMethod = value!);
},
),
isDense: true,
border: const OutlineInputBorder(borderSide: BorderSide()),
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.grey, width: 0.3))),
- controller: TextEditingController(text: requestUrl),
onChanged: (value) {
- requestUrl = value;
+ urlNotifier();
});
}
}
-class Headers extends StatefulWidget {
- final HttpHeaders? headers;
- final bool readOnly; //只读
+class KeyVal {
+ bool enabled = true;
+ String key;
+ String value;
- const Headers({super.key, this.headers, required this.readOnly});
+ KeyVal(this.key, this.value);
+}
+
+///key value
+class KeyValWidget extends StatefulWidget {
+ final String title;
+ final Map>? params;
+ final bool readOnly; //只读
+ final UrlQueryNotifier? paramNotifier;
+
+ const KeyValWidget({super.key, this.params, this.readOnly = false, this.paramNotifier, required this.title});
@override
State createState() {
- return HeadersState();
+ return KeyValState();
}
}
-class HeadersState extends State {
- Map> headers = {};
+class KeyValState extends State {
+ final List _params = [];
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
void initState() {
super.initState();
- if (widget.headers == null && !widget.readOnly) {
- headers["User-Agent"] = ["ProxyPin/1.0.2"];
- headers["Accept"] = ["*/*"];
- return;
- }
- widget.headers?.forEach((name, values) {
- headers[name] = values;
- });
- }
-
- HttpHeaders getHeaders() {
- var headers = HttpHeaders();
- this.headers.forEach((key, values) {
- if (key.isNotEmpty) {
- headers.addValues(key, values);
+ widget.params?.forEach((name, values) {
+ for (var val in values) {
+ var keyVal = KeyVal(name, val);
+ _params.add(keyVal);
}
});
- return headers;
+
+ widget.paramNotifier?.urlListener((url) => onChange(url));
}
- //刷新header
- refreshHeader(HttpHeaders? headers) {
- this.headers.clear();
- headers?.forEach((name, values) {
- this.headers[name] = values;
+ //监听url发生变化 更改表单
+ onChange(String value) {
+ print("onChange $value");
+ var query = value.split("&");
+ int index = 0;
+ while (index < query.length) {
+ var splitFirst = query[index].splitFirst('='.codeUnits.first);
+ String key = splitFirst.first;
+ String? val = splitFirst.length == 1 ? null : splitFirst.last;
+ if (_params.length <= index) {
+ _params.add(KeyVal(key, val ?? ''));
+ continue;
+ }
+
+ var keyVal = _params[index++];
+ keyVal.key = key;
+ keyVal.value = val ?? '';
+ }
+
+ _params.length = index;
+ setState(() {});
+ }
+
+ notifierChange() {
+ if (widget.paramNotifier == null) return;
+ String query = _params
+ .where((e) => e.enabled && e.key.isNotEmpty)
+ .map((e) => "${e.key}=${e.value}".replaceAll("&", "%26"))
+ .join("&");
+ widget.paramNotifier?.onParamChange(query);
+ }
+
+ ///获取所有请求头
+ Map> getParams() {
+ Map> map = {};
+ for (var keVal in _params) {
+ if (keVal.key.isEmpty || !keVal.enabled) {
+ continue;
+ }
+ map[keVal.key] ??= [];
+ map[keVal.key]!.add(keVal.value);
+ }
+
+ return map;
+ }
+
+ //刷新param
+ refreshParam(Map>? headers) {
+ _params.clear();
+ setState(() {
+ headers?.forEach((name, values) {
+ for (var val in values) {
+ _params.add(KeyVal(name, val));
+ }
+ });
});
}
@override
Widget build(BuildContext context) {
return ExpansionTile(
- title: const Text("Headers", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),
+ title: Text(widget.title, style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),
tilePadding: const EdgeInsets.only(left: 0, top: 10, bottom: 10),
initiallyExpanded: true,
shape: const Border(),
children: [
- ...buildHeaders(),
+ ..._buildRows(),
widget.readOnly
? const SizedBox()
: Container(
alignment: Alignment.center,
child: TextButton(
onPressed: () {
- modifyHeader("", "");
+ var keyVal = KeyVal("", "");
+ _params.add(keyVal);
+ modifyParam(keyVal);
},
- child: Text("${localizations.add}Header", textAlign: TextAlign.center))) //添加按钮
+ child: Text(localizations.add, textAlign: TextAlign.center))) //添加按钮
],
);
}
- List buildHeaders() {
+ List _buildRows() {
List list = [];
- headers.forEach((key, values) {
- for (var val in values) {
- var header = row(Text(key, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
- Text(val, style: const TextStyle(fontSize: 12), maxLines: 5, overflow: TextOverflow.ellipsis));
- Widget headerWidget = Padding(padding: const EdgeInsets.only(top: 5, bottom: 5), child: header);
- if (!widget.readOnly) {
- headerWidget =
- InkWell(onTap: () => modifyHeader(key, val), onLongPress: () => deleteHeader(key), child: headerWidget);
- }
-
- list.add(headerWidget);
- list.add(const Divider(thickness: 0.2));
+ for (var element in _params) {
+ Widget headerWidget = Padding(padding: const EdgeInsets.only(top: 5, bottom: 5), child: row(element));
+ if (!widget.readOnly) {
+ headerWidget =
+ InkWell(onTap: () => modifyParam(element), onLongPress: () => deleteHeader(element), child: headerWidget);
}
- });
+
+ list.add(headerWidget);
+ list.add(const Divider(thickness: 0.2));
+ }
+
return list;
}
- /// 修改请求头
- modifyHeader(String key, String val) {
- bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
+ //隐藏输入框焦点
+ void hideKeyword(BuildContext context) {
+ FocusScopeNode currentFocus = FocusScope.of(context);
+ if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
+ currentFocus.focusedChild?.unfocus();
+ }
+ }
- String headerName = key;
+ /// 修改请求头
+ modifyParam(KeyVal keyVal) {
+ //隐藏输入框焦点
+ hideKeyword(context);
+ String headerName = keyVal.key;
+ String val = keyVal.value;
showDialog(
context: context,
builder: (ctx) {
return AlertDialog(
titlePadding: const EdgeInsets.only(left: 25, top: 10),
actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),
- title: Text(isCN ? "修改请求头" : "Modify Header", style: const TextStyle(fontSize: 18)),
+ title: Text(localizations.modifyRequestHeader, style: const TextStyle(fontSize: 18)),
content: Wrap(
children: [
- TextField(
+ TextFormField(
minLines: 1,
maxLines: 3,
- controller: TextEditingController(text: headerName),
- decoration: InputDecoration(labelText: isCN ? "请求头名称" : "Header Name"),
- onChanged: (value) {
- headerName = value;
- },
+ initialValue: headerName,
+ decoration: InputDecoration(labelText: localizations.headerName),
+ onChanged: (value) => headerName = value,
),
- TextField(
+ TextFormField(
minLines: 1,
maxLines: 8,
- controller: TextEditingController(text: val),
- decoration: InputDecoration(labelText: isCN ? "请求头值" : "Header Value"),
- onChanged: (value) {
- val = value;
- },
+ initialValue: val,
+ decoration: InputDecoration(labelText: localizations.value),
+ onChanged: (value) => val = value,
)
],
),
actions: [
- TextButton(
- onPressed: () {
- Navigator.pop(context);
- },
- child: Text(localizations.cancel)),
+ TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
TextButton(
onPressed: () {
setState(() {
- if (headerName != key) {
- headers.remove(key);
- }
-
- headers[headerName] = [val];
+ keyVal.key = headerName;
+ keyVal.value = val;
});
+ notifierChange();
Navigator.pop(context);
},
child: Text(localizations.modify)),
@@ -458,26 +573,18 @@ class HeadersState extends State {
}
//删除
- deleteHeader(String key) {
- bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
-
+ deleteHeader(KeyVal keyVal) {
showDialog(
context: context,
builder: (ctx) {
return AlertDialog(
- title: Text(isCN ? "是否删除该请求头?" : "Do you want to delete the request header?",
- style: const TextStyle(fontSize: 18)),
+ title: Text(localizations.deleteHeaderConfirm, style: const TextStyle(fontSize: 18)),
actions: [
+ TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
TextButton(
onPressed: () {
- Navigator.pop(context);
- },
- child: Text(localizations.cancel)),
- TextButton(
- onPressed: () {
- setState(() {
- headers.remove(key);
- });
+ setState(() => _params.remove(keyVal));
+ notifierChange();
Navigator.pop(context);
},
child: Text(localizations.delete)),
@@ -486,13 +593,23 @@ class HeadersState extends State {
});
}
- Widget row(Widget title, Widget child) {
+ Widget row(KeyVal keyVal) {
return Row(children: [
- Expanded(flex: 3, child: title),
- const SizedBox(width: 10, child: Text(":", style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w600))),
+ if (!widget.readOnly)
+ Checkbox(
+ value: keyVal.enabled,
+ onChanged: (val) {
+ setState(() {
+ keyVal.enabled = val!;
+ });
+ notifierChange();
+ }),
+ Expanded(flex: 4, child: Text(keyVal.key, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))),
+ const Text(":", style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w600)),
+ const SizedBox(width: 8),
Expanded(
flex: 6,
- child: child,
+ child: Text(keyVal.value, style: const TextStyle(fontSize: 13), maxLines: 5, overflow: TextOverflow.ellipsis),
),
]);
}
diff --git a/lib/ui/mobile/setting/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart
index 24d5ede..85ee628 100644
--- a/lib/ui/mobile/setting/request_rewrite.dart
+++ b/lib/ui/mobile/setting/request_rewrite.dart
@@ -196,7 +196,7 @@ class _RequestRuleListState extends State {
child: Center(
child: TextButton(
onPressed: () {},
- child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
+ child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
TextButton.icon(
onPressed: () {
export(selected.toList());
@@ -206,13 +206,11 @@ class _RequestRuleListState extends State {
});
},
icon: const Icon(Icons.share, size: 18),
- label: Text(localizations.export)),
- const SizedBox(width: 15),
+ label: Text(localizations.export, style: const TextStyle(fontSize: 14))),
TextButton.icon(
onPressed: () => removeRewrite(),
icon: const Icon(Icons.delete, size: 18),
- label: Text(localizations.delete)),
- const SizedBox(width: 15),
+ label: Text(localizations.delete, style: const TextStyle(fontSize: 14))),
TextButton.icon(
onPressed: () {
setState(() {
@@ -221,13 +219,13 @@ class _RequestRuleListState extends State {
});
},
icon: const Icon(Icons.cancel, size: 18),
- label: Text(localizations.cancel)),
+ label: Text(localizations.cancel, style: const TextStyle(fontSize: 14))),
]))))
]);
}
List rows(List list) {
- var primaryColor = Theme.of(context).primaryColor;
+ var primaryColor = Theme.of(context).colorScheme.primary;
bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh');
return List.generate(list.length, (index) {
return InkWell(
diff --git a/lib/utils/lang.dart b/lib/utils/lang.dart
index 0fa2f14..b5901a0 100644
--- a/lib/utils/lang.dart
+++ b/lib/utils/lang.dart
@@ -1,6 +1,8 @@
import 'package:date_format/date_format.dart';
import 'package:flutter/material.dart';
+/// @author wanghongen
+/// 2023/10/8
extension ListFirstWhere on Iterable {
T? firstWhereOrNull(bool Function(T) test) {
try {
@@ -62,10 +64,20 @@ class Strings {
/// 当中英文混合,或者中文与数字或者特殊符号,或则英文单词时,文本会被自动换行,
/// 这样会导致,换行时上一行可能会留很大的空白区域
/// 把每个字符插入一个0宽的字符, \u{200B}
-extension FixAutoLines on String {
+extension StringEnhance on String {
String fixAutoLines() {
return Characters(this).join('\u{200B}');
}
+
+ List splitFirst(int code) {
+ var index = codeUnits.indexOf(code);
+ if (index == -1) {
+ return [this];
+ }
+ var key = substring(0, index).trim();
+ var value = substring(index + 1).trim();
+ return [key, value];
+ }
}
class Pair {
diff --git a/pubspec.yaml b/pubspec.yaml
index 1469668..d821c85 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,7 +2,7 @@ name: network_proxy
description: ProxyPin
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-version: 1.0.6+7
+version: 1.0.7+7
environment:
sdk: '>=3.0.2 <4.0.0'