diff --git a/README.md b/README.md index a55e0c9..92131cd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,6 @@ ios下载地址(Safari浏览器打开): https://testflight.apple.com/join/gURG - [ ] 支持安卓微信小程序抓包,安卓分为系统证书和用户证书,下载的自签名根证书安装都是用户证书,微信不信任用户证书,不Root导致Https抓不了了, 目前市场上所有抓包软件抓不了微信的包,后面单独做个运行空间插件,动态反编译修改配置,信任用户证书来解决。 - [ ] WebSocket协议支持。 -image. image +image. image diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 3964e4b..033a214 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png index 9a6b754..6f45587 100644 Binary files a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png and b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 62a2d6d..76b95f6 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 849765c..e143340 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d2250dc..3747671 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index f813c81..a12b9fc 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 728e6c3..c68df94 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -2,117 +2,115 @@ "images": [ { "size": "20x20", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-20@2x.png", - "scale": "2x" + "scale": "2x", + "platform": "ios" }, { "size": "20x20", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-20@3x.png", - "scale": "3x" + "scale": "3x", + "platform": "ios" }, { "size": "29x29", - "idiom": "iphone", - "filename": "icon-29.png", - "scale": "1x" - }, - { - "size": "29x29", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-29@2x.png", - "scale": "2x" + "scale": "2x", + "platform": "ios" }, { "size": "29x29", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-29@3x.png", - "scale": "3x" + "scale": "3x", + "platform": "ios" + }, + { + "size": "38x38", + "idiom": "universal", + "filename": "icon-38@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "38x38", + "idiom": "universal", + "filename": "icon-38@3x.png", + "scale": "3x", + "platform": "ios" }, { "size": "40x40", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-40@2x.png", - "scale": "2x" + "scale": "2x", + "platform": "ios" }, { "size": "40x40", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-40@3x.png", - "scale": "3x" + "scale": "3x", + "platform": "ios" }, { "size": "60x60", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-60@2x.png", - "scale": "2x" + "scale": "2x", + "platform": "ios" }, { "size": "60x60", - "idiom": "iphone", + "idiom": "universal", "filename": "icon-60@3x.png", - "scale": "3x" + "scale": "3x", + "platform": "ios" }, { - "size": "20x20", - "idiom": "ipad", - "filename": "icon-20-ipad.png", - "scale": "1x" + "size": "64x64", + "idiom": "universal", + "filename": "icon-64@2x.png", + "scale": "2x", + "platform": "ios" }, { - "size": "20x20", - "idiom": "ipad", - "filename": "icon-20@2x-ipad.png", - "scale": "2x" + "size": "64x64", + "idiom": "universal", + "filename": "icon-64@3x.png", + "scale": "3x", + "platform": "ios" }, { - "size": "29x29", - "idiom": "ipad", - "filename": "icon-29-ipad.png", - "scale": "1x" - }, - { - "size": "29x29", - "idiom": "ipad", - "filename": "icon-29@2x-ipad.png", - "scale": "2x" - }, - { - "size": "40x40", - "idiom": "ipad", - "filename": "icon-40.png", - "scale": "1x" - }, - { - "size": "40x40", - "idiom": "ipad", - "filename": "icon-40@2x.png", - "scale": "2x" + "size": "68x68", + "idiom": "universal", + "filename": "icon-68@2x.png", + "scale": "2x", + "platform": "ios" }, { "size": "76x76", - "idiom": "ipad", - "filename": "icon-76.png", - "scale": "1x" - }, - { - "size": "76x76", - "idiom": "ipad", + "idiom": "universal", "filename": "icon-76@2x.png", - "scale": "2x" + "scale": "2x", + "platform": "ios" }, { "size": "83.5x83.5", - "idiom": "ipad", + "idiom": "universal", "filename": "icon-83.5@2x.png", - "scale": "2x" + "scale": "2x", + "platform": "ios" }, { "size": "1024x1024", - "idiom": "ios-marketing", + "idiom": "universal", "filename": "icon-1024.png", - "scale": "1x" + "scale": "1x", + "platform": "ios" } ], "info": { diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png index cf4fd63..6596588 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png deleted file mode 100644 index 657a724..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png deleted file mode 100644 index a247447..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png index a247447..173d138 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png index 4b201aa..d292563 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png deleted file mode 100644 index 0277354..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png deleted file mode 100644 index 0277354..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png deleted file mode 100644 index 219a38f..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png index 219a38f..25193f0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png index 59c32c4..766ce4d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png new file mode 100644 index 0000000..727c1d1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png new file mode 100644 index 0000000..2ffef0b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png deleted file mode 100644 index a247447..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png index 1939b70..c95c730 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png index c888fbb..b444227 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png index c888fbb..b444227 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png index 4871392..c9585cf 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png new file mode 100644 index 0000000..3722309 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png new file mode 100644 index 0000000..16176c1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png new file mode 100644 index 0000000..37fe176 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png deleted file mode 100644 index b50173b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png index 8513252..382fb5a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png index 26a922c..567eb87 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/lib/main.dart b/lib/main.dart index 58dcc39..6c9e9d9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,15 @@ +import 'dart:convert'; import 'dart:io'; import 'package:chinese_font_library/chinese_font_library.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/ui/component/split_view.dart'; +import 'package:network_proxy/ui/content/body.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/desktop/left/domain.dart'; +import 'package:network_proxy/ui/desktop/left/request_editor.dart'; import 'package:network_proxy/ui/desktop/toolbar/toolbar.dart'; import 'package:network_proxy/ui/mobile/mobile.dart'; import 'package:network_proxy/utils/platform.dart'; @@ -15,31 +19,65 @@ import 'network/channel.dart'; import 'network/handler.dart'; import 'network/http/http.dart'; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - if (Platforms.isDesktop()) { - //设置窗口大小 - await windowManager.ensureInitialized(); - - WindowOptions windowOptions = WindowOptions( - minimumSize: const Size(980, 600), - size: Platform.isMacOS ? const Size(1200, 750) : const Size(1080, 650), - center: true, - titleBarStyle: Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal); - windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }); +void main(List args) async { + if (Platforms.isMobile()) { + runApp(const FluentApp(MobileHomePage())); + return; } - runApp(const FluentApp()); + //多窗口 + if (args.firstOrNull == 'multi_window') { + final windowId = int.parse(args[1]); + final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map; + runApp(FluentApp(multiWindow(windowId, argument))); + return; + } + + WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + //设置窗口大小 + WindowOptions windowOptions = WindowOptions( + minimumSize: const Size(980, 600), + size: Platform.isMacOS ? const Size(1200, 750) : const Size(1080, 650), + center: true, + titleBarStyle: Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + + runApp(const FluentApp(DesktopHomePage())); +} + +///多窗口 +Widget multiWindow(int windowId, Map argument) { + if (argument['name'] == 'RequestEditor') { + return RequestEditor( + windowController: WindowController.fromWindowId(windowId), + request: HttpRequest.fromJson(argument['request']), + proxyPort: argument['proxyPort']); + } + + if (argument['name'] == 'HttpBodyWidget') { + return HttpBodyWidget( + windowController: WindowController.fromWindowId(windowId), + httpMessage: HttpMessage.fromJson(argument['httpMessage']), + inNewWindow: true); + } + + return const SizedBox(); } /// 主题 final ValueNotifier themeNotifier = ValueNotifier(ThemeMode.system); class FluentApp extends StatelessWidget { - const FluentApp({super.key}); + final Widget home; + + const FluentApp( + this.home, { + super.key, + }); @override Widget build(BuildContext context) { @@ -59,7 +97,7 @@ class FluentApp extends StatelessWidget { theme: lightTheme, darkTheme: darkTheme, themeMode: currentMode, - home: Platforms.isDesktop() ? const DesktopHomePage() : const MobileHomePage(), + home: home, ); }); } @@ -74,9 +112,9 @@ class DesktopHomePage extends StatefulWidget { class _DesktopHomePagePageState extends State implements EventListener { final domainStateKey = GlobalKey(); - final NetworkTabController panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18)); late ProxyServer proxyServer; + late NetworkTabController panel; @override void onRequest(Channel channel, HttpRequest request) { @@ -92,6 +130,8 @@ class _DesktopHomePagePageState extends State implements EventL void initState() { super.initState(); proxyServer = ProxyServer(listener: this); + panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18), proxyServer: proxyServer); + proxyServer.initializedListener(() { if (!proxyServer.guide) { return; diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index 2f4df9a..b6585da 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -22,8 +22,11 @@ class ProxyServer { //是否初始化 bool init = false; int port = 9099; + + //是否启用https抓包 bool _enableSsl = false; + //是否启用桌面抓包 bool enableDesktop = true; //是否引导 @@ -33,7 +36,11 @@ class ProxyServer { bool get isRunning => server?.isRunning ?? false; Server? server; + + //请求事件监听 EventListener? listener; + + //请求重写 RequestRewrites requestRewrites = RequestRewrites(); final List _initializedListeners = []; @@ -48,7 +55,8 @@ class ProxyServer { Future homeDir() async { String? userHome; if (Platforms.isDesktop()) { - userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + userHome = + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; } else { userHome = (await getApplicationSupportDirectory()).path; } @@ -93,8 +101,11 @@ class ProxyServer { server.enableSsl = _enableSsl; server.initChannel((channel) { - channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(), - HttpChannelHandler(listener: listener, requestRewrites: requestRewrites)); + channel.pipeline.handle( + HttpRequestCodec(), + HttpResponseCodec(), + HttpChannelHandler( + listener: listener, requestRewrites: requestRewrites)); }); return server.bind(port).then((serverSocket) { logger.i("listen on $port"); @@ -163,7 +174,8 @@ class ProxyServer { /// 加载请求重写配置文件 Future _loadRequestRewriteConfig() async { var home = await homeDir(); - var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); + var file = + File('${home.path}${Platform.pathSeparator}request_rewrite.json'); var exits = await file.exists(); if (!exits) { return; @@ -178,7 +190,8 @@ class ProxyServer { /// 保存请求重写配置文件 flushRequestRewriteConfig() async { var home = await homeDir(); - var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); + var file = + File('${home.path}${Platform.pathSeparator}request_rewrite.json'); bool exists = await file.exists(); if (!exists) { await file.create(recursive: true); diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 0c24a2e..55dbf50 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -93,7 +93,6 @@ class HttpChannelHandler extends ChannelHandler { /// 转发请求 Future forward(Channel channel, HttpRequest httpRequest) async { - var remoteChannel = await _getRemoteChannel(channel, httpRequest); //实现抓包代理转发 diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index 92440b1..c3f2a6b 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -28,6 +28,17 @@ abstract class HttpMessage { HttpMessage(this.protocolVersion); + //json序列化 + factory HttpMessage.fromJson(Map json) { + if (json["_class"] == "HttpRequest") { + return HttpRequest.fromJson(json); + } + + return HttpResponse.fromJson(json); + } + + Map toJson(); + ContentType get contentType => contentTypes.entries .firstWhere((element) => headers.contentType.contains(element.key), orElse: () => const MapEntry("unknown", ContentType.http)) @@ -79,6 +90,24 @@ class HttpRequest extends HttpMessage { return request; } + @override + Map toJson() { + return { + '_class': 'HttpRequest', + 'uri': requestUrl, + 'method': method.name, + 'headers': headers.toJson(), + 'body': bodyAsString, + }; + } + + factory HttpRequest.fromJson(Map json) { + var request = HttpRequest(HttpMethod.valueOf(json['method']), json['uri']); + request.headers.addAll(HttpHeaders.fromJson(json['headers'])); + request.body = utf8.encode(json['body']); + return request; + } + @override String toString() { return 'HttpReqeust{version: $protocolVersion, url: $uri, method: ${method.name}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; @@ -102,6 +131,26 @@ class HttpResponse extends HttpMessage { return '${responseTime.difference(request!.requestTime).inMilliseconds}ms'; } + factory HttpResponse.fromJson(Map json) { + return HttpResponse(json['protocolVersion'], HttpStatus(json['status']['code'], json['status']['reasonPhrase'])) + ..headers.addAll(HttpHeaders.fromJson(json['headers'])) + ..body = utf8.encode(json['body']); + } + + @override + Map toJson() { + return { + '_class': 'HttpResponse', + 'protocolVersion': protocolVersion, + 'status': { + 'code': status.code, + 'reasonPhrase': status.reasonPhrase, + }, + 'headers': headers.toJson(), + 'body': bodyAsString, + }; + } + @override String toString() { return 'HttpResponse{status: ${status.code}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; @@ -110,13 +159,13 @@ class HttpResponse extends HttpMessage { ///HTTP请求方法。 enum HttpMethod { - options("OPTIONS"), get("GET"), - head("HEAD"), post("POST"), put("PUT"), patch("PATCH"), delete("DELETE"), + options("OPTIONS"), + head("HEAD"), trace("TRACE"), connect("CONNECT"), propfind("PROPFIND"), diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart index 84cb738..9f041a1 100644 --- a/lib/network/http/http_headers.dart +++ b/lib/network/http/http_headers.dart @@ -13,6 +13,8 @@ class HttpHeaders { // 由小写标头名称键入的原始标头名称。 final Map> _originalHeaderNames = {}; + HttpHeaders(); + ///设置header。 void set(String name, String value) { _headers[name.toLowerCase()] = [value]; @@ -21,22 +23,29 @@ class HttpHeaders { ///添加header。 void add(String name, String value) { - if (_headers.containsKey(name.toLowerCase())) { - _headers[name.toLowerCase()]!.add(value); - if (!_originalHeaderNames.containsKey(name)) { - _originalHeaderNames[name] = []; - } - _originalHeaderNames[name]!.add(value); - return; + if (!_headers.containsKey(name.toLowerCase())) { + _headers[name.toLowerCase()] = []; + _originalHeaderNames[name] = []; } - _headers[name.toLowerCase()] = [value]; - _originalHeaderNames[name] = [value]; + _headers[name.toLowerCase()]?.add(value); + _originalHeaderNames[name]?.add(value); } - //批量添加 - addAll(HttpHeaders headers) { - headers.forEach((key, values) { + ///添加header。 + void addValues(String name, List values) { + if (!_headers.containsKey(name.toLowerCase())) { + _headers[name.toLowerCase()] = []; + _originalHeaderNames[name] = []; + } + + _headers[name.toLowerCase()]?.addAll(values); + _originalHeaderNames[name]?.addAll(values); + } + + ///从headers中添加 + addAll(HttpHeaders? headers) { + headers?.forEach((key, values) { for (var val in values) { add(key, val); } @@ -103,6 +112,28 @@ class HttpHeaders { return sb.toString(); } + + ///转换json + Map toJson() { + Map json = {}; + forEach((name, values) { + json[name] = values; + }); + return json; + } + + ///从json解析 + factory HttpHeaders.fromJson(Map json) { + HttpHeaders headers = HttpHeaders(); + json.forEach((key, values) { + for (var element in (values as List)) { + headers.add(key, element.toString()); + } + }); + + return headers; + } + @override String toString() { return 'HttpHeaders{$_originalHeaderNames}'; diff --git a/lib/network/http_client.dart b/lib/network/http_client.dart index f49b067..7ce0ea8 100644 --- a/lib/network/http_client.dart +++ b/lib/network/http_client.dart @@ -48,7 +48,7 @@ class HttpClients { /// 发送代理请求 static Future proxyRequest(String proxyHost, int port, HttpRequest request, - {Duration duration = const Duration(seconds: 3)}) async { + {Duration timeout = const Duration(seconds: 3)}) async { var httpResponseHandler = HttpResponseHandler(); bool isHttps = request.uri.startsWith("https://"); @@ -60,14 +60,13 @@ class HttpClients { if (isHttps) { HttpRequest proxyRequest = HttpRequest(HttpMethod.connect, request.uri); await channel.write(proxyRequest); - var proxyResp = await httpResponseHandler.getResponse(duration); - print(proxyResp); + await httpResponseHandler.getResponse(timeout); channel.secureSocket = await SecureSocket.secure(channel.socket, onBadCertificate: (certificate) => true); } httpResponseHandler.resetResponse(); await channel.write(request); - return httpResponseHandler.getResponse(duration).whenComplete(() => channel.close()); + return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close()); } } @@ -87,4 +86,10 @@ class HttpResponseHandler extends ChannelHandler { void resetResponse() { _completer = Completer(); } + + @override + void channelInactive(Channel channel) { + log.i("[${channel.id}] channelInactive"); + _completer.completeError("channelInactive"); + } } diff --git a/lib/ui/component/share.dart b/lib/ui/component/share.dart index 6500727..f910f08 100644 --- a/lib/ui/component/share.dart +++ b/lib/ui/component/share.dart @@ -3,17 +3,20 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/ui/mobile/request/request_editor.dart'; import 'package:network_proxy/utils/curl.dart'; import 'package:share_plus/share_plus.dart'; ///分享按钮 class ShareWidget extends StatelessWidget { + final ProxyServer proxyServer; final HttpRequest? request; final HttpResponse? response; - const ShareWidget({super.key, this.request, this.response}); + const ShareWidget({super.key, required this.proxyServer, this.request, this.response}); @override Widget build(BuildContext context) { @@ -52,6 +55,14 @@ class ShareWidget extends StatelessWidget { name: "cURL.txt", mimeType: "txt"); Share.shareXFiles([file], text: "ProxyPin全平台抓包软件"); }), + PopupMenuItem( + child: const Text('编辑请求重放'), + onTap: () { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => MobileRequestEditor(request: request, proxyServer: proxyServer))); + }); + }), ]); }); } diff --git a/lib/ui/content/body.dart b/lib/ui/content/body.dart index d74adf7..e1f147c 100644 --- a/lib/ui/content/body.dart +++ b/lib/ui/content/body.dart @@ -1,17 +1,22 @@ import 'dart:convert'; +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_json_viewer_new/flutter_json_viewer.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/utils/platform.dart'; +import 'package:window_manager/window_manager.dart'; class HttpBodyWidget extends StatefulWidget { final HttpMessage? httpMessage; final bool inNewWindow; //是否在新窗口 + final WindowController? windowController; - const HttpBodyWidget({super.key, required this.httpMessage, this.inNewWindow = false}); + const HttpBodyWidget({super.key, required this.httpMessage, this.inNewWindow = false, this.windowController}); @override State createState() { @@ -23,9 +28,24 @@ class HttpBodyState extends State { ValueNotifier tabIndex = ValueNotifier(0); String? body; + @override + void initState() { + super.initState(); + RawKeyboard.instance.addListener(onKeyEvent); + } + + void onKeyEvent(RawKeyEvent event) { + if (event.isKeyPressed(LogicalKeyboardKey.metaLeft) && event.isKeyPressed(LogicalKeyboardKey.keyW)) { + RawKeyboard.instance.removeListener(onKeyEvent); + widget.windowController?.close(); + return; + } + } + @override void dispose() { tabIndex.dispose(); + RawKeyboard.instance.removeListener(onKeyEvent); super.dispose(); } @@ -53,17 +73,25 @@ class HttpBodyState extends State { ) //body ]; - return DefaultTabController( + var tabController = DefaultTabController( length: tabs.list.length, child: widget.inNewWindow ? ListView(children: list) : Column(crossAxisAlignment: CrossAxisAlignment.start, children: list)); + + if (widget.inNewWindow) { + return Scaffold( + appBar: AppBar(title: titleWidget(inNewWindow: true), toolbarHeight: Platform.isWindows ? 36 : null), + body: tabController); + } + return tabController; } Widget titleWidget({inNewWindow = false}) { var type = widget.httpMessage is HttpRequest ? "Request" : "Response"; return Row( + mainAxisAlignment: widget.inNewWindow ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ Text('$type Body', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(width: 15), @@ -84,13 +112,26 @@ class HttpBodyState extends State { ); } - void openNew() { + void openNew() async { + if (Platforms.isDesktop()) { + var size = MediaQuery.of(context).size; + var ratio = 1.0; + if (Platform.isWindows) { + WindowManager.instance.getDevicePixelRatio(); + } + final window = await DesktopMultiWindow.createWindow(jsonEncode( + {'name': 'HttpBodyWidget', 'httpMessage': widget.httpMessage, 'inNewWindow': true}, + )); + window + ..setTitle(widget.httpMessage is HttpRequest ? '请求体' : '响应体') + ..setFrame(const Offset(100, 100) & Size(800 * ratio, size.height * ratio)) + ..center() + ..show(); + return; + } + Navigator.push( - context, - MaterialPageRoute( - builder: (_) => Scaffold( - appBar: AppBar(title: titleWidget(inNewWindow: true)), - body: HttpBodyWidget(httpMessage: widget.httpMessage, inNewWindow: true)))); + context, MaterialPageRoute(builder: (_) => HttpBodyWidget(httpMessage: widget.httpMessage, inNewWindow: true))); } Widget getBody(ViewType type) { diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index f75ac5b..f572cbc 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/ui/component/share.dart'; import 'package:network_proxy/ui/component/utils.dart'; @@ -14,12 +15,14 @@ class NetworkTabController extends StatefulWidget { 'Cookies', ]; + final ProxyServer proxyServer; final ValueWrap request = ValueWrap(); final ValueWrap response = ValueWrap(); final Widget? title; final TextStyle? tabStyle; - NetworkTabController({HttpRequest? httpRequest, HttpResponse? httpResponse, this.title, this.tabStyle}) + NetworkTabController( + {HttpRequest? httpRequest, HttpResponse? httpResponse, this.title, this.tabStyle, required this.proxyServer}) : super(key: GlobalKey()) { request.set(httpRequest); response.set(httpResponse); @@ -71,7 +74,10 @@ class NetworkTabState extends State with SingleTickerProvi : AppBar( title: widget.title, bottom: tabBar, - actions: [ShareWidget(request: widget.request.get(), response: widget.response.get())], + actions: [ + ShareWidget( + proxyServer: widget.proxyServer, request: widget.request.get(), response: widget.response.get()) + ], ); return Scaffold( @@ -156,7 +162,7 @@ class NetworkTabState extends State with SingleTickerProvi headers.add(Row(children: [ SelectableText('$name: ', style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.deepOrangeAccent)), - Expanded( child: SelectableText(v, contextMenuBuilder: contextMenu, maxLines: 5, minLines: 1)), + Expanded(child: SelectableText(v, contextMenuBuilder: contextMenu, maxLines: 5, minLines: 1)), ])); headers.add(const Divider(thickness: 0.1)); } diff --git a/lib/ui/desktop/left/path.dart b/lib/ui/desktop/left/path.dart index 96442d2..4b55ae1 100644 --- a/lib/ui/desktop/left/path.dart +++ b/lib/ui/desktop/left/path.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:date_format/date_format.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,6 +14,7 @@ import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/utils/curl.dart'; import 'package:network_proxy/utils/lang.dart'; +import 'package:window_manager/window_manager.dart'; ///请求 URI class PathRow extends StatefulWidget { @@ -101,15 +106,44 @@ class _PathRowState extends State { height: 38, child: const Text("重放请求", style: TextStyle(fontSize: 14)), onTap: () { + if (!widget.proxyServer.isRunning) { + FlutterToastr.show('代理服务未启动', context); + return; + } var request = widget.request.copy(uri: widget.request.requestUrl); HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request); FlutterToastr.show('已重新发送请求', context); }), + PopupMenuItem( + height: 38, + child: const Text("编辑重放请求", style: TextStyle(fontSize: 14)), + onTap: () { + WidgetsBinding.instance.addPostFrameCallback((_) { + requestEdit(); + }); + }), ], ); } + requestEdit() async { + var size = MediaQuery.of(context).size; + var ratio = 1.0; + if (Platform.isWindows) { + WindowManager.instance.getDevicePixelRatio(); + } + + final window = await DesktopMultiWindow.createWindow(jsonEncode( + {'name': 'RequestEditor', 'request': widget.request, 'proxyPort': widget.proxyServer.port}, + )); + window.setTitle('请求编辑'); + window + ..setFrame(const Offset(100, 100) & Size(860 * ratio, size.height * ratio)) + ..center() + ..show(); + } + //点击事件 void onClick() { if (selected) { diff --git a/lib/ui/desktop/left/request_editor.dart b/lib/ui/desktop/left/request_editor.dart new file mode 100644 index 0000000..5bd35ff --- /dev/null +++ b/lib/ui/desktop/left/request_editor.dart @@ -0,0 +1,257 @@ +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_toastr/flutter_toastr.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/http/http_headers.dart'; +import 'package:network_proxy/network/http_client.dart'; + +class RequestEditor extends StatefulWidget { + final WindowController? windowController; + final HttpRequest? request; + final int proxyPort; + + const RequestEditor({super.key, this.request, this.windowController, required this.proxyPort}); + + @override + State createState() { + return RequestEditorState(); + } +} + +class RequestEditorState extends State { + final requestLineKey = GlobalKey<_RequestLineState>(); + final headerKey = GlobalKey(); + + String requestBody = ""; + + @override + void initState() { + super.initState(); + RawKeyboard.instance.addListener(onKeyEvent); + requestBody = widget.request?.bodyAsString ?? ''; + } + + void onKeyEvent(RawKeyEvent event) { + if (event.isKeyPressed(LogicalKeyboardKey.metaLeft) && event.isKeyPressed(LogicalKeyboardKey.keyW)) { + RawKeyboard.instance.removeListener(onKeyEvent); + widget.windowController?.close(); + return; + } + } + + @override + void dispose() { + RawKeyboard.instance.removeListener(onKeyEvent); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("请求编辑", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + toolbarHeight: Platform.isWindows ? 36 : null, + centerTitle: true, + actions: [ + TextButton.icon(onPressed: () async => sendRequest(), icon: const Icon(Icons.send), label: const Text("发送")) + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(15), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _RequestLine(request: widget.request, key: requestLineKey), // 请求行 + Headers(headers: widget.request?.headers, key: headerKey), // 请求头 + const Text("Body", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)), + body() // 请求体 + ]))); + } + + ///发送请求 + sendRequest() async { + var currentState = requestLineKey.currentState!; + HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), currentState.requestUrl); + var headers = headerKey.currentState?.getHeaders(); + request.headers.addAll(headers); + request.body = requestBody.codeUnits; + HttpClients.proxyRequest("127.0.0.1", widget.proxyPort, request); + + FlutterToastr.show('已重新发送请求', context); + RawKeyboard.instance.removeListener(onKeyEvent); + await Future.delayed(const Duration(milliseconds: 500), () => widget.windowController?.close()); + } + + Widget body() { + return TextField( + controller: TextEditingController(text: requestBody), + onChanged: (value) { + requestBody = value; + }, + minLines: 3, + maxLines: 10); + } +} + +///请求行 +class _RequestLine extends StatefulWidget { + final HttpRequest? request; + + const _RequestLine({super.key, this.request}); + + @override + State createState() { + return _RequestLineState(); + } +} + +class _RequestLineState extends State<_RequestLine> { + String requestUrl = ""; + String requestMethod = HttpMethod.get.name; + + @override + void initState() { + super.initState(); + if (widget.request == null) { + return; + } + var request = widget.request!; + requestUrl = request.requestUrl; + requestMethod = request.method.name; + } + + @override + Widget build(BuildContext context) { + return TextField( + decoration: InputDecoration( + prefix: DropdownButton( + padding: const EdgeInsets.only(right: 10), + underline: const SizedBox(), + isDense: true, + focusColor: Colors.transparent, + value: requestMethod, + items: HttpMethod.values.map((it) => DropdownMenuItem(value: it.name, child: Text(it.name))).toList(), + onChanged: (String? 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; + }); + } +} + +///请求头 +class Headers extends StatefulWidget { + final HttpHeaders? headers; + + const Headers({super.key, this.headers}); + + @override + State createState() { + return HeadersState(); + } +} + +class HeadersState extends State { + Map> headers = {}; + + @override + void initState() { + super.initState(); + if (widget.headers == null) { + return; + } + widget.headers?.forEach((name, values) { + headers[TextEditingController(text: name)] = values.map((it) => TextEditingController(text: it)).toList(); + }); + } + + ///获取所有请求头 + HttpHeaders getHeaders() { + var headers = HttpHeaders(); + this.headers.forEach((name, values) { + if (name.text.isEmpty) { + return; + } + for (var element in values) { + if (element.text.isNotEmpty) { + headers.add(name.text, element.text); + } + } + }); + return headers; + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 15), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox( + width: double.infinity, + child: Text("Headers", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue))), + const SizedBox(height: 10), + DataTable( + dataRowMaxHeight: 38, + dataRowMinHeight: 38, + dividerThickness: 0.2, + border: TableBorder.all(color: Theme.of(context).highlightColor), + columns: const [ + DataColumn(label: Text('Key')), + DataColumn(label: Text('Value')), + DataColumn(label: Text('')) + ], + rows: buildRows()), + const SizedBox(height: 10), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + TextButton( + onPressed: () { + setState(() { + headers[TextEditingController()] = [TextEditingController()]; + }); + }, + child: const Text("添加Header", textAlign: TextAlign.center)) + ]), + ])); + } + + List buildRows() { + var width = MediaQuery.of(context).size.width; + List list = []; + + headers.forEach((key, values) { + for (var val in values) { + list.add(DataRow(cells: [ + cell(key, width: 200, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + cell(val, width: width - 410), + DataCell(InkWell( + onTap: () { + setState(() { + headers.remove(key); + }); + }, + child: const Icon(Icons.remove_circle, size: 16))) + ])); + } + }); + + return list; + } + + DataCell cell(TextEditingController val, {TextStyle? style = const TextStyle(fontSize: 14), double? width}) { + return DataCell(SizedBox( + width: width, + child: TextFormField( + style: style, + controller: val, + decoration: const InputDecoration(isDense: true, border: InputBorder.none, hintText: "Header")))); + } +} diff --git a/lib/ui/desktop/left/search.dart b/lib/ui/desktop/left/search.dart index 85211e8..0d72280 100644 --- a/lib/ui/desktop/left/search.dart +++ b/lib/ui/desktop/left/search.dart @@ -17,7 +17,7 @@ class Search extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), child: TextField( - cursorHeight: 15, + cursorHeight: 22, onChanged: (val) async { value = val; diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index cfd1052..88147b7 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -23,7 +23,8 @@ class _SettingState extends State { @override void initState() { - enableDesktopListenable = ValueNotifier(widget.proxyServer.enableDesktop); + enableDesktopListenable = + ValueNotifier(widget.proxyServer.enableDesktop); super.initState(); } @@ -44,7 +45,9 @@ class _SettingState extends State { return [ PopupMenuItem( padding: const EdgeInsets.all(0), - child: PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13))), + child: PortWidget( + proxyServer: widget.proxyServer, + textStyle: const TextStyle(fontSize: 13))), PopupMenuItem( padding: const EdgeInsets.all(0), child: ValueListenableBuilder( @@ -145,7 +148,8 @@ class _PortState extends State { textController.text = widget.proxyServer.port.toString(); portFocus.addListener(() async { //失去焦点 - if (!portFocus.hasFocus && textController.text != widget.proxyServer.port.toString()) { + if (!portFocus.hasFocus && + textController.text != widget.proxyServer.port.toString()) { widget.proxyServer.port = int.parse(textController.text); if (widget.proxyServer.isRunning) { widget.proxyServer.restart(); diff --git a/lib/ui/desktop/toolbar/ssl/ssl.dart b/lib/ui/desktop/toolbar/ssl/ssl.dart index e8155b9..4b78d83 100644 --- a/lib/ui/desktop/toolbar/ssl/ssl.dart +++ b/lib/ui/desktop/toolbar/ssl/ssl.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/util/crts.dart'; import 'package:network_proxy/utils/ip.dart'; @@ -91,6 +92,10 @@ class _SslState extends State { focusColor: Colors.transparent, trailing: const Icon(Icons.arrow_right), onTap: () async { + if (!widget.proxyServer.isRunning) { + FlutterToastr.show("请先启动抓包", context); + return; + } launchUrl(Uri.parse("http://127.0.0.1:${widget.proxyServer.port}/ssl")); }), ) @@ -220,7 +225,14 @@ class _SslState extends State { const SizedBox(height: 10), const Text("2. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书"), const SizedBox(height: 10), - Image.network("https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png", height: 600) + ClipRRect( + child: Align( + alignment: Alignment.topCenter, + heightFactor: .7, + child: Image.network( + "https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png", + height: 550, + ))) ]); }); } diff --git a/lib/ui/launch/launch.dart b/lib/ui/launch/launch.dart index ed31f64..a5c47c9 100644 --- a/lib/ui/launch/launch.dart +++ b/lib/ui/launch/launch.dart @@ -49,6 +49,7 @@ class _SocketLaunchState extends State with WindowListener, Widget print("onWindowClose"); await widget.proxyServer.stop(); started = false; + windowManager.destroy(); } @override diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index cea8ac6..65a52ae 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -85,8 +85,9 @@ class RequestListState extends State { class RequestSequence extends StatefulWidget { final List list; final ProxyServer proxyServer; + final bool displayDomain; - const RequestSequence({super.key, required this.list, required this.proxyServer}); + const RequestSequence({super.key, required this.list, required this.proxyServer, this.displayDomain = true}); @override State createState() { @@ -186,12 +187,16 @@ class RequestSequenceState extends State with AutomaticKeepAliv return ListView.separated( cacheExtent: 1000, - separatorBuilder: (context, index) => Divider(height: 0.5, color: Theme.of(context).focusColor), + separatorBuilder: (context, index) => Divider(thickness: 0.2, color: Theme.of(context).dividerColor), itemCount: view.length, itemBuilder: (context, index) { GlobalKey key = GlobalKey(); indexes[view.elementAt(index)] = key; - return RequestRow(key: key, request: view.elementAt(index), proxyServer: widget.proxyServer); + return RequestRow( + key: key, + request: view.elementAt(index), + proxyServer: widget.proxyServer, + displayDomain: widget.displayDomain); }); } } @@ -316,7 +321,8 @@ class DomainListState extends State with AutomaticKeepAliveClientMix Widget build(BuildContext context) { super.build(context); return ListView.separated( - separatorBuilder: (context, index) => Divider(height: 0.5, color: Theme.of(context).focusColor), + padding: EdgeInsets.zero, + separatorBuilder: (context, index) => Divider(thickness: 0.2, color: Theme.of(context).dividerColor), cacheExtent: 1000, itemCount: list.length, itemBuilder: (ctx, index) => title(index)); @@ -326,6 +332,7 @@ class DomainListState extends State with AutomaticKeepAliveClientMix var time = formatDate(containerMap[list.elementAt(index)]!.last.requestTime, [m, '/', d, ' ', HH, ':', nn, ':', ss]); return ListTile( + visualDensity: const VisualDensity( vertical: -4), title: Text(list.elementAt(index).domain, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: const Icon(Icons.chevron_right), subtitle: Text("最后请求时间: $time, 次数: ${containerMap[list.elementAt(index)]!.length}", @@ -335,9 +342,10 @@ class DomainListState extends State with AutomaticKeepAliveClientMix Navigator.push(context, MaterialPageRoute(builder: (context) { showHostAndPort = list.elementAt(index); return Scaffold( - appBar: AppBar(title: const Text("请求列表")), + appBar: AppBar(title: Text(list.elementAt(index).domain, style: const TextStyle(fontSize: 16))), body: RequestSequence( key: requestSequenceKey, + displayDomain: false, list: containerMap[list.elementAt(index)]!, proxyServer: widget.proxyServer)); })); diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index c056451..17e5fdb 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -7,14 +7,16 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http_client.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/content/panel.dart'; +import 'package:network_proxy/ui/mobile/request/request_editor.dart'; import 'package:network_proxy/utils/curl.dart'; ///请求行 class RequestRow extends StatefulWidget { final HttpRequest request; final ProxyServer proxyServer; + final bool displayDomain; - const RequestRow({super.key, required this.request, required this.proxyServer}); + const RequestRow({super.key, required this.request, required this.proxyServer, this.displayDomain = true}); @override State createState() { @@ -41,20 +43,22 @@ class RequestRowState extends State { @override Widget build(BuildContext context) { - var title = '${request.method.name} ${request.requestUrl}'; + var title = '${request.method.name} ${widget.displayDomain ? request.requestUrl : request.path()}'; var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]); var subTitle = '$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''}'; return ListTile( - leading: Icon(getIcon(response), size: 16, color: Colors.green), - title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1), - subtitle: Text(subTitle, maxLines: 1), + visualDensity: const VisualDensity(vertical: -4), + leading: widget.displayDomain ? null : Icon(getIcon(response), size: 16, color: Colors.green), + title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1, style: const TextStyle(fontSize: 14)), + subtitle: Text(subTitle, maxLines: 1, style: const TextStyle(fontSize: 12)), trailing: const Icon(Icons.chevron_right), onLongPress: () => menu(menuPosition(context)), onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return NetworkTabController( + proxyServer: widget.proxyServer, httpRequest: request, httpResponse: response, title: const Text("抓包详情", style: TextStyle(fontSize: 16))); @@ -62,7 +66,7 @@ class RequestRowState extends State { }); } - ///右键菜单 + ///菜单 menu(RelativeRect position) { showModalBottomSheet( context: context, @@ -76,13 +80,26 @@ class RequestRowState extends State { menuItem("复制 cURL 请求", () => curlRequest(widget.request)), const Divider(thickness: 0.5), TextButton( - child: const SizedBox(width: double.infinity, child: Text("重放请求", textAlign: TextAlign.center)), + child: const SizedBox(width: double.infinity, child: Text("请求重放", textAlign: TextAlign.center)), onPressed: () { var request = widget.request.copy(uri: widget.request.requestUrl); + if (!widget.proxyServer.isRunning) { + FlutterToastr.show('代理服务未启动', context); + return; + } HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request); FlutterToastr.show('已重新发送请求', context); Navigator.of(context).pop(); }), + const Divider(thickness: 0.5), + TextButton( + child: const SizedBox(width: double.infinity, child: Text("编辑请求重放", textAlign: TextAlign.center)), + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => + MobileRequestEditor(request: widget.request, proxyServer: widget.proxyServer))); + }), Container( color: Theme.of(context).hoverColor, height: 8, diff --git a/lib/ui/mobile/request/request_editor.dart b/lib/ui/mobile/request/request_editor.dart new file mode 100644 index 0000000..be3e496 --- /dev/null +++ b/lib/ui/mobile/request/request_editor.dart @@ -0,0 +1,307 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/http/http_headers.dart'; +import 'package:network_proxy/network/http_client.dart'; + +class MobileRequestEditor extends StatefulWidget { + final HttpRequest? request; + final ProxyServer proxyServer; + + const MobileRequestEditor({super.key, this.request, required this.proxyServer}); + + @override + State createState() { + return RequestEditorState(); + } +} + +class RequestEditorState extends State { + final requestLineKey = GlobalKey<_RequestLineState>(); + final headerKey = GlobalKey(); + + String requestBody = ""; + + @override + void initState() { + super.initState(); + requestBody = widget.request?.bodyAsString ?? ""; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("编辑请求", style: TextStyle(fontSize: 16)), + centerTitle: true, + leading: TextButton( + onPressed: () => Navigator.pop(context), + child: Text("取消", style: Theme.of(context).textTheme.bodyMedium)), + actions: [TextButton.icon(icon: const Icon(Icons.send), label: const Text("发送"), onPressed: sendRequest)], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(15), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _RequestLine(request: widget.request, key: requestLineKey), // 请求行 + Headers(headers: widget.request?.headers, key: headerKey), // 请求头 + const SizedBox(height: 10), + const Text("Body", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)), + body() + ]))); + } + + ///发送请求 + sendRequest() { + if (!widget.proxyServer.isRunning) { + FlutterToastr.show('代理服务未启动', context); + return; + } + + var currentState = requestLineKey.currentState!; + HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), currentState.requestUrl); + var headers = headerKey.currentState?.getHeaders(); + request.headers.addAll(headers); + request.body = requestBody.codeUnits; + HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request); + FlutterToastr.show('已重新发送请求', context); + Navigator.pop(context, request); + } + + ///请求体 + Widget body() { + return TextField( + controller: TextEditingController(text: requestBody), + onChanged: (value) { + requestBody = value; + }, + minLines: 3, + maxLines: 10); + } +} + +class _RequestLine extends StatefulWidget { + final HttpRequest? request; + + const _RequestLine({this.request, super.key}); + + @override + State createState() { + return _RequestLineState(); + } +} + +class _RequestLineState extends State<_RequestLine> { + String requestUrl = ""; + String requestMethod = HttpMethod.get.name; + + @override + void initState() { + super.initState(); + if (widget.request == null) { + return; + } + var request = widget.request!; + requestUrl = request.requestUrl; + requestMethod = request.method.name; + } + + @override + Widget build(BuildContext context) { + return TextField( + style: const TextStyle(fontSize: 14), + minLines: 1, + maxLines: 5, + decoration: InputDecoration( + prefix: DropdownButton( + padding: const EdgeInsets.only(right: 10), + underline: const SizedBox(), + isDense: true, + focusColor: Colors.transparent, + value: requestMethod, + items: HttpMethod.values + .map((it) => + DropdownMenuItem(value: it.name, child: Text(it.name, style: const TextStyle(fontSize: 12)))) + .toList(), + onChanged: (String? 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; + }); + } +} + +class Headers extends StatefulWidget { + final HttpHeaders? headers; + + const Headers({super.key, this.headers}); + + @override + State createState() { + return HeadersState(); + } +} + +class HeadersState extends State { + Map> headers = {}; + + @override + void initState() { + super.initState(); + if (widget.headers == null) { + 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); + } + }); + return headers; + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox( + width: double.infinity, + child: Text("Headers", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue))), + const SizedBox(height: 10), + ...buildHeaders(), + Container( + alignment: Alignment.center, + child: TextButton( + onPressed: () { + modifyHeader("", ""); + }, + child: const Text("添加Header", textAlign: TextAlign.center))) //添加按钮 + ])); + } + + List buildHeaders() { + 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)); + var ink = InkWell( + onTap: () => modifyHeader(key, val), + onLongPress: () => deleteHeader(key), + child: Padding(padding: const EdgeInsets.only(top: 5, bottom: 5), child: header)); + list.add(ink); + list.add(const Divider(thickness: 0.2)); + } + }); + return list; + } + + /// 修改请求头 + modifyHeader(String key, String val) { + String headerName = key; + showDialog( + context: context, + builder: (ctx) { + return AlertDialog( + titlePadding: const EdgeInsets.only(left: 25, top: 10), + actionsPadding: const EdgeInsets.only(right: 10, bottom: 10), + title: const Text("修改请求头", style: TextStyle(fontSize: 18)), + content: Wrap( + children: [ + TextField( + minLines: 1, + maxLines: 3, + controller: TextEditingController(text: headerName), + decoration: const InputDecoration(labelText: "请求头名称"), + onChanged: (value) { + headerName = value; + }, + ), + TextField( + minLines: 1, + maxLines: 8, + controller: TextEditingController(text: val), + decoration: const InputDecoration(labelText: "请求头值"), + onChanged: (value) { + val = value; + }, + ) + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("取消")), + TextButton( + onPressed: () { + setState(() { + if (headerName != key) { + headers.remove(key); + } + + headers[headerName] = [val]; + }); + Navigator.pop(context); + }, + child: const Text("修改")), + ], + ); + }); + } + + //删除 + deleteHeader(String key) { + HapticFeedback.heavyImpact(); + showDialog( + context: context, + builder: (ctx) { + return AlertDialog( + title: const Text("是否删除该请求头?", style: TextStyle(fontSize: 18)), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("取消")), + TextButton( + onPressed: () { + setState(() { + headers.remove(key); + }); + Navigator.pop(context); + }, + child: const Text("删除")), + ], + ); + }); + } + + Widget row(Widget title, Widget child) { + return Row(children: [ + Expanded(flex: 3, child: title), + const SizedBox(width: 10, child: Text(":", style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w600))), + Expanded( + flex: 6, + child: child, + ), + ]); + } +} diff --git a/lib/ui/mobile/setting/ssl.dart b/lib/ui/mobile/setting/ssl.dart index b7389d5..5b48b08 100644 --- a/lib/ui/mobile/setting/ssl.dart +++ b/lib/ui/mobile/setting/ssl.dart @@ -79,7 +79,14 @@ class _MobileSslState extends State { List android() { return [ TextButton(onPressed: () {}, child: const Text("2. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书")), - Image.network("https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png"), + ClipRRect( + child: Align( + alignment: Alignment.topCenter, + heightFactor: .7, + child: Image.network( + "https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png", + height: 680, + ))) ]; } @@ -88,7 +95,7 @@ class _MobileSslState extends State { showDialog( context: context, builder: (context) { - return const Text("请先启动代理服务器"); + return const Text("请先启动抓包"); }); return; } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e332053..c4e6c1a 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,12 +6,16 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_multi_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopMultiWindowPlugin"); + desktop_multi_window_plugin_register_with_registrar(desktop_multi_window_registrar); g_autoptr(FlPluginRegistrar) proxy_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ProxyManagerPlugin"); proxy_manager_plugin_register_with_registrar(proxy_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7b363ea..e067901 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_multi_window proxy_manager screen_retriever url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9f7137c..7339a3c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import desktop_multi_window import path_provider_foundation import proxy_manager import screen_retriever @@ -14,6 +15,7 @@ import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ProxyManagerPlugin.register(with: registry.registrar(forPlugin: "ProxyManagerPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f..1ac8f69 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -3,61 +3,55 @@ { "size" : "16x16", "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", + "filename" : "icon_16x16.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", - "filename" : "app_icon_64.png", + "filename" : "icon_32x32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "icon_32x32@2x.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", - "filename" : "app_icon_128.png", + "filename" : "icon_128x128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", - "filename" : "app_icon_256.png", + "filename" : "icon_128x128@2x.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", - "filename" : "app_icon_256.png", + "filename" : "icon_256x256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", - "filename" : "app_icon_512.png", + "filename" : "icon_256x256@2x.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", - "filename" : "app_icon_512.png", + "filename" : "icon_512x512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", - "filename" : "app_icon_1024.png", + "filename" : "icon_512x512@2x.png", "scale" : "2x" } ], diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 567ecd5..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index a82c3bc..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index d406e97..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index 3269cc6..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 253bf05..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index d44ff69..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 91f8a5c..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..4bcfc79 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..9e7ecce Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..92001e2 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..c1a7079 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..9e7ecce Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..2670c25 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..c1a7079 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..59eca58 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..2670c25 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..ed3c246 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/pubspec.lock b/pubspec.lock index b2399a6..b2204ea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.7" + desktop_multi_window: + dependency: "direct main" + description: + name: desktop_multi_window + sha256: "29971186ae0790e32b156f127f9c22c5ee77bdb94b14f7cea23f2356d0c76cfc" + url: "https://pub.dev" + source: hosted + version: "0.2.0" easy_permission: dependency: "direct main" description: @@ -420,10 +428,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pointycastle: dependency: transitive description: @@ -601,10 +609,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" + sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" url: "https://pub.dev" source: hosted - version: "6.0.36" + version: "6.0.37" url_launcher_ios: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 26069e4..db9fd3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: logger: ^1.4.0 date_format: ^2.0.7 window_manager: ^0.3.5 + desktop_multi_window: ^0.2.0 path_provider: ^2.0.15 url_launcher: ^6.1.11 proxy_manager: ^0.0.3 diff --git a/test/widget_test.dart b/test/widget_test.dart index bd8c300..9da8e48 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:network_proxy/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const FluentApp()); + await tester.pumpWidget(const FluentApp(DesktopHomePage())); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8223173..80a5a79 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopMultiWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin")); ProxyManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ProxyManagerPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6c36c83..e935db6 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_multi_window proxy_manager screen_retriever share_plus diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index 6d0a2fa..a31be75 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ