desktop request list sort

This commit is contained in:
wanghongenpin
2025-01-18 16:20:10 +08:00
parent 7074183fac
commit 263115c092
10 changed files with 161 additions and 73 deletions

2
.gitignore vendored
View File

@@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
Podfile.lock
# IntelliJ related

View File

@@ -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",

View File

@@ -151,6 +151,8 @@
"editRequest": "编辑请求",
"reSendRequest": "已重新发送请求",
"viewExport": "视图导出",
"timeDesc": "按时间降序",
"timeAsc": "按时间升序",
"search": "搜索",
"clearSearch": "清除搜索",

View File

@@ -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;
}

View File

@@ -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 <PopupMenuEntry>[
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<HttpRequest> 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<HttpRequest> 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);
}
}
}
}
}

View File

@@ -75,6 +75,8 @@ class DomainWidgetState extends State<DomainList> with AutomaticKeepAliveClientM
//关键词高亮监听
late VoidCallback highlightListener;
bool sortDesc = true;
changeState() {
if (!changing) {
changing = true;
@@ -92,7 +94,7 @@ class DomainWidgetState extends State<DomainList> 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<DomainList> 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<DomainList> 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<DomainList> 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<DomainList> 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<DomainRequests> {
@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) {

View File

@@ -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<DesktopRequestListWidget> with Autom
//请求列表容器
ListenableList<HttpRequest> container = ListenableList();
bool sortDesc = true;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
@@ -83,9 +89,11 @@ class DesktopRequestListState extends State<DesktopRequestListWidget> 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<DesktopRequestListWidget> with Autom
]))));
}
Widget popupMenus() {
return PopupMenuButton(
offset: const Offset(0, 32),
icon: const Icon(Icons.more_vert_outlined, size: 20),
itemBuilder: (BuildContext context) {
return <PopupMenuEntry>[
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<DesktopRequestListWidget> 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);
}
}
}
}
}

View File

@@ -50,6 +50,8 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
Queue<HttpRequest> view = Queue();
bool changing = false;
bool sortDesc = true;
//搜索的内容
SearchModel? searchModel;
@@ -147,7 +149,12 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
return;
}
view.addFirst(request);
if (sortDesc) {
view.addFirst(request);
} else {
view.addLast(request);
}
changeState();
}
@@ -190,4 +197,12 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
view.addAll(widget.container.source.reversed);
});
}
///排序
sort(bool desc) {
sortDesc = desc;
setState(() {
view = Queue.of(view.toList().reversed);
});
}
}

View File

@@ -33,6 +33,8 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
Queue<HttpRequest> view = Queue();
bool changing = false;
bool sortDesc = true;
//搜索的内容
SearchModel? searchModel;
@@ -65,7 +67,12 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
return;
}
view.addFirst(request);
if (sortDesc) {
view.addFirst(request);
} else {
view.addLast(request);
}
changeState();
}
@@ -164,4 +171,16 @@ class RequestSequenceState extends State<RequestSequence> 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);
});
}
}

View File

@@ -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;