/* * 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 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 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 requestIds) { _RequestWidgetState.removeAutoReadByIds(requestIds); } } class _RequestWidgetState extends State { //选择的节点 static _RequestWidgetState? selectedState; static LruCacheSet autoReadRequests = LruCacheSet(5000); static bool markAutoRead(String requestId) { return autoReadRequests.add(requestId); } static void removeAutoReadByIds(Iterable 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 _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 _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 _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 _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 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 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 }