From 10d2c51465ad34f62004d851e54aad9d0a30328b Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Thu, 4 Jan 2024 19:15:01 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=B7=E6=B1=82=E7=BC=96=E8=BE=91URL?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=94=AF=E6=8C=81=E8=A1=A8=E5=8D=95=E7=BC=96?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- README.md | 24 +- README_EN.md | 28 ++ l10n.yaml | 3 +- lib/l10n/app_en.arb | 6 +- lib/l10n/app_zh.arb | 10 +- lib/network/http/http_headers.dart | 4 + lib/ui/configuration.dart | 7 +- lib/ui/content/body.dart | 14 +- lib/ui/desktop/desktop.dart | 2 + lib/ui/desktop/left/favorite.dart | 2 + lib/ui/desktop/left/history.dart | 4 +- lib/ui/desktop/left/list.dart | 4 +- lib/ui/desktop/left/request.dart | 7 +- lib/ui/desktop/left/request_editor.dart | 345 +++++++++++----- lib/ui/desktop/left/search.dart | 2 + lib/ui/desktop/preference.dart | 2 + lib/ui/desktop/toolbar/phone_connect.dart | 2 + .../toolbar/setting/external_proxy.dart | 2 + lib/ui/desktop/toolbar/setting/filter.dart | 2 + .../toolbar/setting/request_rewrite.dart | 4 +- .../setting/rewrite/rewrite_replace.dart | 2 + .../setting/rewrite/rewrite_update.dart | 2 + lib/ui/desktop/toolbar/setting/script.dart | 2 + lib/ui/desktop/toolbar/setting/setting.dart | 2 + lib/ui/desktop/toolbar/setting/theme.dart | 2 + lib/ui/desktop/toolbar/toolbar.dart | 3 +- lib/ui/mobile/mobile.dart | 2 + lib/ui/mobile/request/request_editor.dart | 369 ++++++++++++------ lib/ui/mobile/setting/request_rewrite.dart | 12 +- lib/utils/lang.dart | 14 +- pubspec.yaml | 2 +- 32 files changed, 638 insertions(+), 252 deletions(-) create mode 100644 README_EN.md 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优化。 image. image 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. + +image. image + + 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'