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协议支持。
-
.
+
.
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