diff --git a/.gitignore b/.gitignore index a21dcae..d843c51 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ Podfile.lock # IntelliJ related diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7cf9fb7..10cc246 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -151,6 +151,8 @@ "editRequest": "Edit and Request", "reSendRequest": "The request has been resent", "viewExport": "View Export", + "timeDesc": "Descending by time", + "timeAsc": "Ascending by time", "search": "Search", "clearSearch": "Clear Search", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 64b9a9c..fa218bf 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -151,6 +151,8 @@ "editRequest": "编辑请求", "reSendRequest": "已重新发送请求", "viewExport": "视图导出", + "timeDesc": "按时间降序", + "timeAsc": "按时间升序", "search": "搜索", "clearSearch": "清除搜索", diff --git a/lib/network/proxy_helper.dart b/lib/network/proxy_helper.dart index aed0b39..3598339 100644 --- a/lib/network/proxy_helper.dart +++ b/lib/network/proxy_helper.dart @@ -108,7 +108,7 @@ class ProxyHelper { ..hostAndPort = hostAndPort; request.processInfo ??= channelContext.processInfo; - if (request.method == HttpMethod.connect && !request.requestUrl.startsWith("http")) { + if (request.method == HttpMethod.connect && !request.uri.startsWith("http")) { request.uri = hostAndPort.domain; } diff --git a/lib/ui/desktop/left_menus/history.dart b/lib/ui/desktop/left_menus/history.dart index bb593b1..429e2d6 100644 --- a/lib/ui/desktop/left_menus/history.dart +++ b/lib/ui/desktop/left_menus/history.dart @@ -86,41 +86,6 @@ class HistoryPageWidget extends StatelessWidget { localizations.historyRecordTitle( item.requestLength, item.name.substring(0, min(item.name.length, 25))), style: const TextStyle(fontSize: 14)), - actions: [ - PopupMenuButton( - offset: const Offset(0, 32), - icon: const Icon(Icons.more_vert_outlined, size: 20), - itemBuilder: (BuildContext context) { - return [ - CustomPopupMenuItem( - height: 35, - onTap: () { - String fileName = '${item.name.contains("ProxyPin") ? '' : 'ProxyPin'}${item.name}.har' - .replaceAll(" ", "_") - .replaceAll(":", "_"); - requestListKey.currentState?.export(fileName); - }, - child: IconText( - icon: const Icon(Icons.share, size: 16), - text: localizations.viewExport, - textStyle: const TextStyle(fontSize: 13))), - CustomPopupMenuItem( - height: 35, - onTap: () async { - var requests = requestListKey.currentState?.currentView(); - if (requests == null) return; - - //重发所有请求 - _repeatAllRequests(requests.toList(), proxyServer, - context: context.mounted ? context : null); - }, - child: IconText( - icon: const Icon(Icons.repeat, size: 16), - text: localizations.repeatAllRequests, - textStyle: const TextStyle(fontSize: 13))), - ]; - }), - ], )), body: futureWidget(HistoryStorage.instance.then((value) => value.getRequests(item)), (data) { //shrinkWrap: false, @@ -130,26 +95,6 @@ class HistoryPageWidget extends StatelessWidget { } } -///重发所有请求 -void _repeatAllRequests(Iterable requests, ProxyServer proxyServer, {BuildContext? context}) async { - var localizations = context == null ? null : AppLocalizations.of(context); - - for (var request in requests) { - var httpRequest = request.copy(uri: request.requestUrl); - var proxyInfo = proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", proxyServer.port) : null; - try { - await HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3)); - if (context != null && context.mounted) { - FlutterToastr.show(localizations!.reSendRequest, rootNavigator: true, context); - } - } catch (e) { - if (context != null && context.mounted) { - FlutterToastr.show('${localizations!.fail} $e', rootNavigator: true, context); - } - } - } -} - ///历史记录列表 class _HistoryListWidget extends StatefulWidget { // 存储 @@ -299,7 +244,7 @@ class _HistoryListState extends State<_HistoryListWidget> { onTap: () async { var requests = (await storage.getRequests(item)).reversed; //重发所有请求 - _repeatAllRequests(requests.toList(), proxyServer, context: rootContext.mounted ? rootContext : null); + _repeatAllRequests(requests.toList()); }), const PopupMenuDivider(height: 3), CustomPopupMenuItem( @@ -383,4 +328,24 @@ class _HistoryListState extends State<_HistoryListWidget> { if (mounted) FlutterToastr.show(localizations.exportSuccess, context); Future.delayed(const Duration(seconds: 30), () => item.requests = null); } + + ///重发所有请求 + void _repeatAllRequests(Iterable requests) async { + var localizations = AppLocalizations.of(context); + + for (var request in requests) { + var httpRequest = request.copy(uri: request.requestUrl); + var proxyInfo = proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", proxyServer.port) : null; + try { + await HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3)); + if (mounted) { + FlutterToastr.show(localizations!.reSendRequest, rootNavigator: true, context); + } + } catch (e) { + if (mounted) { + FlutterToastr.show('${localizations!.fail} $e', rootNavigator: true, context); + } + } + } + } } diff --git a/lib/ui/desktop/request/domians.dart b/lib/ui/desktop/request/domians.dart index 889c260..99055f9 100644 --- a/lib/ui/desktop/request/domians.dart +++ b/lib/ui/desktop/request/domians.dart @@ -75,6 +75,8 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM //关键词高亮监听 late VoidCallback highlightListener; + bool sortDesc = true; + changeState() { if (!changing) { changing = true; @@ -92,7 +94,7 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM var container = widget.list; for (var request in container.source) { DomainRequests domainRequests = getDomainRequests(request); - domainRequests.addRequest(request.requestId, request); + domainRequests.addRequest(request.requestId, request, sortDesc); } highlightListener = () { //回调时机在高亮设置页面dispose之后。所以需要在下一帧刷新,否则会报错 @@ -172,10 +174,10 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM DomainRequests domainRequests = getDomainRequests(request); var isNew = domainRequests.body.isEmpty; - domainRequests.addRequest(request.requestId, request); + domainRequests.addRequest(request.requestId, request, sortDesc); //搜索视图 if (searchModel?.isNotEmpty == true && searchModel?.filter(request, null) == true) { - searchView[host]?.addRequest(request.requestId, request); + searchView[host]?.addRequest(request.requestId, request, sortDesc); } if (isNew) { @@ -242,7 +244,7 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM if (searchModel?.isNotEmpty == true && searchModel?.filter(pathRow.request, response) == true) { var requests = searchView[domain]; if (requests?.getRequest(response) == null) { - requests?.addRequest(response.requestId, pathRow.request); + requests?.addRequest(response.requestId, pathRow.request, sortDesc); } requests?.getRequest(response)?.setResponse(response); } @@ -264,7 +266,7 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM var container = widget.list; for (var request in container.source) { DomainRequests domainRequests = getDomainRequests(request); - domainRequests.addRequest(request.requestId, request); + domainRequests.addRequest(request.requestId, request, sortDesc); } }); } @@ -276,6 +278,17 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM } return container.expand((list) => list.body.map((it) => it.request)).toList(); } + + ///排序 + sort(bool desc) { + sortDesc = desc; + containerMap.forEach((key, request) { + var reversed = request.body.toList().reversed; + request.body.clear(); + request.body.addAll(reversed); + request.changeState(); + }); + } } ///标题和内容布局 标题是域名 内容是域名下请求 @@ -302,15 +315,17 @@ class DomainRequests extends StatefulWidget { : super(key: GlobalKey<_DomainRequestsState>()); ///添加请求 - void addRequest(String? requestId, HttpRequest request) { + void addRequest(String? requestId, HttpRequest request, bool sortDesc) { if (requestMap.containsKey(requestId)) return; var requestWidget = RequestWidget(request, index: body.length, proxyServer: proxyServer, displayDomain: false, remove: (it) => _remove(it)); - body.addFirst(requestWidget); + sortDesc ? body.addFirst(requestWidget) : body.addLast(requestWidget); + if (requestId == null) { return; } + requestMap[requestId] = requestWidget; changeState(); } @@ -407,6 +422,7 @@ class _DomainRequestsState extends State { @override Widget build(BuildContext context) { + return Column(children: [ _hostWidget(widget.domain), Offstage(offstage: !selected, child: Column(children: widget.body.toList())) @@ -521,7 +537,7 @@ class HostWidget extends StatelessWidget { final String host; final Function()? onMenu; - HostWidget(this.host, {this.onMenu}); + const HostWidget(this.host, {super.key, this.onMenu}); @override Widget build(BuildContext context) { diff --git a/lib/ui/desktop/request/list.dart b/lib/ui/desktop/request/list.dart index c3bd241..f3b52dd 100644 --- a/lib/ui/desktop/request/list.dart +++ b/lib/ui/desktop/request/list.dart @@ -21,12 +21,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/network/channel.dart'; +import 'package:proxypin/network/host_port.dart'; import 'package:proxypin/network/http/http.dart'; +import 'package:proxypin/network/http_client.dart'; +import 'package:proxypin/ui/component/widgets.dart'; import 'package:proxypin/ui/content/panel.dart'; import 'package:proxypin/ui/desktop/request/model/search_model.dart'; import 'package:proxypin/ui/desktop/request/request_sequence.dart'; import 'package:proxypin/ui/desktop/request/search.dart'; import 'package:proxypin/utils/har.dart'; +import 'package:proxypin/utils/lang.dart'; import 'package:proxypin/utils/listenable_list.dart'; import 'domians.dart'; @@ -52,6 +56,8 @@ class DesktopRequestListState extends State with Autom //请求列表容器 ListenableList container = ListenableList(); + bool sortDesc = true; + AppLocalizations get localizations => AppLocalizations.of(context)!; @override @@ -83,9 +89,11 @@ class DesktopRequestListState extends State with Autom length: tabs.length, child: Scaffold( appBar: AppBar( - toolbarHeight: 40, - title: SizedBox(height: 40, child: TabBar(tabs: tabs)), - automaticallyImplyLeading: false), + toolbarHeight: 40, + title: SizedBox(height: 40, child: TabBar(tabs: tabs)), + automaticallyImplyLeading: false, + actions: [popupMenus()], + ), bottomNavigationBar: Search(onSearch: search), body: Padding( padding: const EdgeInsets.only(right: 5), @@ -104,6 +112,41 @@ class DesktopRequestListState extends State with Autom ])))); } + Widget popupMenus() { + return PopupMenuButton( + offset: const Offset(0, 32), + icon: const Icon(Icons.more_vert_outlined, size: 20), + itemBuilder: (BuildContext context) { + return [ + CustomPopupMenuItem( + height: 35, + onTap: () => export('ProxyPin_${DateTime.now().dateFormat()}.har'), + child: IconText( + icon: const Icon(Icons.share, size: 16), + text: localizations.viewExport, + textStyle: const TextStyle(fontSize: 13))), + CustomPopupMenuItem( + height: 35, + onTap: () => repeatAllRequests(), + child: IconText( + icon: const Icon(Icons.repeat, size: 16), + text: localizations.repeatAllRequests, + textStyle: const TextStyle(fontSize: 13))), + CustomPopupMenuItem( + height: 35, + onTap: () { + sortDesc = !sortDesc; + requestSequenceKey.currentState?.sort(sortDesc); + domainListKey.currentState?.sort(sortDesc); + }, + child: IconText( + icon: const Icon(Icons.sort, size: 16), + text: sortDesc ? localizations.timeDesc : localizations.timeAsc, + textStyle: const TextStyle(fontSize: 13))), + ]; + }); + } + ///添加请求 add(Channel channel, HttpRequest request) { container.add(request); @@ -176,4 +219,28 @@ class DesktopRequestListState extends State with Autom if (mounted) FlutterToastr.show(AppLocalizations.of(context)!.exportSuccess, context); } + + ///重发所有请求 + void repeatAllRequests() async { + var requests = currentView(); + if (requests == null) return; + + var localizations = AppLocalizations.of(context); + final proxyServer = widget.proxyServer; + + for (var request in requests) { + var httpRequest = request.copy(uri: request.requestUrl); + var proxyInfo = proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", proxyServer.port) : null; + try { + await HttpClients.proxyRequest(httpRequest, proxyInfo: proxyInfo, timeout: const Duration(seconds: 3)); + if (mounted) { + FlutterToastr.show(localizations!.reSendRequest, rootNavigator: true, context); + } + } catch (e) { + if (mounted) { + FlutterToastr.show('${localizations!.fail} $e', rootNavigator: true, context); + } + } + } + } } diff --git a/lib/ui/desktop/request/request_sequence.dart b/lib/ui/desktop/request/request_sequence.dart index c797732..3577bc6 100644 --- a/lib/ui/desktop/request/request_sequence.dart +++ b/lib/ui/desktop/request/request_sequence.dart @@ -50,6 +50,8 @@ class RequestSequenceState extends State with AutomaticKeepAliv Queue view = Queue(); bool changing = false; + bool sortDesc = true; + //搜索的内容 SearchModel? searchModel; @@ -147,7 +149,12 @@ class RequestSequenceState extends State with AutomaticKeepAliv return; } - view.addFirst(request); + if (sortDesc) { + view.addFirst(request); + } else { + view.addLast(request); + } + changeState(); } @@ -190,4 +197,12 @@ class RequestSequenceState extends State with AutomaticKeepAliv view.addAll(widget.container.source.reversed); }); } + + ///排序 + sort(bool desc) { + sortDesc = desc; + setState(() { + view = Queue.of(view.toList().reversed); + }); + } } diff --git a/lib/ui/mobile/request/request_sequence.dart b/lib/ui/mobile/request/request_sequence.dart index cb67082..4514ba6 100644 --- a/lib/ui/mobile/request/request_sequence.dart +++ b/lib/ui/mobile/request/request_sequence.dart @@ -33,6 +33,8 @@ class RequestSequenceState extends State with AutomaticKeepAliv Queue view = Queue(); bool changing = false; + bool sortDesc = true; + //搜索的内容 SearchModel? searchModel; @@ -65,7 +67,12 @@ class RequestSequenceState extends State with AutomaticKeepAliv return; } - view.addFirst(request); + if (sortDesc) { + view.addFirst(request); + } else { + view.addLast(request); + } + changeState(); } @@ -164,4 +171,16 @@ class RequestSequenceState extends State with AutomaticKeepAliv PrimaryScrollController.maybeOf(context) ?.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.ease); } + + ///排序 + sort(bool desc) { + if (sortDesc == desc) { + return; + } + + sortDesc = desc; + setState(() { + view = Queue.of(view.toList().reversed); + }); + } } diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 87e0f1b..af2edca 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -622,7 +622,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -763,7 +763,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -792,7 +792,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.1.7; PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0;