mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-30 17:05:49 +08:00
596 lines
20 KiB
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 }
|