mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-30 07:09:48 +08:00
400 lines
13 KiB
Dart
400 lines
13 KiB
Dart
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:flutter_gen/gen_l10n/app_localizations.dart';
|
|
import 'package:flutter_toastr/flutter_toastr.dart';
|
|
import 'package:network_proxy/network/bin/server.dart';
|
|
import 'package:network_proxy/network/host_port.dart';
|
|
import 'package:network_proxy/network/http/http.dart';
|
|
import 'package:network_proxy/network/http_client.dart';
|
|
import 'package:network_proxy/storage/favorites.dart';
|
|
import 'package:network_proxy/ui/component/state_component.dart';
|
|
import 'package:network_proxy/ui/component/utils.dart';
|
|
import 'package:network_proxy/ui/component/widgets.dart';
|
|
import 'package:network_proxy/ui/content/panel.dart';
|
|
import 'package:network_proxy/ui/desktop/request/repeat.dart';
|
|
import 'package:network_proxy/utils/curl.dart';
|
|
import 'package:network_proxy/utils/lang.dart';
|
|
import 'package:network_proxy/utils/python.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
|
|
/// 请求 URI
|
|
/// @author wanghongen
|
|
/// 2023/10/8
|
|
class RequestWidget extends StatefulWidget {
|
|
final HttpRequest request;
|
|
final ValueWrap<HttpResponse> response = ValueWrap();
|
|
|
|
final ProxyServer proxyServer;
|
|
final Function(RequestWidget)? remove;
|
|
|
|
RequestWidget(this.request, {Key? key, required this.proxyServer, this.remove})
|
|
: 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;
|
|
|
|
bool selected = false;
|
|
|
|
Color? highlightColor; //高亮颜色
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var request = widget.request;
|
|
var response = widget.response.get() ?? request.response;
|
|
String title = '${request.method.name} ${request.uri}';
|
|
try {
|
|
title = '${request.method.name} ${Uri.parse(request.requestUrl).path}';
|
|
} catch (_) {}
|
|
|
|
var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);
|
|
String contentType = response?.contentType.name.toUpperCase() ?? '';
|
|
var packagesSize = getPackagesSize(request, response);
|
|
|
|
return GestureDetector(
|
|
onSecondaryTap: contextualMenu,
|
|
child: ListTile(
|
|
minLeadingWidth: 5,
|
|
textColor: color(),
|
|
selectedColor: color(),
|
|
selectedTileColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
|
leading: getIcon(widget.response.get() ?? widget.request.response),
|
|
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1),
|
|
subtitle: Text(
|
|
'$time - [${response?.status.code ?? ''}] $contentType $packagesSize ${response?.costTime() ?? ''}',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.visible),
|
|
subtitleTextStyle: const TextStyle(fontSize: 12, color: Colors.grey),
|
|
selected: selected,
|
|
dense: true,
|
|
visualDensity: const VisualDensity(vertical: -4),
|
|
contentPadding: const EdgeInsets.only(left: 28),
|
|
onTap: onClick));
|
|
}
|
|
|
|
Color? color() {
|
|
if (highlightColor != null) {
|
|
return highlightColor;
|
|
}
|
|
|
|
return KeywordHighlightDialog.getHighlightColor(widget.request.uri);
|
|
}
|
|
|
|
void changeState() {
|
|
setState(() {});
|
|
}
|
|
|
|
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, 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, context));
|
|
}),
|
|
MenuItem(
|
|
label: localizations.copyRequestResponse,
|
|
onClick: (_) {
|
|
Clipboard.setData(ClipboardData(text: copyRequest(widget.request, widget.response.get())))
|
|
.then((value) => FlutterToastr.show(localizations.copied, context));
|
|
}),
|
|
MenuItem(
|
|
label: localizations.copyAsPythonRequests,
|
|
onClick: (_) {
|
|
Clipboard.setData(ClipboardData(text: copyAsPythonRequests(widget.request)))
|
|
.then((value) => FlutterToastr.show(localizations.copied, context));
|
|
},
|
|
),
|
|
]),
|
|
onClick: (_) {
|
|
Clipboard.setData(ClipboardData(text: curlRequest(widget.request)))
|
|
.then((value) => FlutterToastr.show(localizations.copied, context));
|
|
}),
|
|
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.favorite,
|
|
onClick: (_) {
|
|
FavoriteStorage.addFavorite(widget.request);
|
|
FlutterToastr.show(localizations.operationSuccess, context);
|
|
}),
|
|
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(
|
|
label: localizations.reset,
|
|
onClick: (_) {
|
|
setState(() {
|
|
highlightColor = null;
|
|
});
|
|
}),
|
|
MenuItem(
|
|
label: localizations.keyword,
|
|
onClick: (_) {
|
|
showDialog(context: context, builder: (BuildContext context) => const KeywordHighlightDialog());
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
//显示高级重发
|
|
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);
|
|
});
|
|
}
|
|
|
|
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, rootNavigator: true, context);
|
|
}
|
|
|
|
PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {
|
|
return CustomPopupMenuItem(height: 32, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));
|
|
}
|
|
|
|
///请求编辑
|
|
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 onClick() {
|
|
if (!selected) {
|
|
setState(() {
|
|
selected = true;
|
|
});
|
|
}
|
|
|
|
//切换选中的节点
|
|
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 KeywordHighlightDialog extends StatefulWidget {
|
|
static Map<Color, String> keywords = {};
|
|
static ValueNotifier keywordsController = ValueNotifier<Map>(keywords);
|
|
|
|
static Color? getHighlightColor(String key) {
|
|
for (var entry in keywords.entries) {
|
|
if (key.contains(entry.value)) {
|
|
return entry.key;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const KeywordHighlightDialog({super.key});
|
|
|
|
@override
|
|
State<KeywordHighlightDialog> createState() => _KeywordHighlightState();
|
|
}
|
|
|
|
class _KeywordHighlightState extends State<KeywordHighlightDialog> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
AppLocalizations localizations = AppLocalizations.of(context)!;
|
|
var colors = {
|
|
Colors.red: localizations.red,
|
|
Colors.yellow.shade600: localizations.yellow,
|
|
Colors.blue: localizations.blue,
|
|
Colors.green: localizations.green,
|
|
Colors.grey: localizations.gray,
|
|
};
|
|
|
|
var map = Map.of(KeywordHighlightDialog.keywords);
|
|
|
|
return AlertDialog(
|
|
title: ListTile(
|
|
title: Text(localizations.keyword + localizations.highlight,
|
|
textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500))),
|
|
titlePadding: const EdgeInsets.all(0),
|
|
actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),
|
|
contentPadding: const EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 5),
|
|
actions: [
|
|
TextButton(
|
|
child: Text(localizations.cancel),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
TextButton(
|
|
child: Text(localizations.done),
|
|
onPressed: () {
|
|
KeywordHighlightDialog.keywords = map;
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
content: SizedBox(
|
|
height: 180,
|
|
width: 400,
|
|
child: DefaultTabController(
|
|
length: colors.length,
|
|
child: Scaffold(
|
|
appBar: TabBar(tabs: colors.entries.map((e) => Tab(text: e.value)).toList()),
|
|
body: TabBarView(
|
|
children: colors.entries
|
|
.map((e) => KeepAliveWrapper(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(15),
|
|
child: TextFormField(
|
|
minLines: 2,
|
|
maxLines: 2,
|
|
initialValue: map[e.key],
|
|
onChanged: (value) {
|
|
if (value.isEmpty) {
|
|
map.remove(e.key);
|
|
} else {
|
|
map[e.key] = value;
|
|
}
|
|
},
|
|
decoration: decoration(localizations.keyword),
|
|
))))
|
|
.toList()),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
InputDecoration decoration(String label, {String? hintText}) {
|
|
return InputDecoration(
|
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
|
labelText: label,
|
|
isDense: true,
|
|
border: const OutlineInputBorder(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
KeywordHighlightDialog.keywordsController.value = Map.from(KeywordHighlightDialog.keywords);
|
|
super.dispose();
|
|
}
|
|
}
|