mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-15 04:23:17 +08:00
420 lines
15 KiB
Dart
420 lines
15 KiB
Dart
/*
|
|
* Copyright 2023 Hongen Wang
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
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/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_desktop_context_menu/flutter_desktop_context_menu.dart';
|
|
import 'package:proxypin/l10n/app_localizations.dart';
|
|
import 'package:flutter_toastr/flutter_toastr.dart';
|
|
import 'package:proxypin/network/bin/server.dart';
|
|
import 'package:proxypin/network/components/manager/script_manager.dart';
|
|
import 'package:proxypin/network/channel/host_port.dart';
|
|
import 'package:proxypin/network/http/http.dart';
|
|
import 'package:proxypin/network/http/http_client.dart';
|
|
import 'package:proxypin/storage/favorites.dart';
|
|
import 'package:proxypin/ui/component/app_dialog.dart';
|
|
import 'package:proxypin/ui/component/multi_window.dart';
|
|
import 'package:proxypin/ui/component/utils.dart';
|
|
import 'package:proxypin/ui/component/widgets.dart';
|
|
import 'package:proxypin/ui/configuration.dart';
|
|
import 'package:proxypin/ui/content/panel.dart';
|
|
import 'package:proxypin/ui/desktop/request/repeat.dart';
|
|
import 'package:proxypin/ui/desktop/setting/request_map.dart';
|
|
import 'package:proxypin/ui/desktop/setting/script.dart';
|
|
import 'package:proxypin/ui/desktop/widgets/highlight.dart';
|
|
import 'package:proxypin/utils/curl.dart';
|
|
import 'package:proxypin/utils/keyword_highlight.dart';
|
|
import 'package:proxypin/utils/lang.dart';
|
|
import 'package:proxypin/utils/python.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
|
|
import '../../../utils/export_request.dart';
|
|
import '../common.dart';
|
|
|
|
/// 请求 URI
|
|
/// @author wanghongen
|
|
/// 2023/10/8
|
|
class RequestWidget extends StatefulWidget {
|
|
final int index;
|
|
final HttpRequest request;
|
|
final ValueWrap<HttpResponse> response = ValueWrap();
|
|
final bool displayDomain;
|
|
|
|
final ProxyServer proxyServer;
|
|
final Function(RequestWidget)? remove;
|
|
final Widget? trailing;
|
|
|
|
RequestWidget(this.request,
|
|
{Key? key, required this.proxyServer, this.remove, this.displayDomain = true, this.trailing, required this.index})
|
|
: super(key: GlobalKey<_RequestWidgetState>());
|
|
|
|
@override
|
|
State<RequestWidget> createState() => _RequestWidgetState();
|
|
|
|
void setResponse(HttpResponse response) {
|
|
this.response.set(response);
|
|
var state = key as GlobalKey<_RequestWidgetState>;
|
|
state.currentState?.changeState();
|
|
}
|
|
}
|
|
|
|
class _RequestWidgetState extends State<RequestWidget> {
|
|
//选择的节点
|
|
static _RequestWidgetState? selectedState;
|
|
|
|
static Set<String> autoReadRequests = <String>{};
|
|
|
|
bool selected = false;
|
|
|
|
Color? highlightColor; //高亮颜色
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var request = widget.request;
|
|
var response = widget.response.get() ?? request.response;
|
|
String path = widget.displayDomain ? request.domainPath : request.path;
|
|
String title = '${request.method.name} $path';
|
|
|
|
var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);
|
|
String contentType = response?.contentType.name.toUpperCase() ?? '';
|
|
var packagesSize = getPackagesSize(request, response);
|
|
|
|
var requestColor = color(path);
|
|
|
|
return GestureDetector(
|
|
onSecondaryTap: contextualMenu,
|
|
child: ListTile(
|
|
minLeadingWidth: 5,
|
|
textColor: requestColor,
|
|
selectedColor: requestColor,
|
|
selectedTileColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
|
leading: getIcon(widget.response.get() ?? widget.request.response, color: requestColor),
|
|
trailing: widget.trailing,
|
|
title: Text(title.fixAutoLines(), overflow: TextOverflow.ellipsis, maxLines: 2),
|
|
subtitle: Container(
|
|
padding: const EdgeInsets.only(top: 3),
|
|
child: Text.rich(
|
|
maxLines: 1,
|
|
TextSpan(
|
|
children: [
|
|
TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 11, color: Colors.teal)),
|
|
TextSpan(
|
|
text:
|
|
'$time - [${response?.status.code ?? ''}] $contentType $packagesSize ${response?.costTime() ?? ''}',
|
|
style: const TextStyle(fontSize: 11, color: Colors.grey))
|
|
],
|
|
))),
|
|
selected: selected,
|
|
dense: true,
|
|
visualDensity: const VisualDensity(vertical: -4),
|
|
contentPadding: const EdgeInsets.only(left: 28),
|
|
onTap: onClick));
|
|
}
|
|
|
|
Color? color(String url) {
|
|
if (highlightColor != null) {
|
|
return highlightColor;
|
|
}
|
|
|
|
highlightColor = KeywordHighlights.getHighlightColor(url);
|
|
if (highlightColor != null) {
|
|
return highlightColor;
|
|
}
|
|
|
|
return autoReadRequests.contains(widget.request.requestId) ? Colors.grey : null;
|
|
}
|
|
|
|
void changeState() {
|
|
setState(() {});
|
|
}
|
|
|
|
void contextualMenu() {
|
|
Menu menu = Menu(items: [
|
|
MenuItem(
|
|
label: localizations.copyUrl,
|
|
onClick: (_) {
|
|
var requestUrl = widget.request.requestUrl;
|
|
Clipboard.setData(ClipboardData(text: requestUrl))
|
|
.then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));
|
|
}),
|
|
MenuItem(
|
|
label: localizations.copy,
|
|
type: 'submenu',
|
|
submenu: Menu(items: [
|
|
MenuItem(
|
|
label: localizations.copyCurl,
|
|
onClick: (_) {
|
|
Clipboard.setData(ClipboardData(text: curlRequest(widget.request)))
|
|
.then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));
|
|
}),
|
|
MenuItem(
|
|
label: localizations.copyRawRequest,
|
|
onClick: (_) {
|
|
Clipboard.setData(ClipboardData(text: copyRawRequest(widget.request)))
|
|
.then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));
|
|
}),
|
|
MenuItem(
|
|
label: localizations.copyRequestResponse,
|
|
onClick: (_) {
|
|
Clipboard.setData(ClipboardData(text: copyRequest(widget.request, widget.response.get())))
|
|
.then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));
|
|
}),
|
|
MenuItem(
|
|
label: localizations.copyAsPythonRequests,
|
|
onClick: (_) {
|
|
Clipboard.setData(ClipboardData(text: copyAsPythonRequests(widget.request)))
|
|
.then((value) => FlutterToastr.show(localizations.copied, rootNavigator: true, context));
|
|
},
|
|
),
|
|
])),
|
|
MenuItem.separator(),
|
|
MenuItem(
|
|
label: localizations.openNewWindow,
|
|
onClick: (_) {
|
|
openDetailInNewWindow();
|
|
}),
|
|
MenuItem.separator(),
|
|
MenuItem(
|
|
label: localizations.export,
|
|
type: 'submenu',
|
|
submenu: Menu(items: [
|
|
MenuItem(label: localizations.request, onClick: (_) => exportRequest(widget.request)),
|
|
MenuItem(label: localizations.requestBody, onClick: (_) => exportRequestBody(widget.request)),
|
|
MenuItem.separator(),
|
|
MenuItem(label: localizations.response, onClick: (_) => exportResponse(widget.response.get())),
|
|
MenuItem(label: localizations.responseBody, onClick: (_) => exportResponseBody(widget.response.get())),
|
|
MenuItem.separator(),
|
|
MenuItem(label: "HAR", onClick: (_) => exportHar(widget.request)),
|
|
])),
|
|
MenuItem.separator(),
|
|
MenuItem(label: localizations.repeat, onClick: (_) => onRepeat(widget.request)),
|
|
MenuItem(label: localizations.customRepeat, onClick: (_) => showCustomRepeat(widget.request)),
|
|
MenuItem(
|
|
label: localizations.editRequest,
|
|
onClick: (_) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
requestEdit();
|
|
});
|
|
}),
|
|
MenuItem.separator(),
|
|
MenuItem(label: localizations.requestRewrite, onClick: (_) => showRequestRewriteDialog(context, widget.request)),
|
|
MenuItem(
|
|
label: localizations.requestMap,
|
|
onClick: (_) async {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) =>
|
|
RequestMapEdit(url: widget.request.domainPath, title: widget.request.hostAndPort?.host));
|
|
}),
|
|
MenuItem(
|
|
label: localizations.script,
|
|
onClick: (_) async {
|
|
var scriptManager = await ScriptManager.instance;
|
|
var url = widget.request.domainPath;
|
|
var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.urls.contains(url));
|
|
|
|
String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem);
|
|
if (!mounted) return;
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => ScriptEdit(
|
|
scriptItem: scriptItem, script: script, url: url, title: widget.request.hostAndPort?.host));
|
|
}),
|
|
MenuItem.separator(),
|
|
MenuItem(
|
|
label: localizations.favorite,
|
|
onClick: (_) {
|
|
FavoriteStorage.addFavorite(widget.request);
|
|
FlutterToastr.show(localizations.operationSuccess, context, rootNavigator: true);
|
|
}),
|
|
MenuItem(
|
|
label: localizations.highlight,
|
|
type: 'submenu',
|
|
submenu: highlightMenu(),
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = Colors.red;
|
|
});
|
|
}),
|
|
MenuItem.separator(),
|
|
MenuItem(
|
|
label: localizations.delete,
|
|
onClick: (_) {
|
|
widget.remove?.call(widget);
|
|
}),
|
|
]);
|
|
|
|
popUpContextMenu(menu);
|
|
}
|
|
|
|
///高亮
|
|
Menu highlightMenu() {
|
|
return Menu(
|
|
items: [
|
|
MenuItem(
|
|
label: localizations.red,
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = Colors.red;
|
|
});
|
|
}),
|
|
MenuItem(
|
|
label: localizations.yellow,
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = Colors.yellow.shade600;
|
|
});
|
|
}),
|
|
MenuItem(
|
|
label: localizations.blue,
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = Colors.blue;
|
|
});
|
|
}),
|
|
MenuItem(
|
|
label: localizations.green,
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = Colors.green;
|
|
});
|
|
}),
|
|
MenuItem(
|
|
label: localizations.gray,
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = Colors.grey;
|
|
});
|
|
}),
|
|
MenuItem.separator(),
|
|
MenuItem.checkbox(
|
|
label: localizations.autoRead,
|
|
checked: AppConfiguration.current?.autoReadEnabled,
|
|
onClick: (_) {
|
|
setState(() {
|
|
AppConfiguration.current?.autoReadEnabled = !AppConfiguration.current!.autoReadEnabled;
|
|
});
|
|
}),
|
|
MenuItem.separator(),
|
|
MenuItem(
|
|
label: localizations.reset,
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = null;
|
|
autoReadRequests.clear();
|
|
});
|
|
}),
|
|
MenuItem(
|
|
label: localizations.keyword,
|
|
onClick: (_) {
|
|
showDialog(context: context, builder: (BuildContext context) => const DesktopKeywordHighlight());
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
//显示高级重发
|
|
Future<void> showCustomRepeat(HttpRequest request) async {
|
|
var prefs = await SharedPreferences.getInstance();
|
|
if (!mounted) return;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return CustomRepeatDialog(onRepeat: () => onRepeat(request), prefs: prefs);
|
|
});
|
|
}
|
|
|
|
void onRepeat(HttpRequest httpRequest) {
|
|
var request = httpRequest.copy(uri: httpRequest.requestUrl);
|
|
var proxyInfo = widget.proxyServer.isRunning ? ProxyInfo.of("127.0.0.1", widget.proxyServer.port) : null;
|
|
HttpClients.proxyRequest(request, proxyInfo: proxyInfo);
|
|
|
|
CustomToast.success(localizations.reSendRequest).show(context);
|
|
}
|
|
|
|
PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {
|
|
return CustomPopupMenuItem(height: 32, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));
|
|
}
|
|
|
|
///请求编辑
|
|
Future<void> 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, 'proxyPort': widget.proxyServer.port},
|
|
));
|
|
|
|
window.setTitle(localizations.requestEdit);
|
|
window
|
|
..setFrame(const Offset(100, 100) & Size(960 * ratio, size.height * ratio))
|
|
..center()
|
|
..show();
|
|
}
|
|
|
|
// 新窗口打开详情
|
|
void openDetailInNewWindow() async {
|
|
MultiWindow.openWindow(
|
|
localizations.captureDetail,
|
|
'RequestDetailPage',
|
|
args: {
|
|
'request': widget.request,
|
|
'response': widget.request.response ?? widget.response.get(),
|
|
},
|
|
size: Size(850, 900),
|
|
);
|
|
}
|
|
|
|
//点击事件
|
|
void onClick() {
|
|
if (!selected) {
|
|
setState(() {
|
|
selected = true;
|
|
});
|
|
}
|
|
|
|
if (AppConfiguration.current?.autoReadEnabled == true) {
|
|
autoReadRequests.add(widget.request.requestId);
|
|
}
|
|
|
|
//切换选中的节点
|
|
if (selectedState?.mounted == true && selectedState != this) {
|
|
selectedState?.setState(() {
|
|
selectedState?.selected = false;
|
|
});
|
|
}
|
|
|
|
selectedState = this;
|
|
NetworkTabController.current?.change(widget.request, widget.response.get() ?? widget.request.response);
|
|
}
|
|
}
|