diff --git a/lib/main.dart b/lib/main.dart index 2f854ee..d55d2aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,20 +4,14 @@ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/configuration.dart'; -import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/ui/component/chinese_font.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/desktop.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'; import 'package:window_manager/window_manager.dart'; -import 'network/channel.dart'; -import 'network/handler.dart'; import 'network/http/http.dart'; void main(List args) async { @@ -40,8 +34,8 @@ void main(List args) async { await windowManager.ensureInitialized(); //设置窗口大小 WindowOptions windowOptions = WindowOptions( - minimumSize: const Size(980, 600), - size: Platform.isMacOS ? const Size(1200, 750) : const Size(1080, 650), + minimumSize: const Size(1000, 600), + size: Platform.isMacOS ? const Size(1230, 750) : const Size(1100, 650), center: true, titleBarStyle: Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal); windowManager.waitUntilReadyToShow(windowOptions, () async { @@ -56,9 +50,7 @@ void main(List args) async { Widget multiWindow(int windowId, Map argument) { if (argument['name'] == 'RequestEditor') { return RequestEditor( - windowController: WindowController.fromWindowId(windowId), - request: HttpRequest.fromJson(argument['request']), - proxyPort: argument['proxyPort']); + windowController: WindowController.fromWindowId(windowId), request: HttpRequest.fromJson(argument['request'])); } if (argument['name'] == 'HttpBodyWidget') { @@ -84,8 +76,26 @@ class FluentApp extends StatelessWidget { @override Widget build(BuildContext context) { - var lightTheme = ThemeData.light(useMaterial3: true); + var lightTheme = ThemeData.light(useMaterial3: !Platforms.isDesktop()); var darkTheme = ThemeData.dark(useMaterial3: !Platforms.isDesktop()); + + if (!lightTheme.useMaterial3) { + lightTheme = lightTheme.copyWith( + expansionTileTheme: lightTheme.expansionTileTheme.copyWith( + textColor: lightTheme.textTheme.titleMedium?.color, + ), + appBarTheme: lightTheme.appBarTheme.copyWith( + color: Colors.transparent, + elevation: 0, + titleTextStyle: lightTheme.textTheme.titleMedium, + iconTheme: lightTheme.iconTheme, + ), + tabBarTheme: lightTheme.tabBarTheme.copyWith( + labelColor: lightTheme.indicatorColor, + unselectedLabelColor: lightTheme.textTheme.titleMedium?.color, + )); + } + if (Platform.isWindows) { lightTheme = lightTheme.useSystemChineseFont(); darkTheme = darkTheme.useSystemChineseFont(); @@ -105,77 +115,3 @@ class FluentApp extends StatelessWidget { }); } } - -class DesktopHomePage extends StatefulWidget { - final Configuration configuration; - - const DesktopHomePage({super.key, required this.configuration}); - - @override - State createState() => _DesktopHomePagePageState(); -} - -class _DesktopHomePagePageState extends State implements EventListener { - final domainStateKey = GlobalKey(); - - late ProxyServer proxyServer; - late NetworkTabController panel; - - @override - void onRequest(Channel channel, HttpRequest request) { - domainStateKey.currentState!.add(channel, request); - } - - @override - void onResponse(Channel channel, HttpResponse response) { - domainStateKey.currentState!.addResponse(channel, response); - } - - @override - void initState() { - super.initState(); - proxyServer = ProxyServer(widget.configuration, listener: this); - panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18), proxyServer: proxyServer); - - if (widget.configuration.guide) { - WidgetsBinding.instance.addPostFrameCallback((_) { - //首次引导 - showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - return AlertDialog( - actions: [ - TextButton( - onPressed: () { - widget.configuration.guide = false; - widget.configuration.flushConfig(); - Navigator.pop(context); - }, - child: const Text('关闭')) - ], - title: const Text('提示', style: TextStyle(fontSize: 18)), - content: const Text( - '默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' - '点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n' - '新增更新:\n' - '1. 增加高级搜索,点击搜索Icon触发。\n' - '2. 显示SSL握手异常、建立连接异常、未知异常等请求。\n' - '3.响应体大时异步加载json,请求重写增加域名,修复手机扫码连接未开启代理时不转发问题', - style: TextStyle(fontSize: 14))); - }); - }); - } - } - - @override - Widget build(BuildContext context) { - final domainWidget = DomainWidget(key: domainStateKey, proxyServer: proxyServer, panel: panel); - - return Scaffold( - appBar: Tab( - child: Toolbar(proxyServer, domainStateKey), - ), - body: VerticalSplitView(ratio: 0.3, minRatio: 0.15, maxRatio: 0.9, left: domainWidget, right: panel)); - } -} diff --git a/lib/network/bin/configuration.dart b/lib/network/bin/configuration.dart index 2a67091..4ecaa74 100644 --- a/lib/network/bin/configuration.dart +++ b/lib/network/bin/configuration.dart @@ -71,8 +71,11 @@ class Configuration { await _loadConfig(); } + String? userHome; Future homeDir() async { - String? userHome; + if (userHome != null) { + return File("${userHome!}${Platform.pathSeparator}.proxypin"); + } if (Platforms.isDesktop()) { userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; } else { diff --git a/lib/network/http/http_parser.dart b/lib/network/http/http_parser.dart index 22d1b57..ae8ce33 100644 --- a/lib/network/http/http_parser.dart +++ b/lib/network/http/http_parser.dart @@ -5,7 +5,7 @@ import 'package:network_proxy/network/http/http_headers.dart'; /// http解析器 class HttpParse { - final _HeaderParse __headerParse = _HeaderParse(); + static const int defaultMaxLength = 40960; /// 解析请求行 List parseInitialLine(ByteBuf data, int size) { @@ -28,10 +28,6 @@ class HttpParse { return initialLine; } - bool parseHeaders(ByteBuf data, HttpHeaders headers) { - return __headerParse.parseHeader(data, headers); - } - //分割行 List _splitLine(Uint8List data) { List lines = []; @@ -48,18 +44,10 @@ class HttpParse { lines.add(String.fromCharCodes(data.sublist(start))); return lines; } -} -//是否行结束 -bool _isLineEnd(ByteBuf data, int index) { - return index + 1 < data.length && data.get(index) == HttpConstants.cr && data.get(index + 1) == HttpConstants.lf; -} - -class _HeaderParse { - static const int defaultMaxLength = 40960; /// 解析请求头 - bool parseHeader(ByteBuf data, HttpHeaders headers) { + bool parseHeaders(ByteBuf data, HttpHeaders headers) { if (!data.isReadable()) { return false; } @@ -84,6 +72,11 @@ class _HeaderParse { return _isLineEnd(data, data.readerIndex - 4) && _isLineEnd(data, data.readerIndex - 2); } + //是否行结束 + bool _isLineEnd(ByteBuf data, int index) { + return index + 1 < data.length && data.get(index) == HttpConstants.cr && data.get(index + 1) == HttpConstants.lf; + } + //分割头 List _splitHeader(List data) { List headers = []; @@ -97,3 +90,4 @@ class _HeaderParse { return headers; } } + diff --git a/lib/network/http_client.dart b/lib/network/http_client.dart index 69c1329..288d736 100644 --- a/lib/network/http_client.dart +++ b/lib/network/http_client.dart @@ -20,6 +20,8 @@ import 'dart:io'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/network.dart'; +import 'package:network_proxy/network/util/system_proxy.dart'; +import 'package:proxy_manager/proxy_manager.dart'; import 'channel.dart'; import 'http/codec.dart'; @@ -98,13 +100,18 @@ class HttpClients { } /// 发送代理请求 - static Future proxyRequest(String proxyHost, int port, HttpRequest request, - {Duration timeout = const Duration(seconds: 3)}) async { + static Future proxyRequest(HttpRequest request, + {ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 3)}) async { + if (proxyInfo == null) { + var proxyTypes = request.uri.startsWith("https://") ? ProxyTypes.https : ProxyTypes.http; + proxyInfo = await SystemProxy.getSystemProxy(proxyTypes); + } + print(proxyInfo); var httpResponseHandler = HttpResponseHandler(); HostAndPort hostPort = HostAndPort.of(request.uri); - Channel channel = await proxyConnect(proxyInfo: ProxyInfo.of(proxyHost, port), hostPort, httpResponseHandler); + Channel channel = await proxyConnect(proxyInfo: proxyInfo, hostPort, httpResponseHandler); if (hostPort.isSsl()) { channel.secureSocket = await SecureSocket.secure(channel.socket, onBadCertificate: (certificate) => true); diff --git a/lib/network/util/system_proxy.dart b/lib/network/util/system_proxy.dart index a076b70..80cc2f2 100644 --- a/lib/network/util/system_proxy.dart +++ b/lib/network/util/system_proxy.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/utils/ip.dart'; import 'package:proxy_manager/proxy_manager.dart'; @@ -8,6 +9,17 @@ import 'package:proxy_manager/proxy_manager.dart'; class SystemProxy { static String? _hardwarePort; + ///获取系统代理 + static Future getSystemProxy(ProxyTypes proxyTypes) async { + if (Platform.isWindows) { + return await _getSystemProxyWindows(); + } else if (Platform.isMacOS) { + return await _getSystemProxyMacOS(proxyTypes); + } else { + return null; + } + } + /// 设置系统代理 static Future setSystemProxy(int port, bool sslSetting) async { if (Platform.isMacOS) { @@ -23,7 +35,8 @@ class SystemProxy { } } - /// 设置系统代理 @param sslSetting 是否设置https代理只在mac中有效 + /// 设置系统代理 + /// @param sslSetting 是否设置https代理只在mac中有效 static Future setSystemProxyEnable(int port, bool enable, bool sslSetting) async { //启用系统代理 if (enable) { @@ -55,6 +68,36 @@ class SystemProxy { return results.exitCode == 0; } + static Future getProxyEnable() async { + _hardwarePort ??= await hardwarePort(); + try { + var results = await Process.run('bash', ['-c', 'networksetup -getwebproxy $_hardwarePort']); + var proxyEnableLine = + (results.stdout as String).split('\n').where((item) => item.contains('Enabled')).first.trim(); + return proxyEnableLine.endsWith('Yes'); + } catch (e) { + print(e); + return false; + } + } + + ///获取系统代理 + static Future _getSystemProxyMacOS(ProxyTypes proxyTypes) async { + _hardwarePort = await hardwarePort(); + var result = await Process.run('bash', [ + '-c', + 'networksetup ${proxyTypes == ProxyTypes.http ? '-getwebproxy' : '-getsecurewebproxy'} $_hardwarePort', + ]).then((results) => results.stdout.toString().split('\n')); + + var proxyEnable = result.firstWhere((item) => item.contains('Enabled')).trim().split(": ")[1]; + var proxyServer = result.firstWhere((item) => item.contains('Server')).trim().split(": ")[1]; + var proxyPort = result.firstWhere((item) => item.contains('Port')).trim().split(": ")[1]; + if (proxyEnable == 'Yes' && proxyServer.isNotEmpty) { + return ProxyInfo.of(proxyServer, int.parse(proxyPort)); + } + return null; + } + static Future setProxyEnableMacOS(bool proxyEnable, bool sslSetting) async { var proxyMode = proxyEnable ? 'on' : 'off'; _hardwarePort ??= await hardwarePort(); @@ -130,6 +173,34 @@ class SystemProxy { return results.exitCode == 0; } + /// 获取系统代理 + static Future _getSystemProxyWindows() async { + var results = await Process.run('reg', [ + 'query', + 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', + '/v', + 'ProxyEnable', + ]).then((it) => it.stdout.toString()); + + var proxyEnableLine = results.split('\r\n').where((item) => item.contains('ProxyEnable')).first; + if (proxyEnableLine.substring(proxyEnableLine.length - 1) != '1') { + return null; + } + + return Process.run('reg', [ + 'query', + 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', + '/v', + 'ProxyServer', + ]).then((results) { + var proxyServerLine = + (results.stdout as String).split('\r\n').where((item) => item.contains('ProxyServer')).first; + var proxyServerLineSplits = proxyServerLine.split(RegExp(r"\s+")); + proxyServerLineSplits[proxyServerLineSplits.length - 1]; + return null; + }); + } + static _concatCommands(List commands) { return commands.where((element) => element.isNotEmpty).join(' && '); } diff --git a/lib/storage/favorites.dart b/lib/storage/favorites.dart new file mode 100644 index 0000000..0e1b7ea --- /dev/null +++ b/lib/storage/favorites.dart @@ -0,0 +1,85 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:network_proxy/network/http/http.dart'; +import 'package:path_provider/path_provider.dart'; + +class FavoriteStorage { + static Queue? _requests; + + static Future> get favorites async { + if (_requests == null) { + var file = await _path; + print(file); + _requests = ListQueue(); + if (await file.exists()) { + var value = await file.readAsString(); + + try { + var list = jsonDecode(value) as List; + for (var element in list) { + _requests!.add(_Item.fromJson(element).request); + } + } catch (e) { + print(e); + } + } + } + return _requests!; + } + + static Future get _path async { + final directory = await getApplicationSupportDirectory(); + var file = File('${directory.path}${Platform.pathSeparator}favorites.json'); + if (!await file.exists()) { + await file.create(); + } + return file; + } + + static Future addFavorite(HttpRequest request) async { + var favorites = await FavoriteStorage.favorites; + if (favorites.contains(request)) { + return; + } + + favorites.addFirst(request); + _path.then((file) async { + file.writeAsString(jsonEncode(toJson(favorites))); + }); + } + + static Future removeFavorite(HttpRequest request) async { + var list = await favorites; + list.remove(request); + + _path.then((file) => file.writeAsString(jsonEncode(toJson(list)))); + } + + static List toJson(Queue list) { + return list.map((e) => _Item(e).toJson()).toList(); + } +} + +class _Item { + final HttpRequest request; + HttpResponse? response; + + _Item(this.request, [this.response]) { + response ??= request.response; + request.response = response; + } + + factory _Item.fromJson(Map json) { + return _Item(HttpRequest.fromJson(json['request']), + json['response'] == null ? null : HttpResponse.fromJson(json['response'])); + } + + toJson() { + return { + 'request': request.toJson(), + 'response': response?.toJson(), + }; + } +} diff --git a/lib/ui/component/split_view.dart b/lib/ui/component/split_view.dart index f7bf937..04172a3 100644 --- a/lib/ui/component/split_view.dart +++ b/lib/ui/component/split_view.dart @@ -57,7 +57,7 @@ class _VerticalSplitViewState extends State { height: double.infinity, child: (_ratio <= 0 || _ratio >= 1) ? const Icon(Icons.drag_handle, size: 16) - : const VerticalDivider(thickness: 2), + : const VerticalDivider(thickness: 1), )), onPanUpdate: (DragUpdateDetails details) { setState(() { diff --git a/lib/ui/component/state_component.dart b/lib/ui/component/state_component.dart new file mode 100644 index 0000000..5eacf47 --- /dev/null +++ b/lib/ui/component/state_component.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class StateComponent extends StatefulWidget { + final Widget child; + final Function? onChange; + + const StateComponent(this.child, {Key? key, this.onChange }) : super(key: key); + + @override + State createState() { + return _StateComponentState(); + } +} + +class _StateComponentState extends State { + void changeState() { + setState(() {}); + if (widget.onChange != null) { + widget.onChange!(); + } + } + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/ui/content/body.dart b/lib/ui/content/body.dart index 9e1bf8e..2e9d2d9 100644 --- a/lib/ui/content/body.dart +++ b/lib/ui/content/body.dart @@ -77,7 +77,11 @@ class HttpBodyState extends State { }), Padding( padding: const EdgeInsets.all(10), - child: _Body(key: bodyKey, message: widget.httpMessage, viewType: tabs.list[tabIndex], scrollController:widget.scrollController)) //body + child: _Body( + key: bodyKey, + message: widget.httpMessage, + viewType: tabs.list[tabIndex], + scrollController: widget.scrollController)) //body ]; var tabController = DefaultTabController( @@ -101,7 +105,7 @@ class HttpBodyState extends State { return Row( mainAxisAlignment: widget.inNewWindow ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ - Text('$type Body', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + Text('$type Body', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), const SizedBox(width: 15), IconButton( icon: const Icon(Icons.copy), @@ -208,7 +212,10 @@ class _BodyState extends State<_Body> { try { if (type == ViewType.jsonText) { var jsonObject = json.decode(message!.bodyAsString); - return JsonText(json: jsonObject, colorTheme: ColorTheme.of(Theme.of(context).brightness), scrollController: widget.scrollController); + return JsonText( + json: jsonObject, + colorTheme: ColorTheme.of(Theme.of(context).brightness), + scrollController: widget.scrollController); } if (type == ViewType.json) { diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index c027c65..6774dd4 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -178,7 +178,7 @@ class NetworkTabState extends State with SingleTickerProvi Widget headerWidget = ExpansionTile( tilePadding: const EdgeInsets.only(left: 0), - title: Text("$type Headers", style: const TextStyle(fontWeight: FontWeight.bold)), + title: Text("$type Headers", style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), initiallyExpanded: true, shape: const Border(), children: headers); @@ -188,7 +188,7 @@ class NetworkTabState extends State with SingleTickerProvi Widget expansionTile(String title, List content) { return ExpansionTile( - title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + title: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), tilePadding: const EdgeInsets.only(left: 0), expandedAlignment: Alignment.topLeft, initiallyExpanded: true, diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart new file mode 100644 index 0000000..bc44a14 --- /dev/null +++ b/lib/ui/desktop/desktop.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/configuration.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/channel.dart'; +import 'package:network_proxy/network/handler.dart'; +import 'package:network_proxy/network/http/http.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/favorite.dart'; +import 'package:network_proxy/ui/desktop/toolbar/toolbar.dart'; + +import '../component/split_view.dart'; + +class DesktopHomePage extends StatefulWidget { + final Configuration configuration; + + const DesktopHomePage({super.key, required this.configuration}); + + @override + State createState() => _DesktopHomePagePageState(); +} + +class _DesktopHomePagePageState extends State implements EventListener { + final domainStateKey = GlobalKey(); + final PageController pageController = PageController(); + final ValueNotifier _selectIndex = ValueNotifier(0); + + late ProxyServer proxyServer; + late NetworkTabController panel; + + final List destinations = const [ + NavigationRailDestination(icon: Icon(Icons.workspaces), label: Text("抓包", style: TextStyle(fontSize: 12))), + // NavigationRailDestination(icon: Icon(Icons.history), label: Text("历史", style: TextStyle(fontSize: 12))), + NavigationRailDestination(icon: Icon(Icons.favorite), label: Text("收藏", style: TextStyle(fontSize: 12))), + // NavigationRailDestination(icon: Icon(Icons.construction), label: Text("工具箱", style: TextStyle(fontSize: 12))), + ]; + + @override + void onRequest(Channel channel, HttpRequest request) { + domainStateKey.currentState!.add(channel, request); + } + + @override + void onResponse(Channel channel, HttpResponse response) { + domainStateKey.currentState!.addResponse(channel, response); + } + + @override + void initState() { + super.initState(); + proxyServer = ProxyServer(widget.configuration, listener: this); + panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18), proxyServer: proxyServer); + + if (widget.configuration.guide) { + WidgetsBinding.instance.addPostFrameCallback((_) { + guideDialog(); + }); + } + } + + @override + Widget build(BuildContext context) { + final domainWidget = DomainWidget(key: domainStateKey, proxyServer: proxyServer, panel: panel); + + return Scaffold( + appBar: Tab( + child: Toolbar(proxyServer, domainStateKey), + ), + body: Row( + children: [ + ValueListenableBuilder(valueListenable: _selectIndex, builder: (_, index, __) => leftNavigation(index)), + const VerticalDivider(thickness: 0.3), + Expanded( + child: VerticalSplitView( + ratio: 0.3, + minRatio: 0.15, + maxRatio: 0.9, + left: PageView( + controller: pageController, + children: [domainWidget, Favorites(panel: panel)]), + right: panel), + ) + ], + )); + } + + Widget leftNavigation(int index) { + return NavigationRail( + minWidth: 35, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + labelType: NavigationRailLabelType.all, + destinations: destinations, + selectedIndex: index, + onDestinationSelected: (int index) { + pageController.jumpToPage(index); + _selectIndex.value = index; + }); + } + + //首次引导 + guideDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + return AlertDialog( + actions: [ + TextButton( + onPressed: () { + widget.configuration.guide = false; + widget.configuration.flushConfig(); + Navigator.pop(context); + }, + child: const Text('关闭')) + ], + title: const Text('提示', style: TextStyle(fontSize: 18)), + content: const Text( + '默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' + '点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n' + '新增更新:\n' + '1. 增加高级搜索,点击搜索Icon触发。\n' + '2. 显示SSL握手异常、建立连接异常、未知异常等请求。\n' + '3.响应体大时异步加载json,请求重写增加域名,修复手机扫码连接未开启代理时不转发问题', + style: TextStyle(fontSize: 14))); + }); + } +} diff --git a/lib/ui/desktop/left/domain.dart b/lib/ui/desktop/left/domain.dart index 5fb52cd..282fa2f 100644 --- a/lib/ui/desktop/left/domain.dart +++ b/lib/ui/desktop/left/domain.dart @@ -28,7 +28,7 @@ class DomainWidget extends StatefulWidget { } } -class DomainWidgetState extends State { +class DomainWidgetState extends State with AutomaticKeepAliveClientMixin{ LinkedHashMap containerMap = LinkedHashMap(); LinkedHashMap searchView = LinkedHashMap(); @@ -47,8 +47,12 @@ class DomainWidgetState extends State { } } + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); var list = containerMap.values; //根究搜素文本过滤 diff --git a/lib/ui/desktop/left/favorite.dart b/lib/ui/desktop/left/favorite.dart new file mode 100644 index 0000000..ac9b953 --- /dev/null +++ b/lib/ui/desktop/left/favorite.dart @@ -0,0 +1,179 @@ +import 'dart:collection'; +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'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/http_client.dart'; +import 'package:network_proxy/storage/favorites.dart'; +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:window_manager/window_manager.dart'; + +class Favorites extends StatefulWidget { + final NetworkTabController panel; + + const Favorites({Key? key, required this.panel}) : super(key: key); + + @override + State createState() { + return _FavoritesState(); + } +} + +class _FavoritesState extends State { + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: FavoriteStorage.favorites, + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + var favorites = snapshot.data ?? Queue(); + return ListView.separated( + itemCount: favorites.length, + itemBuilder: (_, index) { + var request = favorites.elementAt(index); + return _FavoriteItem( + request, + panel: widget.panel, + onRemove: (HttpRequest request) { + FavoriteStorage.removeFavorite(request); + FlutterToastr.show('已删除收藏', context); + setState(() {}); + }, + ); + }, + separatorBuilder: (_, __) => const Divider(height: 2, thickness: 0.5), + ); + } else { + return const SizedBox(); + } + }); + } +} + +class _FavoriteItem extends StatefulWidget { + final HttpRequest request; + final NetworkTabController panel; + final Function(HttpRequest request)? onRemove; + + const _FavoriteItem(this.request, {Key? key, required this.panel, required this.onRemove}) : super(key: key); + + @override + State<_FavoriteItem> createState() => _FavoriteItemState(); +} + +class _FavoriteItemState extends State<_FavoriteItem> { + //选择的节点 + static _FavoriteItemState? selectedState; + + bool selected = false; + + @override + Widget build(BuildContext context) { + var request = widget.request; + var response = request.response; + var title = '${request.method.name} ${request.requestUrl}'; + var time = formatDate(request.requestTime, [mm, '-', d, ' ', HH, ':', nn, ':', ss]); + return GestureDetector( + onSecondaryLongPressDown: menu, + child: ListTile( + minLeadingWidth: 25, + leading: getIcon(response), + title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 2), + subtitle: Text( + '$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ', + maxLines: 1), + selected: selected, + dense: true, + onTap: onClick)); + } + + ///右键菜单 + menu(LongPressDownDetails details) { + showMenu( + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy, + ), + items: [ + popupItem("复制请求链接", onTap: () { + var requestUrl = widget.request.requestUrl; + Clipboard.setData(ClipboardData(text: requestUrl)).then((value) => FlutterToastr.show('已复制到剪切板', context)); + }), + popupItem("复制请求和响应", onTap: () { + Clipboard.setData(ClipboardData(text: copyRequest(widget.request, widget.request.response))) + .then((value) => FlutterToastr.show('已复制到剪切板', context)); + }), + popupItem("复制 cURL 请求", onTap: () { + Clipboard.setData(ClipboardData(text: curlRequest(widget.request))) + .then((value) => FlutterToastr.show('已复制到剪切板', context)); + }), + popupItem("重放请求", onTap: () { + var request = widget.request.copy(uri: widget.request.requestUrl); + HttpClients.proxyRequest(request); + + FlutterToastr.show('已重新发送请求', context); + }), + popupItem("编辑请求", onTap: () { + WidgetsBinding.instance.addPostFrameCallback((_) { + requestEdit(); + }); + }), + popupItem("删除收藏", onTap: () { + widget.onRemove?.call(widget.request); + }) + ], + ); + } + + PopupMenuItem popupItem(String text, {VoidCallback? onTap}) { + return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14))); + } + + ///请求编辑 + requestEdit() async { + var size = MediaQuery.of(context).size; + var ratio = 1.0; + if (Platform.isWindows) { + ratio = WindowManager.instance.getDevicePixelRatio(); + } + + final window = await DesktopMultiWindow.createWindow(jsonEncode( + {'name': 'RequestEditor', 'request': widget.request}, + )); + window.setTitle('请求编辑'); + window + ..setFrame(const Offset(100, 100) & Size(960 * ratio, size.height * ratio)) + ..center() + ..show(); + } + + //点击事件 + void onClick() { + if (selected) { + return; + } + setState(() { + selected = true; + }); + + //切换选中的节点 + if (selectedState?.mounted == true && selectedState != this) { + selectedState?.setState(() { + selectedState?.selected = false; + }); + } + selectedState = this; + widget.panel.change(widget.request, widget.request.response); + } +} diff --git a/lib/ui/desktop/left/path.dart b/lib/ui/desktop/left/path.dart index 532ab6f..f9c5d32 100644 --- a/lib/ui/desktop/left/path.dart +++ b/lib/ui/desktop/left/path.dart @@ -10,6 +10,7 @@ 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_client.dart'; +import 'package:network_proxy/storage/favorites.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/utils/curl.dart'; @@ -48,8 +49,11 @@ class _PathRowState extends State { @override Widget build(BuildContext context) { var request = widget.request; - var response = widget.response.get(); - var title = '${request.method.name} ${Uri.parse(request.uri).path}'; + var response = widget.response.get() ?? request.response; + String title = '${request.method.name} ${request.uri}'; + try { + title = '${request.method.name} ${Uri.parse(request.uri).path}'; + } catch (_) {} var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]); return GestureDetector( onSecondaryLongPressDown: menu, @@ -62,7 +66,7 @@ class _PathRowState extends State { maxLines: 1), selected: selected, dense: true, - contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50.0), + contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 42.0), onTap: onClick)); } @@ -81,59 +85,45 @@ class _PathRowState extends State { details.globalPosition.dy, ), items: [ - PopupMenuItem( - height: 38, - child: const Text("复制请求链接", style: TextStyle(fontSize: 14)), - onTap: () { - var requestUrl = widget.request.requestUrl; - Clipboard.setData(ClipboardData(text: requestUrl)) - .then((value) => FlutterToastr.show('已复制到剪切板', context)); - }), - PopupMenuItem( - height: 38, - child: const Text("复制请求和响应", style: TextStyle(fontSize: 14)), - onTap: () { - Clipboard.setData(ClipboardData(text: copyRequest(widget.request, widget.response.get()))) - .then((value) => FlutterToastr.show('已复制到剪切板', context)); - }), - PopupMenuItem( - height: 38, - child: const Text("复制 cURL 请求", style: TextStyle(fontSize: 14)), - onTap: () { - Clipboard.setData(ClipboardData(text: curlRequest(widget.request))) - .then((value) => FlutterToastr.show('已复制到剪切板', context)); - }), - PopupMenuItem( - 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); + popupItem("复制请求链接", onTap: () { + var requestUrl = widget.request.requestUrl; + Clipboard.setData(ClipboardData(text: requestUrl)).then((value) => FlutterToastr.show('已复制到剪切板', context)); + }), + popupItem("复制请求和响应", onTap: () { + Clipboard.setData(ClipboardData(text: copyRequest(widget.request, widget.response.get()))) + .then((value) => FlutterToastr.show('已复制到剪切板', context)); + }), + popupItem("复制 cURL 请求", onTap: () { + Clipboard.setData(ClipboardData(text: curlRequest(widget.request))) + .then((value) => FlutterToastr.show('已复制到剪切板', context)); + }), + popupItem("重放请求", onTap: () { + var request = widget.request.copy(uri: widget.request.requestUrl); + HttpClients.proxyRequest(request); - FlutterToastr.show('已重新发送请求', context); - }), - PopupMenuItem( - height: 38, - child: const Text("编辑重放请求", style: TextStyle(fontSize: 14)), - onTap: () { - WidgetsBinding.instance.addPostFrameCallback((_) { - requestEdit(); - }); - }), - PopupMenuItem( - height: 38, - child: const Text("删除", style: TextStyle(fontSize: 14)), - onTap: () { - widget.remove?.call(widget); - }), + FlutterToastr.show('已重新发送请求', context); + }), + popupItem("编辑请求", onTap: () { + WidgetsBinding.instance.addPostFrameCallback((_) { + requestEdit(); + }); + }), + popupItem("收藏请求", onTap: () { + FavoriteStorage.addFavorite(widget.request); + FlutterToastr.show('收藏成功', context); + }), + popupItem("删除", onTap: () { + widget.remove?.call(widget); + }), ], ); } + PopupMenuItem popupItem(String text, {VoidCallback? onTap}) { + return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14))); + } + + ///请求编辑 requestEdit() async { var size = MediaQuery.of(context).size; var ratio = 1.0; @@ -146,7 +136,7 @@ class _PathRowState extends State { )); window.setTitle('请求编辑'); window - ..setFrame(const Offset(100, 100) & Size(860 * ratio, size.height * ratio)) + ..setFrame(const Offset(100, 100) & Size(960 * ratio, size.height * ratio)) ..center() ..show(); } diff --git a/lib/ui/desktop/left/request_editor.dart b/lib/ui/desktop/left/request_editor.dart index 5bd35ff..68f7c6f 100644 --- a/lib/ui/desktop/left/request_editor.dart +++ b/lib/ui/desktop/left/request_editor.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -7,13 +8,13 @@ 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'; +import 'package:network_proxy/ui/component/split_view.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}); + const RequestEditor({super.key, this.request, this.windowController}); @override State createState() { @@ -23,15 +24,14 @@ class RequestEditor extends StatefulWidget { class RequestEditorState extends State { final requestLineKey = GlobalKey<_RequestLineState>(); - final headerKey = GlobalKey(); - - String requestBody = ""; + final requestKey = GlobalKey<_HttpState>(); + ValueNotifier responseChange = ValueNotifier(false); + HttpResponse? response; @override void initState() { super.initState(); RawKeyboard.instance.addListener(onKeyEvent); - requestBody = widget.request?.bodyAsString ?? ''; } void onKeyEvent(RawKeyEvent event) { @@ -52,45 +52,139 @@ class RequestEditorState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("请求编辑", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + 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("发送")) + TextButton.icon( + onPressed: () async => sendRequest(), icon: const Icon(Icons.send), label: const Text("发送")), + const SizedBox(width: 10) ], ), - 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() // 请求体 - ]))); + body: Column(children: [ + _RequestLine(key: requestLineKey, request: widget.request), + Expanded( + child: VerticalSplitView( + ratio: 0.53, + left: _HttpWidget( + key: requestKey, + title: const Text("Request", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + message: widget.request), + right: ValueListenableBuilder( + valueListenable: responseChange, + builder: (_, value, __) => _HttpWidget( + title: Row(children: [ + const Text("Response", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const Spacer(), + Text(response?.status.toString() ?? '', style: const TextStyle(fontSize: 14)) + ]), + message: response, + readOnly: true)), + )), + ])); } ///发送请求 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); + var headers = requestKey.currentState?.getHeaders(); + var requestBody = requestKey.currentState?.getBody(); - FlutterToastr.show('已重新发送请求', context); - RawKeyboard.instance.removeListener(onKeyEvent); - await Future.delayed(const Duration(milliseconds: 500), () => widget.windowController?.close()); + HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), currentState.requestUrl); + request.headers.addAll(headers); + request.body = requestBody?.codeUnits; + + HttpClients.proxyRequest(request).then((response) { + this.response = response; + responseChange.value = !responseChange.value; + }); + + if (context.mounted) { + FlutterToastr.show('已发送请求', context); + } + } +} + +class _HttpWidget extends StatefulWidget { + final HttpMessage? message; + final bool readOnly; + final Widget title; + + const _HttpWidget({this.message, this.readOnly = false, super.key, required this.title}); + + @override + State createState() { + return _HttpState(); + } +} + +class _HttpState extends State<_HttpWidget> { + final tabs = ['Header', 'Body']; + final headerKey = GlobalKey(); + String? body; + + @override + void initState() { + super.initState(); } - Widget body() { + String? getBody() { + return body; + } + + HttpHeaders? getHeaders() { + return headerKey.currentState?.getHeaders(); + } + + @override + Widget build(BuildContext context) { + body = widget.message?.bodyAsString; + headerKey.currentState?.refreshHeader(widget.message?.headers); + + if (widget.message == null && widget.readOnly) { + return Scaffold(appBar: AppBar(title: widget.title), body: const Center(child: Text("无数据"))); + } + + return SingleChildScrollView( + child: SizedBox( + height: MediaQuery.of(context).size.height - 120, + child: DefaultTabController( + length: tabs.length, + child: Scaffold( + primary: false, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(70), + child: AppBar( + title: widget.title, + bottom: TabBar(tabs: tabs.map((e) => Tab(text: e, height: 35)).toList()), + )), + body: Padding( + padding: const EdgeInsets.only(left: 10), + child: TabBarView( + children: [ + Headers(key: headerKey, headers: widget.message?.headers, readOnly: widget.readOnly), + _body() + ], + )), + )))); + } + + Widget _body() { + if (body != null && widget.readOnly && widget.message?.contentType == ContentType.json) { + try { + body = const JsonEncoder.withIndent(' ').convert(const JsonDecoder().convert(body!)); + } catch (_) {} + } + return TextField( - controller: TextEditingController(text: requestBody), + autofocus: true, + controller: TextEditingController(text: body), + readOnly: widget.readOnly, onChanged: (value) { - requestBody = value; + body = value; }, - minLines: 3, - maxLines: 10); + minLines: 20, + maxLines: 20); } } @@ -116,6 +210,7 @@ class _RequestLineState extends State<_RequestLine> { if (widget.request == null) { return; } + var request = widget.request!; requestUrl = request.requestUrl; requestMethod = request.method.name; @@ -151,8 +246,9 @@ class _RequestLineState extends State<_RequestLine> { ///请求头 class Headers extends StatefulWidget { final HttpHeaders? headers; + final bool readOnly; //只读 - const Headers({super.key, this.headers}); + const Headers({super.key, this.headers, this.readOnly = false}); @override State createState() { @@ -160,8 +256,11 @@ class Headers extends StatefulWidget { } } -class HeadersState extends State { - Map> headers = {}; +class HeadersState extends State with AutomaticKeepAliveClientMixin { + final Map> _headers = {}; + + @override + bool get wantKeepAlive => true; @override void initState() { @@ -170,14 +269,24 @@ class HeadersState extends State { return; } widget.headers?.forEach((name, values) { - headers[TextEditingController(text: name)] = values.map((it) => TextEditingController(text: it)).toList(); + _headers[TextEditingController(text: name)] = values.map((it) => TextEditingController(text: it)).toList(); + }); + } + + //刷新header + refreshHeader(HttpHeaders? headers) { + _headers.clear(); + setState(() { + 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) { + _headers.forEach((name, values) { if (name.text.isEmpty) { return; } @@ -192,66 +301,71 @@ class HeadersState extends State { @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)) - ]), - ])); + super.build(context); + + var list = [ + _row(const Text('Key'), const Text('Value'), const Text('')), + ..._buildRows(), + ]; + + if (!widget.readOnly) { + list.add(TextButton( + child: const Text("添加Header", textAlign: TextAlign.center), + onPressed: () { + setState(() { + _headers[TextEditingController()] = [TextEditingController()]; + }); + }, + )); + } + return Scaffold( + body: Padding( + padding: const EdgeInsets.only(top: 10), + child: ListView.separated( + separatorBuilder: (context, index) => + index == list.length ? const SizedBox() : const Divider(thickness: 0.2), + itemBuilder: (context, index) => list[index], + itemCount: list.length))); } - List buildRows() { - var width = MediaQuery.of(context).size.width; - List list = []; + List _buildRows() { + List list = []; - headers.forEach((key, values) { + _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))) - ])); + list.add(_row( + _cell(key, isKey: true), + _cell(val), + widget.readOnly + ? null + : Padding( + padding: const EdgeInsets.only(right: 15), + child: InkWell( + onTap: () { + setState(() { + _headers.remove(key); + }); + }, + child: const Icon(Icons.remove_circle, size: 16))))); } }); return list; } - DataCell cell(TextEditingController val, {TextStyle? style = const TextStyle(fontSize: 14), double? width}) { - return DataCell(SizedBox( - width: width, + Widget _cell(TextEditingController val, {bool isKey = false}) { + return Container( + padding: const EdgeInsets.only(right: 5), child: TextFormField( - style: style, + readOnly: widget.readOnly, + style: TextStyle(fontSize: 12, fontWeight: isKey ? FontWeight.w500 : null), controller: val, - decoration: const InputDecoration(isDense: true, border: InputBorder.none, hintText: "Header")))); + minLines: 1, + maxLines: 3, + decoration: InputDecoration(isDense: true, border: InputBorder.none, hintText: isKey ? "Key" : "Value"))); + } + + Widget _row(Widget key, Widget val, Widget? op) { + return Row(children: [Expanded(flex: 4, child: key), Expanded(flex: 6, child: val), op ?? const SizedBox()]); } } diff --git a/lib/ui/desktop/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart index b41afcf..f9681a9 100644 --- a/lib/ui/desktop/toolbar/toolbar.dart +++ b/lib/ui/desktop/toolbar/toolbar.dart @@ -59,31 +59,44 @@ class _ToolbarState extends State { @override Widget build(BuildContext context) { - return Row( - children: [ - Padding(padding: EdgeInsets.only(left: Platform.isMacOS ? 80 : 30)), - SocketLaunch(proxyServer: widget.proxyServer), - const Padding(padding: EdgeInsets.only(left: 20)), - IconButton( - tooltip: "清理", - icon: const Icon(Icons.cleaning_services_outlined), - onPressed: () { - widget.domainStateKey.currentState?.clean(); - }), - const Padding(padding: EdgeInsets.only(left: 20)), - SslWidget(proxyServer: widget.proxyServer), - const Padding(padding: EdgeInsets.only(left: 20)), - Setting(proxyServer: widget.proxyServer), - const Padding(padding: EdgeInsets.only(left: 20)), - IconButton( - tooltip: "手机连接", - icon: const Icon(Icons.phone_iphone), - onPressed: () async { - final ips = await localIps(); - phoneConnect(ips, widget.proxyServer.port); - }), - ], - ); + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor, width: 0.1), + )), + child: Row( + children: [ + Padding(padding: EdgeInsets.only(left: Platform.isMacOS ? 80 : 30)), + SocketLaunch(proxyServer: widget.proxyServer), + const Padding(padding: EdgeInsets.only(left: 20)), + IconButton( + tooltip: "清理", + icon: const Icon(Icons.cleaning_services_outlined), + onPressed: () { + widget.domainStateKey.currentState?.clean(); + }), + const Padding(padding: EdgeInsets.only(left: 20)), + SslWidget(proxyServer: widget.proxyServer), + const Padding(padding: EdgeInsets.only(left: 20)), + Setting(proxyServer: widget.proxyServer), + const Padding(padding: EdgeInsets.only(left: 20)), + IconButton( + tooltip: "手机连接", + icon: const Icon(Icons.phone_iphone), + onPressed: () async { + final ips = await localIps(); + phoneConnect(ips, widget.proxyServer.port); + }), + const Expanded(child: SizedBox()), //自动扩展挤压 + IconButton( + icon: const Icon(Icons.space_dashboard, size: 20, color: Colors.blueGrey), + onPressed: () { + + }, + ), //右对齐 + const Padding(padding: EdgeInsets.only(left: 30)), + ], + )); } phoneConnect(List hosts, int port) { diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index 744e5aa..9a8a0b8 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -196,7 +196,7 @@ class RequestSequenceState extends State with AutomaticKeepAliv return ListView.separated( cacheExtent: 1000, - separatorBuilder: (context, index) => Divider(thickness: 0.2, color: Theme.of(context).dividerColor), + separatorBuilder: (context, index) => Divider(thickness: 0.2, height: 0, color: Theme.of(context).dividerColor), itemCount: view.length, itemBuilder: (context, index) { GlobalKey key = GlobalKey(); @@ -341,7 +341,8 @@ class DomainListState extends State with AutomaticKeepAliveClientMix super.build(context); return ListView.separated( padding: EdgeInsets.zero, - separatorBuilder: (context, index) => Divider(thickness: 0.2, color: Theme.of(context).dividerColor), + separatorBuilder: (context, index) => + Divider(thickness: 0.2, height: 0.5, color: Theme.of(context).dividerColor), itemCount: list.length, itemBuilder: (ctx, index) => title(index)); } diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index 382c2b8..1e536e0 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -3,6 +3,7 @@ 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/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http_client.dart'; import 'package:network_proxy/ui/component/utils.dart'; @@ -91,7 +92,8 @@ class RequestRowState extends State { FlutterToastr.show('代理服务未启动', context); return; } - HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request); + + HttpClients.proxyRequest(proxyInfo: ProxyInfo.of("127.0.0.1", widget.proxyServer.port), request); FlutterToastr.show('已重新发送请求', context); Navigator.of(context).pop(); }), diff --git a/lib/ui/mobile/request/request_editor.dart b/lib/ui/mobile/request/request_editor.dart index 91d2071..6601af4 100644 --- a/lib/ui/mobile/request/request_editor.dart +++ b/lib/ui/mobile/request/request_editor.dart @@ -1,6 +1,7 @@ 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/host_port.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'; @@ -63,7 +64,7 @@ class RequestEditorState extends State { var headers = headerKey.currentState?.getHeaders(); request.headers.addAll(headers); request.body = requestBody.codeUnits; - HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request); + HttpClients.proxyRequest(proxyInfo: ProxyInfo.of("127.0.0.1", widget.proxyServer.port), request); FlutterToastr.show('已重新发送请求', context); Navigator.pop(context, request); } diff --git a/lib/ui/ui_configuration.dart b/lib/ui/ui_configuration.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/utils/har.dart b/lib/utils/har.dart new file mode 100644 index 0000000..a528735 --- /dev/null +++ b/lib/utils/har.dart @@ -0,0 +1,67 @@ +import 'package:network_proxy/network/http/http.dart'; + +class Hae { + List entries = []; + + void addEntry(HttpRequest request) { + entries.add({ + "startedDateTime": request.requestTime, // 请求发出的时间(ISO 8601) + "time": request.response?.responseTime.difference(request.requestTime).inMilliseconds, // 请求耗时(ms) + "request": { + "method": request.method.name, // 请求方法 + "url": request.requestUrl, // 请求地址 + "httpVersion": request.protocolVersion, // HTTP协议版本 + "cookies": [], // 请求携带的cookie + "headers": _headers(request), // 请求头 + "queryString": [], // 请求参数 + "postData": { + "mimeType": request.contentType, // 请求体类型 + "text": request.bodyAsString, // 请求体内容 + }, + "headersSize": -1, // 请求头大小 + "bodySize": request.body?.length ?? -1, // 请求体大小 + }, + 'response': { + "status": request.response?.status.code, // 响应状态码 + "statusText": request.response?.status.reasonPhrase, // 响应状态码描述 + "httpVersion": request.response?.protocolVersion, // HTTP协议版本 + "cookies": [], // 响应携带的cookie + "headers": _headers(request.response), // 响应头 + "content": { + "size": request.response?.body?.length, // 响应体大小 + "mimeType": request.response?.contentType, // 响应体类型 + "text": request.response?.bodyAsString, // 响应体内容 + }, + "redirectURL": '', // 重定向地址 + "headersSize": -1, // 响应头大小 + "bodySize": request.response?.body?.length ?? -1, // 响应体大小 + }, + "cache": {}, + 'timings': { + 'send': 0, + 'wait': request.response?.responseTime.difference(request.requestTime).inMilliseconds, + 'receive': 0, + }, + 'serverIPAddress': request.response?.remoteAddress + }); + } + + void toFile() { + Map har = {}; + har["log"] = { + "version": "1.2", + "creator": {"name": "ProxyPin", "version": "1.0.1"}, + "entries": entries, + }; + } + + List _headers(HttpMessage? message) { + var headers = >[]; + message?.headers.forEach((name, values) { + for (var element in values) { + headers.add({'name': name, 'value': element}); + } + }); + return headers; + } +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 6b8bfe8..6b4c395 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -263,7 +263,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index f7078a4..854226d 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.2 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1d7a708..6a16cc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: network_proxy description: network proxy publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.1+1 +version: 1.0.1+2 environment: sdk: '>=3.0.2 <4.0.0' diff --git a/test/widget_test.dart b/test/widget_test.dart index e112485..a946f93 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -10,6 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:network_proxy/main.dart'; import 'package:network_proxy/network/bin/configuration.dart'; +import 'package:network_proxy/ui/desktop/desktop.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async {