Files
proxypin/lib/ui/desktop/request/request.dart

596 lines
20 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/network/util/cache.dart';
import 'package:proxypin/storage/favorites.dart';
import 'package:proxypin/ui/component/multi_select_controller.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;
final MultiSelectController multiSelectController;
final RequestSelectionHandlers selectionHandlers;
RequestWidget(this.request,
{Key? key,
required this.proxyServer,
this.remove,
this.displayDomain = true,
this.trailing,
required this.selectionHandlers,
required this.index,
required this.multiSelectController})
: super(key: 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();
}
void changeState() {
var state = key as GlobalKey<_RequestWidgetState>;
state.currentState?.changeState();
}
static void removeAutoReadByIds(Iterable<String> requestIds) {
_RequestWidgetState.removeAutoReadByIds(requestIds);
}
}
class _RequestWidgetState extends State<RequestWidget> {
//选择的节点
static _RequestWidgetState? selectedState;
static LruCacheSet<String> autoReadRequests = LruCacheSet<String>(5000);
static bool markAutoRead(String requestId) {
return autoReadRequests.add(requestId);
}
static void removeAutoReadByIds(Iterable<String> requestIds) {
autoReadRequests.removeAll(requestIds);
}
bool selected = false;
Color? highlightColor; //高亮颜色
AppLocalizations get localizations => AppLocalizations.of(context)!;
bool get selectionMode => widget.multiSelectController.isSelectionMode;
int get selectionCount => widget.multiSelectController.selectedCount;
@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);
bool selectedInSelectionMode = widget.multiSelectController.contains(request.requestId);
return GestureDetector(
onLongPress: () {
if (!selectionMode) {
widget.multiSelectController.enterSelectionMode(widget.request.requestId);
}
},
onSecondaryTap: contextualMenu,
child: ListTile(
minLeadingWidth: 5,
textColor: requestColor,
selectedColor: requestColor,
selectedTileColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
leading: _leading(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 || selectedInSelectionMode,
dense: true,
visualDensity: const VisualDensity(vertical: -4),
contentPadding: EdgeInsets.only(left: selectedInSelectionMode ? 6 : 28),
onTap: onClick));
}
Widget _leading(Color? requestColor) {
bool selectedInSelectionMode = widget.multiSelectController.contains(widget.request.requestId);
var icon = getIcon(widget.response.get() ?? widget.request.response, color: requestColor);
if (!selectedInSelectionMode) {
return icon;
}
return Row(mainAxisSize: MainAxisSize.min, children: [
Icon(selectedInSelectionMode ? Icons.check_box_outlined : Icons.check_box_outline_blank_outlined,
size: 18, color: selectedInSelectionMode ? Theme.of(context).colorScheme.primary : Colors.grey),
const SizedBox(width: 4),
icon,
]);
}
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() {
popUpContextMenu(selectionMode && selectionCount > 1 ? _batchMenu() : _requestMenu());
}
Menu _batchMenu() {
return Menu(items: [
_menuAction(localizations.repeat, _RequestMenuAction.batchRepeat),
_menuAction(localizations.export, _RequestMenuAction.batchExport),
MenuItem.separator(),
_menuAction(localizations.delete, _RequestMenuAction.batchDelete),
MenuItem.separator(),
_menuAction(localizations.cancel, _RequestMenuAction.batchCancel),
]);
}
Menu _requestMenu() {
return Menu(items: [
_menuAction(localizations.copyUrl, _RequestMenuAction.copyUrl),
MenuItem(label: localizations.copy, type: 'submenu', submenu: _copySubmenu()),
MenuItem.separator(),
_menuAction(localizations.openNewWindow, _RequestMenuAction.openNewWindow),
MenuItem.separator(),
MenuItem(label: localizations.export, type: 'submenu', submenu: _exportSubmenu()),
MenuItem.separator(),
_menuAction(localizations.repeat, _RequestMenuAction.repeat),
_menuAction(localizations.customRepeat, _RequestMenuAction.customRepeat),
_menuAction(localizations.editRequest, _RequestMenuAction.editRequest),
MenuItem.separator(),
_menuAction(localizations.requestRewrite, _RequestMenuAction.requestRewrite),
_menuAction(localizations.requestMap, _RequestMenuAction.requestMap),
_menuAction(localizations.script, _RequestMenuAction.script),
MenuItem.separator(),
_menuAction(localizations.favorite, _RequestMenuAction.favorite),
MenuItem(label: localizations.highlight, type: 'submenu', submenu: highlightMenu()),
MenuItem.separator(),
_menuAction(localizations.select, _RequestMenuAction.select),
MenuItem.separator(),
_menuAction(localizations.delete, _RequestMenuAction.delete),
]);
}
Menu _copySubmenu() {
return Menu(items: [
_copyMenuAction(localizations.copyCurl, _RequestCopyMenuAction.curl),
_copyMenuAction(localizations.copyRawRequest, _RequestCopyMenuAction.rawRequest),
_copyMenuAction(localizations.copyRequestResponse, _RequestCopyMenuAction.requestResponse),
_copyMenuAction(localizations.copyAsPythonRequests, _RequestCopyMenuAction.pythonRequests),
_copyMenuAction(localizations.copyAsFetch, _RequestCopyMenuAction.fetch),
]);
}
Menu _exportSubmenu() {
return Menu(items: [
_exportMenuAction(localizations.request, _RequestExportMenuAction.request),
_exportMenuAction(localizations.requestBody, _RequestExportMenuAction.requestBody),
MenuItem.separator(),
_exportMenuAction(localizations.response, _RequestExportMenuAction.response),
_exportMenuAction(localizations.responseBody, _RequestExportMenuAction.responseBody),
MenuItem.separator(),
_exportMenuAction('HAR', _RequestExportMenuAction.har),
]);
}
MenuItem _menuAction(String label, _RequestMenuAction action) {
return MenuItem(label: label, onClick: (_) => _onMenuAction(action));
}
MenuItem _copyMenuAction(String label, _RequestCopyMenuAction action) {
return MenuItem(label: label, onClick: (_) => _onCopyMenuAction(action));
}
MenuItem _exportMenuAction(String label, _RequestExportMenuAction action) {
return MenuItem(label: label, onClick: (_) => _onExportMenuAction(action));
}
Future<void> _onMenuAction(_RequestMenuAction action) async {
switch (action) {
case _RequestMenuAction.copyUrl:
await _copyText(widget.request.requestUrl);
break;
case _RequestMenuAction.openNewWindow:
openDetailInNewWindow();
break;
case _RequestMenuAction.repeat:
onRepeat(widget.request);
break;
case _RequestMenuAction.customRepeat:
await showCustomRepeat(widget.request);
break;
case _RequestMenuAction.editRequest:
WidgetsBinding.instance.addPostFrameCallback((_) {
requestEdit();
});
break;
case _RequestMenuAction.requestRewrite:
showRequestRewriteDialog(context, widget.request);
break;
case _RequestMenuAction.requestMap:
showDialog(
context: context,
builder: (context) =>
RequestMapEdit(url: widget.request.domainPath, title: widget.request.hostAndPort?.host));
break;
case _RequestMenuAction.script:
await _openScriptDialog();
break;
case _RequestMenuAction.favorite:
FavoriteStorage.addFavorite(widget.request);
FlutterToastr.show(localizations.operationSuccess, context, rootNavigator: true);
break;
case _RequestMenuAction.select:
widget.multiSelectController.selectOnly(widget.request.requestId);
break;
case _RequestMenuAction.delete:
widget.remove?.call(widget);
break;
case _RequestMenuAction.batchRepeat:
widget.selectionHandlers.onRepeatSelected?.call();
break;
case _RequestMenuAction.batchExport:
widget.selectionHandlers.onExportSelected?.call();
break;
case _RequestMenuAction.batchDelete:
widget.selectionHandlers.onDeleteSelected?.call();
break;
case _RequestMenuAction.batchCancel:
widget.multiSelectController.clear();
break;
}
}
Future<void> _onCopyMenuAction(_RequestCopyMenuAction action) async {
switch (action) {
case _RequestCopyMenuAction.curl:
await _copyText(curlRequest(widget.request));
break;
case _RequestCopyMenuAction.rawRequest:
await _copyText(copyRawRequest(widget.request));
break;
case _RequestCopyMenuAction.requestResponse:
await _copyText(copyRequest(widget.request, widget.response.get()));
break;
case _RequestCopyMenuAction.pythonRequests:
await _copyText(copyAsPythonRequests(widget.request));
break;
case _RequestCopyMenuAction.fetch:
await _copyText(copyAsFetch(widget.request));
break;
}
}
void _onExportMenuAction(_RequestExportMenuAction action) {
switch (action) {
case _RequestExportMenuAction.request:
exportRequest(widget.request);
break;
case _RequestExportMenuAction.requestBody:
exportRequestBody(widget.request);
break;
case _RequestExportMenuAction.response:
exportResponse(widget.response.get());
break;
case _RequestExportMenuAction.responseBody:
exportResponseBody(widget.response.get());
break;
case _RequestExportMenuAction.har:
exportHar(widget.request);
break;
}
}
Future<void> _openScriptDialog() 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));
}
Future<void> _copyText(String text) async {
await Clipboard.setData(ClipboardData(text: text));
if (mounted) {
FlutterToastr.show(localizations.copied, rootNavigator: true, context);
}
}
///高亮
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);
FlutterToastr.show(localizations.reSendRequest, context, rootNavigator: true);
}
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() {
final keyboard = HardwareKeyboard.instance;
final useToggleSelection = keyboard.isMetaPressed || keyboard.isControlPressed;
final useRangeSelection = keyboard.isShiftPressed;
if (useRangeSelection) {
widget.selectionHandlers.onRangeSelection?.call(widget.request);
return;
}
if (selectionMode || useToggleSelection) {
setState(() {
widget.multiSelectController.toggle(widget.request.requestId);
});
return;
}
if (!selected) {
setState(() {
selected = true;
});
}
if (AppConfiguration.current?.autoReadEnabled == true) {
markAutoRead(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);
}
}
class RequestSelectionHandlers {
final Function(HttpRequest request)? onRangeSelection;
final VoidCallback? onDeleteSelected;
final VoidCallback? onRepeatSelected;
final VoidCallback? onExportSelected;
const RequestSelectionHandlers({
this.onRangeSelection,
this.onDeleteSelected,
this.onRepeatSelected,
this.onExportSelected,
});
}
enum _RequestMenuAction {
copyUrl,
openNewWindow,
repeat,
customRepeat,
editRequest,
requestRewrite,
requestMap,
script,
favorite,
select,
delete,
batchRepeat,
batchExport,
batchDelete,
batchCancel,
}
enum _RequestCopyMenuAction { curl, rawRequest, requestResponse, pythonRequests, fetch }
enum _RequestExportMenuAction { request, requestBody, response, responseBody, har }