mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-06-03 17:25:48 +08:00
桌面端增加历史记录功能
This commit is contained in:
@@ -4,10 +4,12 @@ import 'package:network_proxy/network/bin/server.dart';
|
||||
import 'package:network_proxy/network/channel.dart';
|
||||
import 'package:network_proxy/network/handler.dart';
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/ui/component/state_component.dart';
|
||||
import 'package:network_proxy/ui/component/toolbox.dart';
|
||||
import 'package:network_proxy/ui/content/panel.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/domain.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/favorite.dart';
|
||||
import 'package:network_proxy/ui/component/toolbox.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/history.dart';
|
||||
import 'package:network_proxy/ui/desktop/toolbar/toolbar.dart';
|
||||
|
||||
import '../component/split_view.dart';
|
||||
@@ -26,13 +28,13 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
final PageController pageController = PageController();
|
||||
final ValueNotifier<int> _selectIndex = ValueNotifier(0);
|
||||
|
||||
late ProxyServer proxyServer;
|
||||
late ProxyServer proxyServer = ProxyServer(widget.configuration);
|
||||
late NetworkTabController panel;
|
||||
|
||||
final List<NavigationRailDestination> destinations = const [
|
||||
NavigationRailDestination(icon: Icon(Icons.workspaces), label: Text("抓包", style: TextStyle(fontSize: 12))),
|
||||
// NavigationRailDestination(icon: Icon(Icons.history), label: Text("历史", style: TextStyle(fontSize: 12))),
|
||||
NavigationRailDestination(icon: Icon(Icons.favorite), label: Text("收藏", style: TextStyle(fontSize: 12))),
|
||||
NavigationRailDestination(icon: Icon(Icons.history), label: Text("历史", style: TextStyle(fontSize: 12))),
|
||||
NavigationRailDestination(icon: Icon(Icons.construction), label: Text("工具箱", style: TextStyle(fontSize: 12))),
|
||||
];
|
||||
|
||||
@@ -49,7 +51,7 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
proxyServer = ProxyServer(widget.configuration, listener: this);
|
||||
proxyServer.addListener(this);
|
||||
panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer);
|
||||
|
||||
if (widget.configuration.upgradeNoticeV2) {
|
||||
@@ -62,7 +64,6 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final domainWidget = DomainWidget(key: domainStateKey, proxyServer: proxyServer, panel: panel);
|
||||
|
||||
return Scaffold(
|
||||
appBar: Tab(child: Toolbar(proxyServer, domainStateKey, sideNotifier: _selectIndex)),
|
||||
body: Row(
|
||||
@@ -86,8 +87,14 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
ratio: 0.3,
|
||||
minRatio: 0.15,
|
||||
maxRatio: 0.9,
|
||||
left: PageView(
|
||||
controller: pageController, children: [domainWidget, Favorites(panel: panel), const Toolbox()]),
|
||||
left: PageView(controller: pageController, physics: const NeverScrollableScrollPhysics(), children: [
|
||||
domainWidget,
|
||||
Favorites(panel: panel),
|
||||
KeepAliveWrapper(
|
||||
child: HistoryPageWidget(
|
||||
proxyServer: proxyServer, domainWidgetState: domainStateKey, panel: panel)),
|
||||
const Toolbox()
|
||||
]),
|
||||
right: panel),
|
||||
)
|
||||
],
|
||||
@@ -130,12 +137,11 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
|
||||
'提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n'
|
||||
'点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n'
|
||||
'新增更新:\n'
|
||||
'1. 增加工具箱,提供常用编解码,增加HTTP请求,可粘贴cURL格式解析发起请求;\n'
|
||||
'2. 请求编辑发送可直接查看响应体,发送请求无需开启代理;\n'
|
||||
'3. 详情增加快速请求重写, 复制cURL格式可直接导入Postman;\n'
|
||||
'4. 增加请求收藏功能;\n'
|
||||
'5. 主题增加Material3切换;\n'
|
||||
'6. 请求删除&大响应体直接转发;',
|
||||
'1. 增加历史记录功能;\n'
|
||||
'2. 请求重写增加名称&URL参数重写;\n'
|
||||
'3. 请求重写增加重定向;\n'
|
||||
'4. 建立连接异常显示请求体;\n'
|
||||
'5. 请求编辑重发响应体查看增加多种格式,详情Body体增加快速解码入口;',
|
||||
style: TextStyle(fontSize: 14)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:network_proxy/network/bin/configuration.dart';
|
||||
import 'package:network_proxy/network/bin/server.dart';
|
||||
@@ -10,17 +9,21 @@ import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/network/util/attribute_keys.dart';
|
||||
import 'package:network_proxy/network/util/host_filter.dart';
|
||||
import 'package:network_proxy/ui/component/transition.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/left/model/search_model.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/path.dart';
|
||||
import 'package:network_proxy/ui/content/panel.dart';
|
||||
import 'package:network_proxy/ui/desktop/left/search.dart';
|
||||
|
||||
///左侧域名
|
||||
class DomainWidget extends StatefulWidget {
|
||||
final NetworkTabController panel;
|
||||
final ProxyServer proxyServer;
|
||||
final List<HttpRequest>? list;
|
||||
final bool shrinkWrap;
|
||||
|
||||
const DomainWidget({super.key, required this.panel, required this.proxyServer});
|
||||
const DomainWidget({super.key, required this.panel, required this.proxyServer, this.list, this.shrinkWrap = true});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -28,7 +31,8 @@ class DomainWidget extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClientMixin{
|
||||
class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClientMixin {
|
||||
List<HttpRequest> container = [];
|
||||
LinkedHashMap<HostAndPort, HeaderBody> containerMap = LinkedHashMap<HostAndPort, HeaderBody>();
|
||||
LinkedHashMap<HostAndPort, HeaderBody> searchView = LinkedHashMap<HostAndPort, HeaderBody>();
|
||||
|
||||
@@ -47,6 +51,22 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.list?.forEach((request) {
|
||||
var host = HostAndPort.of(request.requestUrl);
|
||||
HeaderBody? headerBody = containerMap[host];
|
||||
if (headerBody == null) {
|
||||
headerBody = HeaderBody(host, proxyServer: widget.panel.proxyServer, onRemove: () => remove(host));
|
||||
containerMap[host] = headerBody;
|
||||
}
|
||||
var listURI =
|
||||
PathRow(request, widget.panel, proxyServer: widget.panel.proxyServer, remove: (it) => headerBody!.remove(it));
|
||||
headerBody.addBody(null, listURI);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@@ -63,8 +83,11 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
|
||||
searchView.clear();
|
||||
}
|
||||
|
||||
Widget body = widget.shrinkWrap
|
||||
? SingleChildScrollView(child: Column(children: list.toList()))
|
||||
: ListView.builder(itemCount: list.length, cacheExtent: 1000, itemBuilder: (_, index) => list.elementAt(index));
|
||||
return Scaffold(
|
||||
body: SingleChildScrollView(child: Column(children: list.toList())),
|
||||
body: body,
|
||||
bottomNavigationBar: Search(onSearch: (val) {
|
||||
setState(() {
|
||||
searchModel = val;
|
||||
@@ -88,11 +111,13 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
|
||||
|
||||
///添加请求
|
||||
add(Channel channel, HttpRequest request) {
|
||||
container.add(request);
|
||||
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.host);
|
||||
//按照域名分类
|
||||
HeaderBody? headerBody = containerMap[hostAndPort];
|
||||
if (headerBody != null) {
|
||||
var listURI = PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
|
||||
var listURI =
|
||||
PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
|
||||
headerBody.addBody(channel.id, listURI);
|
||||
|
||||
//搜索视图
|
||||
@@ -103,7 +128,8 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
|
||||
}
|
||||
|
||||
headerBody = HeaderBody(hostAndPort, proxyServer: widget.proxyServer, onRemove: () => remove(hostAndPort));
|
||||
var listURI = PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
|
||||
var listURI =
|
||||
PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it));
|
||||
headerBody.addBody(channel.id, listURI);
|
||||
setState(() {
|
||||
containerMap[hostAndPort] = headerBody!;
|
||||
@@ -113,6 +139,7 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
|
||||
remove(HostAndPort hostAndPort) {
|
||||
setState(() {
|
||||
containerMap.remove(hostAndPort);
|
||||
container.removeWhere((element) => element.hostAndPort == hostAndPort);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,6 +167,7 @@ class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClien
|
||||
clean() {
|
||||
widget.panel.change(null, null);
|
||||
setState(() {
|
||||
container.clear();
|
||||
containerMap.clear();
|
||||
});
|
||||
}
|
||||
@@ -156,7 +184,7 @@ class HeaderBody extends StatefulWidget {
|
||||
final ProxyServer proxyServer;
|
||||
|
||||
//请求列表
|
||||
final Queue<PathRow> _body = Queue();
|
||||
final Queue<PathRow> body = Queue();
|
||||
|
||||
//是否选中
|
||||
final bool selected;
|
||||
@@ -168,8 +196,11 @@ class HeaderBody extends StatefulWidget {
|
||||
: super(key: GlobalKey<_HeaderBodyState>());
|
||||
|
||||
///添加请求
|
||||
void addBody(String key, PathRow widget) {
|
||||
_body.addFirst(widget);
|
||||
void addBody(String? key, PathRow widget) {
|
||||
body.addFirst(widget);
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
channelIdPathMap[key] = widget;
|
||||
changeState();
|
||||
}
|
||||
@@ -179,14 +210,15 @@ class HeaderBody extends StatefulWidget {
|
||||
}
|
||||
|
||||
remove(PathRow pathRow) {
|
||||
if (_body.remove(pathRow)) {
|
||||
if (body.remove(pathRow)) {
|
||||
changeState();
|
||||
}
|
||||
}
|
||||
|
||||
///根据文本过滤
|
||||
Iterable<PathRow> search(SearchModel searchModel) {
|
||||
return _body.where((element) => searchModel.filter(element.request, element.response.get()));
|
||||
return body
|
||||
.where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response));
|
||||
}
|
||||
|
||||
///复制
|
||||
@@ -195,7 +227,7 @@ class HeaderBody extends StatefulWidget {
|
||||
var headerBody = HeaderBody(header,
|
||||
selected: selected ?? state.currentState?.selected == true, onRemove: onRemove, proxyServer: proxyServer);
|
||||
if (body != null) {
|
||||
headerBody._body.addAll(body);
|
||||
headerBody.body.addAll(body);
|
||||
}
|
||||
return headerBody;
|
||||
}
|
||||
@@ -237,13 +269,13 @@ class _HeaderBodyState extends State<HeaderBody> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
_hostWidget(widget.header.domain),
|
||||
Offstage(offstage: !selected, child: Column(children: widget._body.toList()))
|
||||
Offstage(offstage: !selected, child: Column(children: widget.body.toList()))
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _hostWidget(String title) {
|
||||
var host = GestureDetector(
|
||||
onSecondaryLongPressDown: menu,
|
||||
onSecondaryTapDown: menu,
|
||||
child: ListTile(
|
||||
minLeadingWidth: 25,
|
||||
leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 18),
|
||||
@@ -270,46 +302,42 @@ class _HeaderBodyState extends State<HeaderBody> {
|
||||
}
|
||||
|
||||
//域名右键菜单
|
||||
menu(LongPressDownDetails details) {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
),
|
||||
menu(TapDownDetails details) {
|
||||
showContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
items: <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
height: 38,
|
||||
child: const Text("添加黑名单", style: TextStyle(fontSize: 14)),
|
||||
CustomPopupMenuItem(
|
||||
height: 35,
|
||||
child: const Text("添加黑名单", style: TextStyle(fontSize: 13)),
|
||||
onTap: () {
|
||||
HostFilter.blacklist.add(widget.header.host);
|
||||
configuration.flushConfig();
|
||||
}),
|
||||
PopupMenuItem(
|
||||
height: 38,
|
||||
child: const Text("添加白名单", style: TextStyle(fontSize: 14)),
|
||||
CustomPopupMenuItem(
|
||||
height: 35,
|
||||
child: const Text("添加白名单", style: TextStyle(fontSize: 13)),
|
||||
onTap: () {
|
||||
HostFilter.whitelist.add(widget.header.host);
|
||||
configuration.flushConfig();
|
||||
}),
|
||||
PopupMenuItem(
|
||||
height: 38,
|
||||
child: const Text("删除白名单", style: TextStyle(fontSize: 14)),
|
||||
CustomPopupMenuItem(
|
||||
height: 35,
|
||||
child: const Text("删除白名单", style: TextStyle(fontSize: 13)),
|
||||
onTap: () {
|
||||
HostFilter.whitelist.remove(widget.header.host);
|
||||
configuration.flushConfig();
|
||||
}),
|
||||
PopupMenuItem(height: 38, child: const Text("删除", style: TextStyle(fontSize: 14)), onTap: () => _delete()),
|
||||
const PopupMenuDivider(height: 0.3),
|
||||
CustomPopupMenuItem(
|
||||
height: 35, child: const Text("删除", style: TextStyle(fontSize: 13)), onTap: () => _delete()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_delete() {
|
||||
widget.channelIdPathMap.clear();
|
||||
widget._body.clear();
|
||||
widget.body.clear();
|
||||
widget.onRemove?.call();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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/utils.dart';
|
||||
import 'package:network_proxy/ui/component/widgets.dart';
|
||||
import 'package:network_proxy/ui/content/panel.dart';
|
||||
import 'package:network_proxy/utils/curl.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@@ -101,14 +102,9 @@ class _FavoriteItemState extends State<_FavoriteItem> {
|
||||
|
||||
///右键菜单
|
||||
menu(LongPressDownDetails details) {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
),
|
||||
showContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
items: <PopupMenuEntry>[
|
||||
popupItem("复制请求链接", onTap: () {
|
||||
var requestUrl = widget.request.requestUrl;
|
||||
@@ -141,7 +137,7 @@ class _FavoriteItemState extends State<_FavoriteItem> {
|
||||
}
|
||||
|
||||
PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {
|
||||
return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14)));
|
||||
return CustomPopupMenuItem(height: 35, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));
|
||||
}
|
||||
|
||||
///请求编辑
|
||||
|
||||
234
lib/ui/desktop/left/history.dart
Normal file
234
lib/ui/desktop/left/history.dart
Normal file
@@ -0,0 +1,234 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:date_format/date_format.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:network_proxy/network/bin/server.dart';
|
||||
import 'package:network_proxy/network/channel.dart';
|
||||
import 'package:network_proxy/network/handler.dart';
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/storage/histories.dart';
|
||||
import 'package:network_proxy/ui/component/utils.dart';
|
||||
import 'package:network_proxy/ui/component/widgets.dart';
|
||||
import 'package:network_proxy/utils/har.dart';
|
||||
|
||||
import '../../content/panel.dart';
|
||||
import 'domain.dart';
|
||||
|
||||
///历史记录
|
||||
class HistoryPageWidget extends StatelessWidget {
|
||||
final ProxyServer proxyServer;
|
||||
final GlobalKey<DomainWidgetState> domainWidgetState;
|
||||
final NetworkTabController panel;
|
||||
|
||||
const HistoryPageWidget({super.key, required this.proxyServer, required this.domainWidgetState, required this.panel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
onGenerateRoute: (settings) {
|
||||
switch (settings.name) {
|
||||
case "/domain":
|
||||
return MaterialPageRoute(builder: (_) => domainWidget(settings.arguments as Map));
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => futureWidget(
|
||||
HistoryStorage.instance,
|
||||
(storage) => _HistoryWidget(storage,
|
||||
container: domainWidgetState.currentState!.container, proxyServer: proxyServer),
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget domainWidget(Map arguments) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(40),
|
||||
child: AppBar(
|
||||
leading: BackButton(style: ButtonStyle(iconSize: MaterialStateProperty.all(15))),
|
||||
centerTitle: false,
|
||||
title: Text(arguments['title'], style: const TextStyle(fontSize: 14)),
|
||||
)),
|
||||
body: futureWidget(HistoryStorage.instance.then((value) => value.getRequests(arguments['name'])), (data) {
|
||||
return DomainWidget(panel: panel, proxyServer: proxyServer, list: data, shrinkWrap: false);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryWidget extends StatefulWidget {
|
||||
// 存储
|
||||
final HistoryStorage storage;
|
||||
final List<HttpRequest> container;
|
||||
final ProxyServer proxyServer;
|
||||
|
||||
const _HistoryWidget(this.storage, {required this.container, required this.proxyServer});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _HistoryState();
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryState extends State<_HistoryWidget> implements EventListener {
|
||||
///是否保存会话
|
||||
static bool _sessionSaved = false;
|
||||
static WriteTask? writeTask;
|
||||
|
||||
// 存储
|
||||
late HistoryStorage storage;
|
||||
|
||||
late List<HttpRequest> container;
|
||||
late ProxyServer proxyServer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
storage = widget.storage;
|
||||
container = widget.container;
|
||||
proxyServer = widget.proxyServer;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print("_HistoryState build");
|
||||
List<Widget> children = [];
|
||||
if (!_sessionSaved) {
|
||||
//当前会话未保存,是否保存当前会话
|
||||
children.add(buildSaveSession(container));
|
||||
}
|
||||
|
||||
var entries = storage.histories.entries;
|
||||
for (int i = entries.length - 1; i >= 0; i--) {
|
||||
var entry = entries.elementAt(i);
|
||||
children.add(buildItem(context, entry.key, entry.value));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (_, index) => children[index],
|
||||
separatorBuilder: (_, index) => const Divider(thickness: 0.3, height: 0),
|
||||
);
|
||||
}
|
||||
|
||||
//构建保存会话
|
||||
Widget buildSaveSession(List<HttpRequest> container) {
|
||||
var name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]);
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(name),
|
||||
subtitle: Text("当前会话未保存 记录数 ${container.length}"),
|
||||
trailing: TextButton.icon(
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text("保存"),
|
||||
onPressed: () async {
|
||||
await _writeHarFile(container, name);
|
||||
setState(() {
|
||||
_sessionSaved = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
onTap: () => ContextMenuController.removeAny());
|
||||
}
|
||||
|
||||
//构建历史记录
|
||||
Widget buildItem(BuildContext context, String name, HistoryItem item) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: (details) => {
|
||||
showContextMenu(context, details.globalPosition, items: [
|
||||
CustomPopupMenuItem(
|
||||
height: 35,
|
||||
child: const Text('删除', style: TextStyle(fontSize: 13)),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (name == writeTask?.name) {
|
||||
writeTask?.timer?.cancel();
|
||||
writeTask?.open.close();
|
||||
}
|
||||
storage.removeHistory(name);
|
||||
});
|
||||
})
|
||||
])
|
||||
},
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text(name),
|
||||
subtitle: Text("记录数 ${item.requestLength} 文件 ${item.size}"),
|
||||
onTap: () {
|
||||
ContextMenuController.removeAny();
|
||||
Navigator.pushNamed(context, '/domain',
|
||||
arguments: {'title': '$name 记录数 ${item.requestLength}', 'name': name})
|
||||
.then((value) => Future.delayed(const Duration(seconds: 60), () => storage.removeCache(name)));
|
||||
}));
|
||||
}
|
||||
|
||||
//写入文件
|
||||
_writeHarFile(List<HttpRequest> container, String name) async {
|
||||
var file = await HistoryStorage.openFile("${DateTime.now().millisecondsSinceEpoch}.txt");
|
||||
print(file);
|
||||
RandomAccessFile open = await file.open(mode: FileMode.append);
|
||||
storage.addHistory(name, file, 0);
|
||||
|
||||
writeTask = WriteTask(name, open, storage, callback: () => setState(() {}));
|
||||
writeTask?.writeList.addAll(container);
|
||||
writeTask?.startTask();
|
||||
|
||||
proxyServer.addListener(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(Channel channel, HttpRequest request) {}
|
||||
|
||||
@override
|
||||
void onResponse(Channel channel, HttpResponse response) async {
|
||||
if (response.request == null) {
|
||||
return;
|
||||
}
|
||||
writeTask?.writeList.add(response.request!);
|
||||
}
|
||||
}
|
||||
|
||||
class WriteTask {
|
||||
final HistoryStorage historyStorage;
|
||||
final RandomAccessFile open;
|
||||
Queue writeList = Queue();
|
||||
Timer? timer;
|
||||
final Function? callback;
|
||||
final String name;
|
||||
|
||||
WriteTask(this.name, this.open, this.historyStorage, {this.callback});
|
||||
|
||||
//写入任务
|
||||
startTask() {
|
||||
timer = Timer.periodic(const Duration(seconds: 15), (it) => writeTask());
|
||||
}
|
||||
|
||||
//写入任务
|
||||
writeTask() async {
|
||||
if (writeList.isEmpty) {
|
||||
return;
|
||||
}
|
||||
var history = historyStorage.getHistory(name);
|
||||
int length = history.requestLength;
|
||||
|
||||
while (writeList.isNotEmpty) {
|
||||
var request = writeList.removeFirst();
|
||||
var har = Har.toHar(request);
|
||||
|
||||
await open.writeString(jsonEncode(har));
|
||||
await open.writeString(",\n");
|
||||
length++;
|
||||
}
|
||||
|
||||
await open.flush(); //刷新
|
||||
|
||||
history.requestLength = length;
|
||||
history.fileSize = await open.length();
|
||||
historyStorage.updateHistory(name, history);
|
||||
callback?.call();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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/utils.dart';
|
||||
import 'package:network_proxy/ui/component/widgets.dart';
|
||||
import 'package:network_proxy/ui/content/panel.dart';
|
||||
import 'package:network_proxy/utils/curl.dart';
|
||||
import 'package:network_proxy/utils/lang.dart';
|
||||
@@ -55,11 +56,12 @@ class _PathRowState extends State<PathRow> {
|
||||
title = '${request.method.name} ${Uri.parse(request.uri).path}';
|
||||
} catch (_) {}
|
||||
var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);
|
||||
|
||||
return GestureDetector(
|
||||
onSecondaryLongPressDown: menu,
|
||||
onSecondaryTapDown: menu,
|
||||
child: ListTile(
|
||||
minLeadingWidth: 25,
|
||||
leading: getIcon(widget.response.get()),
|
||||
leading: getIcon(widget.response.get() ?? widget.request.response),
|
||||
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
subtitle: Text(
|
||||
'$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ',
|
||||
@@ -75,15 +77,10 @@ class _PathRowState extends State<PathRow> {
|
||||
}
|
||||
|
||||
///右键菜单
|
||||
menu(LongPressDownDetails details) {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
),
|
||||
menu(TapDownDetails details) {
|
||||
showContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
items: <PopupMenuEntry>[
|
||||
popupItem("复制请求链接", onTap: () {
|
||||
var requestUrl = widget.request.requestUrl;
|
||||
@@ -97,6 +94,7 @@ class _PathRowState extends State<PathRow> {
|
||||
Clipboard.setData(ClipboardData(text: curlRequest(widget.request)))
|
||||
.then((value) => FlutterToastr.show('已复制到剪切板', context));
|
||||
}),
|
||||
const PopupMenuDivider(height: 0.3),
|
||||
popupItem("重放请求", onTap: () {
|
||||
var request = widget.request.copy(uri: widget.request.requestUrl);
|
||||
HttpClients.proxyRequest(request);
|
||||
@@ -112,6 +110,7 @@ class _PathRowState extends State<PathRow> {
|
||||
FavoriteStorage.addFavorite(widget.request);
|
||||
FlutterToastr.show('收藏成功', context);
|
||||
}),
|
||||
const PopupMenuDivider(height: 0.3),
|
||||
popupItem("删除", onTap: () {
|
||||
widget.remove?.call(widget);
|
||||
}),
|
||||
@@ -120,7 +119,7 @@ class _PathRowState extends State<PathRow> {
|
||||
}
|
||||
|
||||
PopupMenuItem popupItem(String text, {VoidCallback? onTap}) {
|
||||
return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14)));
|
||||
return CustomPopupMenuItem(height: 32, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13)));
|
||||
}
|
||||
|
||||
///请求编辑
|
||||
@@ -157,6 +156,6 @@ class _PathRowState extends State<PathRow> {
|
||||
});
|
||||
}
|
||||
selectedState = this;
|
||||
widget.panel.change(widget.request, widget.response.get());
|
||||
widget.panel.change(widget.request, widget.response.get() ?? widget.request.response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
|
||||
title: const Text("添加请求重写规则", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
scrollable: true,
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(minWidth: 320),
|
||||
constraints: const BoxConstraints(minWidth: 350, minHeight: 460),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
@@ -158,22 +158,25 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
|
||||
valueListenable: enableNotifier,
|
||||
builder: (_, bool enable, __) {
|
||||
return SwitchListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.only(left: 0),
|
||||
title: const Text('是否启用', textAlign: TextAlign.start),
|
||||
value: enable,
|
||||
onChanged: (value) => enableNotifier.value = value);
|
||||
}),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '名称'),
|
||||
decoration: decoration('名称'),
|
||||
initialValue: rule.name,
|
||||
onSaved: (val) => rule.name = val,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
|
||||
decoration: decoration('域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
|
||||
initialValue: rule.domain,
|
||||
onSaved: (val) => rule.domain = val?.trim()),
|
||||
const SizedBox(height: 5),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'),
|
||||
decoration: decoration('Path', hintText: '/api/v1/*'),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Path不能为空';
|
||||
@@ -182,9 +185,11 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
|
||||
},
|
||||
initialValue: rule.path,
|
||||
onSaved: (val) => rule.path = val!.trim()),
|
||||
const SizedBox(height: 5),
|
||||
DropdownButtonFormField<RuleType>(
|
||||
decoration: const InputDecoration(labelText: '行为'),
|
||||
value: rule.type,
|
||||
isDense: true,
|
||||
decoration: decoration('行为'),
|
||||
items: RuleType.values
|
||||
.map((e) =>
|
||||
DropdownMenuItem(value: e, child: Text(e.name, style: const TextStyle(fontSize: 14))))
|
||||
@@ -194,6 +199,7 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
|
||||
rule.type = val!;
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 5),
|
||||
...rewriteWidgets()
|
||||
]))),
|
||||
actions: [
|
||||
@@ -222,11 +228,25 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
|
||||
]);
|
||||
}
|
||||
|
||||
InputDecoration decoration(String label, {String? hintText}) {
|
||||
Color color = Theme.of(context).colorScheme.primary;
|
||||
// Color color = Colors.blueAccent;
|
||||
|
||||
return InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hintText,
|
||||
isDense: true,
|
||||
border: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)),
|
||||
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)),
|
||||
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: 1.5, color: color)));
|
||||
}
|
||||
|
||||
List<Widget> rewriteWidgets() {
|
||||
if (rule.type == RuleType.redirect) {
|
||||
return [
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '重定向到:', hintText: 'http://www.example.com/api'),
|
||||
decoration: decoration('重定向到:', hintText: 'http://www.example.com/api'),
|
||||
maxLines: 3,
|
||||
initialValue: rule.redirectUrl,
|
||||
onSaved: (val) => rule.redirectUrl = val,
|
||||
validator: (val) {
|
||||
@@ -241,20 +261,22 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
|
||||
return [
|
||||
TextFormField(
|
||||
initialValue: rule.queryParam,
|
||||
decoration: const InputDecoration(labelText: 'URL参数替换为:'),
|
||||
decoration: decoration('URL参数替换为:'),
|
||||
maxLines: 1,
|
||||
onSaved: (val) => rule.queryParam = val),
|
||||
const SizedBox(height: 5),
|
||||
TextFormField(
|
||||
initialValue: rule.requestBody,
|
||||
decoration: const InputDecoration(labelText: '请求体替换为:'),
|
||||
decoration: decoration('请求体替换为:'),
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
onSaved: (val) => rule.requestBody = val),
|
||||
const SizedBox(height: 5),
|
||||
TextFormField(
|
||||
initialValue: rule.responseBody,
|
||||
minLines: 3,
|
||||
maxLines: 15,
|
||||
decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'),
|
||||
maxLines: 10,
|
||||
decoration: decoration('响应体替换为:', hintText: '{"code":"200","data":{}}'),
|
||||
onSaved: (val) => rule.responseBody = val)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -46,15 +46,13 @@ class _SettingState extends State<Setting> {
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem<String>(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13))),
|
||||
PopupMenuItem<String>(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: enableDesktopListenable,
|
||||
builder: (_, val, __) => setSystemProxy(),
|
||||
)),
|
||||
const PopupMenuItem(padding: EdgeInsets.all(0), child: ThemeSetting(dense: true)),
|
||||
valueListenable: enableDesktopListenable,
|
||||
builder: (_, val, __) => setSystemProxy(),
|
||||
)),
|
||||
const PopupMenuItem(child: ThemeSetting(dense: true)),
|
||||
menuItem("域名过滤", onTap: hostFilter),
|
||||
menuItem("请求重写", onTap: requestRewrite),
|
||||
menuItem("外部代理设置", onTap: setExternalProxy),
|
||||
@@ -71,7 +69,6 @@ class _SettingState extends State<Setting> {
|
||||
|
||||
PopupMenuItem<String> menuItem(String title, {GestureTapCallback? onTap}) {
|
||||
return PopupMenuItem<String>(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: ListTile(
|
||||
title: Text(title),
|
||||
dense: true,
|
||||
@@ -185,7 +182,6 @@ class _PortState extends State<PortWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(children: [
|
||||
const Padding(padding: EdgeInsets.only(left: 16)),
|
||||
Text("端口号:", style: widget.textStyle),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
|
||||
@@ -29,10 +29,8 @@ class _SslState extends State<SslWidget> {
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: _Switch(proxyServer: widget.proxyServer, onEnableChange: (val) => setState(() {}))),
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
hoverColor: Colors.transparent,
|
||||
@@ -44,7 +42,6 @@ class _SslState extends State<SslWidget> {
|
||||
},
|
||||
)),
|
||||
PopupMenuItem<String>(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: ListTile(
|
||||
title: const Text("安装根证书到 iOS"),
|
||||
dense: true,
|
||||
@@ -56,7 +53,6 @@ class _SslState extends State<SslWidget> {
|
||||
}),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: ListTile(
|
||||
title: const Text("安装根证书到 Android"),
|
||||
dense: true,
|
||||
@@ -68,7 +64,6 @@ class _SslState extends State<SslWidget> {
|
||||
}),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: ListTile(
|
||||
title: const Text("下载根证书"),
|
||||
dense: true,
|
||||
@@ -288,4 +283,4 @@ class _SwitchState extends State<_Switch> {
|
||||
widget.proxyServer.configuration.flushConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user