mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-10 00:44:12 +08:00
桌面端增加历史记录功能
This commit is contained in:
@@ -18,6 +18,8 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:network_proxy/network/bin/configuration.dart';
|
||||
import 'package:network_proxy/network/channel.dart';
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
|
||||
import '../handler.dart';
|
||||
import '../http/codec.dart';
|
||||
@@ -36,12 +38,12 @@ class ProxyServer {
|
||||
Server? server;
|
||||
|
||||
//请求事件监听
|
||||
EventListener? listener;
|
||||
List<EventListener> listeners = [];
|
||||
|
||||
//配置
|
||||
final Configuration configuration;
|
||||
|
||||
ProxyServer(this.configuration, {this.listener});
|
||||
ProxyServer(this.configuration);
|
||||
|
||||
//是否启动
|
||||
bool get isRunning => server?.isRunning ?? false;
|
||||
@@ -67,8 +69,11 @@ class ProxyServer {
|
||||
Server server = Server(configuration);
|
||||
|
||||
server.initChannel((channel) {
|
||||
channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(),
|
||||
HttpChannelHandler(listener: listener, requestRewrites: configuration.requestRewrites));
|
||||
channel.pipeline.handle(
|
||||
HttpRequestCodec(),
|
||||
HttpResponseCodec(),
|
||||
HttpChannelHandler(
|
||||
listener: CombinedEventListener(listeners), requestRewrites: configuration.requestRewrites));
|
||||
});
|
||||
|
||||
return server.bind(port).then((serverSocket) {
|
||||
@@ -106,4 +111,29 @@ class ProxyServer {
|
||||
restart() {
|
||||
stop().then((value) => start());
|
||||
}
|
||||
|
||||
///添加监听器
|
||||
addListener(EventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
class CombinedEventListener extends EventListener {
|
||||
final List<EventListener> listeners;
|
||||
|
||||
CombinedEventListener(this.listeners);
|
||||
|
||||
@override
|
||||
void onRequest(Channel channel, HttpRequest request) {
|
||||
for (var element in listeners) {
|
||||
element.onRequest(channel, request);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Channel channel, HttpResponse response) {
|
||||
for (var element in listeners) {
|
||||
element.onResponse(channel, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
|
||||
/// 转发请求
|
||||
Future<void> forward(Channel channel, HttpRequest httpRequest) async {
|
||||
log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
// log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
if (channel.error != null) {
|
||||
_exceptionHandler(channel, httpRequest, channel.error);
|
||||
return;
|
||||
|
||||
@@ -63,7 +63,7 @@ class HostAndPort {
|
||||
List<String> hostAndPort = domain.split(":");
|
||||
if (hostAndPort.length == 2) {
|
||||
bool isSsl = ssl ?? hostAndPort[1] == "443";
|
||||
scheme = isSsl ? httpsScheme : httpScheme;
|
||||
scheme ??= isSsl ? httpsScheme : httpScheme;
|
||||
return HostAndPort(scheme, hostAndPort[0], int.parse(hostAndPort[1]));
|
||||
}
|
||||
scheme ??= (ssl == true ? httpsScheme : httpScheme);
|
||||
|
||||
@@ -109,7 +109,7 @@ class HttpClients {
|
||||
{ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 3)}) async {
|
||||
if (request.headers.host == null || request.headers.host?.trim().isEmpty == true) {
|
||||
try {
|
||||
request.headers.host = Uri.parse(request.uri).host;
|
||||
request.headers.host = '${Uri.parse(request.uri).host}:${Uri.parse(request.uri).port}';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,11 +29,13 @@ class HostFilter {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
abstract class HostList {
|
||||
/// 列表
|
||||
final List<RegExp> list = [];
|
||||
bool enabled = false;
|
||||
|
||||
///加载配置
|
||||
void load(Map<String, dynamic>? map) {
|
||||
if (map == null) {
|
||||
return;
|
||||
@@ -62,6 +64,7 @@ abstract class HostList {
|
||||
}
|
||||
}
|
||||
|
||||
// json序列化
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'list': list.map((e) => e.pattern).toList(),
|
||||
@@ -70,8 +73,10 @@ abstract class HostList {
|
||||
}
|
||||
}
|
||||
|
||||
///白名单
|
||||
class Whites extends HostList {}
|
||||
|
||||
///黑名单
|
||||
class Blacks extends HostList {
|
||||
Blacks() {
|
||||
enabled = true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/// @author wanghongen
|
||||
/// 2023/7/26
|
||||
/// 请求重写
|
||||
class RequestRewrites {
|
||||
bool enabled = true;
|
||||
final List<RequestRewriteRule> rules = [];
|
||||
@@ -11,6 +12,7 @@ class RequestRewrites {
|
||||
|
||||
static RequestRewrites get instance => _instance;
|
||||
|
||||
//加载配置
|
||||
load(Map<String, dynamic>? map) {
|
||||
if (map == null) {
|
||||
return;
|
||||
@@ -23,6 +25,7 @@ class RequestRewrites {
|
||||
});
|
||||
}
|
||||
|
||||
///
|
||||
RequestRewriteRule? findRequestRewrite(String? domain, String? url, RuleType type) {
|
||||
if (!enabled || url == null) {
|
||||
return null;
|
||||
@@ -54,6 +57,7 @@ class RequestRewrites {
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
void addRule(RequestRewriteRule rule) {
|
||||
rules.removeWhere((it) => it.path == rule.path && it.domain == rule.domain);
|
||||
rules.add(rule);
|
||||
@@ -110,6 +114,7 @@ class RequestRewriteRule {
|
||||
{this.name, this.type = RuleType.body, this.queryParam, this.requestBody, this.responseBody, this.redirectUrl})
|
||||
: urlReg = RegExp(path.replaceAll("*", ".*"));
|
||||
|
||||
///
|
||||
factory RequestRewriteRule.formJson(Map<String, dynamic> map) {
|
||||
return RequestRewriteRule(map['enabled'] == true, map['path'], map['domain'],
|
||||
name: map['name'],
|
||||
|
||||
149
lib/storage/histories.dart
Normal file
149
lib/storage/histories.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
import 'package:network_proxy/utils/har.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
///历史存储
|
||||
class HistoryStorage {
|
||||
static HistoryStorage? _instance;
|
||||
|
||||
HistoryStorage._internal();
|
||||
|
||||
static final LinkedHashMap<String, HistoryItem> _histories = LinkedHashMap<String, HistoryItem>();
|
||||
|
||||
static final Map<String, List<HttpRequest>> _requests = {};
|
||||
|
||||
///单例
|
||||
static Future<HistoryStorage> get instance async {
|
||||
if (_instance == null) {
|
||||
_instance = HistoryStorage._internal();
|
||||
await _init();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
//初始化
|
||||
static Future<void> _init() async {
|
||||
var file = await _path;
|
||||
if (await file.exists()) {
|
||||
var content = await file.readAsString();
|
||||
if (content.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
final Map<dynamic, dynamic> data = jsonDecode(content);
|
||||
for (var entry in data.entries) {
|
||||
_histories[entry.key] = HistoryItem.formJson(entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取历史记录
|
||||
Map<String, HistoryItem> get histories {
|
||||
return _histories;
|
||||
}
|
||||
|
||||
//获取配置路径
|
||||
static Future<File> get _path async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
var file = File('${directory.path}${Platform.pathSeparator}histories.json');
|
||||
if (!await file.exists()) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
///打开文件
|
||||
static Future<File> openFile(String name) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
var file = File('${directory.path}${Platform.pathSeparator}history${Platform.pathSeparator}$name');
|
||||
return file.create(recursive: true);
|
||||
}
|
||||
|
||||
/// 添加历史记录
|
||||
void addHistory(String name, File file, int requestLength) async {
|
||||
var size = await file.length();
|
||||
_histories[name] = HistoryItem(file.path, requestLength, size);
|
||||
(await _path).writeAsString(jsonEncode(_histories));
|
||||
}
|
||||
|
||||
//更新
|
||||
updateHistory(String name, HistoryItem item) async {
|
||||
_histories[name] = item;
|
||||
(await _path).writeAsString(jsonEncode(_histories));
|
||||
}
|
||||
|
||||
//获取
|
||||
HistoryItem getHistory(String name) {
|
||||
return _histories[name]!;
|
||||
}
|
||||
|
||||
///删除
|
||||
void removeHistory(String name) async {
|
||||
var history = _histories.remove(name);
|
||||
if (history == null) {
|
||||
return;
|
||||
}
|
||||
var file = File(history.path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
(await _path).writeAsString(jsonEncode(_histories));
|
||||
}
|
||||
|
||||
//获取请求列表
|
||||
Future<List<HttpRequest>> getRequests(String name) async {
|
||||
var request = _requests[name];
|
||||
if (request == null) {
|
||||
HistoryItem history = _histories[name]!;
|
||||
var file = File(history.path);
|
||||
_requests[name] = await Har.readFile(file);
|
||||
histories[name]?.requestLength = _requests[name]!.length;
|
||||
file.length().then((size) => histories[name]?.fileSize = size);
|
||||
}
|
||||
return _requests[name]!;
|
||||
}
|
||||
|
||||
void removeCache(String name) {
|
||||
_requests.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// 历史记录
|
||||
class HistoryItem {
|
||||
final String path; // 文件路径
|
||||
int requestLength = 0; // 请求数量
|
||||
int? fileSize; // 文件大小
|
||||
|
||||
HistoryItem(this.path, this.requestLength, this.fileSize);
|
||||
|
||||
//json反序列化
|
||||
factory HistoryItem.formJson(Map<String, dynamic> map) {
|
||||
return HistoryItem(map['path'], map['requestLength'], map['fileSize']);
|
||||
}
|
||||
|
||||
//json序列化
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'path': path,
|
||||
'requestLength': requestLength,
|
||||
'fileSize': fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
//获取文件大小
|
||||
String get size {
|
||||
if (this.fileSize == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
int fileSize = this.fileSize!;
|
||||
if (fileSize > 1024 * 1024) {
|
||||
return "${(fileSize / 1024 / 1024).toStringAsFixed(1)}MB";
|
||||
}
|
||||
|
||||
return "${(fileSize / 1024).toStringAsFixed(1)}KB";
|
||||
}
|
||||
}
|
||||
96
lib/ui/component/context_menu_region.dart
Normal file
96
lib/ui/component/context_menu_region.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ContextMenuBuilder = List<ContextMenuButtonItem> Function();
|
||||
|
||||
/// 根据用户手势显示和隐藏上下文菜单。
|
||||
/// 默认情况下,在右键单击和长按时显示菜单。
|
||||
class ContextMenuRegion extends StatefulWidget {
|
||||
const ContextMenuRegion({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.contextMenuBuilder,
|
||||
});
|
||||
|
||||
/// Builds the context menu.
|
||||
final ContextMenuBuilder contextMenuBuilder;
|
||||
|
||||
/// The child widget that will be listened to for gestures.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<ContextMenuRegion> createState() => _ContextMenuRegionState();
|
||||
}
|
||||
|
||||
class _ContextMenuRegionState extends State<ContextMenuRegion> {
|
||||
Offset? _longPressOffset;
|
||||
|
||||
final ContextMenuController _contextMenuController = ContextMenuController();
|
||||
|
||||
static bool get _longPressEnabled {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
return true;
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _onSecondaryTapUp(TapUpDetails details) {
|
||||
_show(details.globalPosition);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (!_contextMenuController.isShown) {
|
||||
return;
|
||||
}
|
||||
_hide();
|
||||
}
|
||||
|
||||
void _onLongPressStart(LongPressStartDetails details) {
|
||||
_longPressOffset = details.globalPosition;
|
||||
}
|
||||
|
||||
void _onLongPress() {
|
||||
assert(_longPressOffset != null);
|
||||
_show(_longPressOffset!);
|
||||
_longPressOffset = null;
|
||||
}
|
||||
|
||||
void _show(Offset position) {
|
||||
_contextMenuController.show(
|
||||
context: context,
|
||||
contextMenuBuilder: (context) {
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
buttonItems: widget.contextMenuBuilder.call(),
|
||||
anchors: TextSelectionToolbarAnchors(primaryAnchor: position));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _hide() {
|
||||
_contextMenuController.remove();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hide();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onSecondaryTapUp: _onSecondaryTapUp,
|
||||
onTap: _onTap,
|
||||
onLongPress: _longPressEnabled ? _onLongPress : null,
|
||||
onLongPressStart: _longPressEnabled ? _onLongPressStart : null,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
|
||||
///编码类型
|
||||
enum EncoderType {
|
||||
url,
|
||||
base64,
|
||||
|
||||
@@ -96,3 +96,32 @@ void unSelect(EditableTextState editableTextState) {
|
||||
SelectionChangedCause.tap,
|
||||
);
|
||||
}
|
||||
|
||||
Widget futureWidget<T>(Future<T> future, Widget Function(T data) toWidget) {
|
||||
return FutureBuilder<T>(
|
||||
future: future,
|
||||
builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasError) {
|
||||
print(snapshot.error);
|
||||
}
|
||||
return toWidget(snapshot.requireData);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
showContextMenu(BuildContext context, Offset offset, {required List<PopupMenuEntry> items}) {
|
||||
showMenu(
|
||||
context: context,
|
||||
surfaceTintColor:
|
||||
Brightness.dark == Theme.of(context).brightness ? null : Theme.of(context).colorScheme.primaryContainer,
|
||||
position: RelativeRect.fromLTRB(
|
||||
offset.dx,
|
||||
offset.dy - 50,
|
||||
offset.dx,
|
||||
offset.dy - 50,
|
||||
),
|
||||
items: items);
|
||||
}
|
||||
|
||||
30
lib/ui/component/widgets.dart
Normal file
30
lib/ui/component/widgets.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomPopupMenuItem<T> extends PopupMenuItem<T> {
|
||||
final Color? color;
|
||||
|
||||
const CustomPopupMenuItem({
|
||||
super.key,
|
||||
super.onTap,
|
||||
super.height,
|
||||
T? value,
|
||||
bool enabled = true,
|
||||
required Widget child,
|
||||
this.color,
|
||||
}) : super(value: value, enabled: enabled, child: child);
|
||||
|
||||
@override
|
||||
PopupMenuItemState<T, CustomPopupMenuItem<T>> createState() => _CustomPopupMenuItemState<T>();
|
||||
}
|
||||
|
||||
class _CustomPopupMenuItemState<T> extends PopupMenuItemState<T, CustomPopupMenuItem<T>> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
hoverColor: Theme.of(context).focusColor,
|
||||
),
|
||||
child: super.build(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
proxyServer = ProxyServer(widget.configuration, listener: this);
|
||||
proxyServer = ProxyServer(widget.configuration);
|
||||
proxyServer.addListener(this);
|
||||
|
||||
// 远程连接
|
||||
desktop.addListener(() {
|
||||
if (desktop.value.connect) {
|
||||
proxyServer.configuration.remoteHost = "http://${desktop.value.host}:${desktop.value.port}";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||||
@@ -135,7 +133,7 @@ class RequestEditorState extends State<MobileRequestEditor> with SingleTickerPro
|
||||
tabController.animateTo(1);
|
||||
responseChange.value = !responseChange.value;
|
||||
}).catchError((e) {
|
||||
FlutterToastr.show('请求失败', context);
|
||||
FlutterToastr.show('请求失败$e', context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ class _RewriteRuleState extends State<RewriteRule> {
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: ListView(children: <Widget>[
|
||||
@@ -181,16 +181,16 @@ class _RewriteRuleState extends State<RewriteRule> {
|
||||
onChanged: (value) => enableNotifier.value = value);
|
||||
}),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '名称'),
|
||||
decoration: decoration('名称'),
|
||||
initialValue: rule.name,
|
||||
onSaved: (val) => rule.name = val,
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
|
||||
decoration: decoration('域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
|
||||
initialValue: rule.domain,
|
||||
onSaved: (val) => rule.domain = val?.trim()),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'),
|
||||
decoration: decoration('Path', hintText: '/api/v1/*'),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Path不能为空';
|
||||
@@ -200,7 +200,7 @@ class _RewriteRuleState extends State<RewriteRule> {
|
||||
initialValue: rule.path,
|
||||
onSaved: (val) => rule.path = val!.trim()),
|
||||
DropdownButtonFormField<RuleType>(
|
||||
decoration: const InputDecoration(labelText: '行为'),
|
||||
decoration: decoration('行为'),
|
||||
value: rule.type,
|
||||
items: RuleType.values
|
||||
.map((e) =>
|
||||
@@ -216,11 +216,24 @@ class _RewriteRuleState extends State<RewriteRule> {
|
||||
);
|
||||
}
|
||||
|
||||
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'),
|
||||
initialValue: rule.redirectUrl,
|
||||
onSaved: (val) => rule.redirectUrl = val,
|
||||
validator: (val) {
|
||||
@@ -235,12 +248,12 @@ class _RewriteRuleState extends State<RewriteRule> {
|
||||
return [
|
||||
TextFormField(
|
||||
initialValue: rule.queryParam,
|
||||
decoration: const InputDecoration(labelText: 'URL参数替换为:'),
|
||||
decoration: decoration('URL参数替换为:'),
|
||||
maxLines: 1,
|
||||
onSaved: (val) => rule.queryParam = val),
|
||||
TextFormField(
|
||||
initialValue: rule.requestBody,
|
||||
decoration: const InputDecoration(labelText: '请求体替换为:'),
|
||||
decoration: decoration('请求体替换为:'),
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
onSaved: (val) => rule.requestBody = val),
|
||||
@@ -248,7 +261,7 @@ class _RewriteRuleState extends State<RewriteRule> {
|
||||
initialValue: rule.responseBody,
|
||||
minLines: 3,
|
||||
maxLines: 15,
|
||||
decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'),
|
||||
decoration: decoration('响应体替换为:', hintText: '{"code":"200","data":{}}'),
|
||||
onSaved: (val) => rule.responseBody = val)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:network_proxy/network/http/http.dart';
|
||||
|
||||
class Hae {
|
||||
List<Map> entries = [];
|
||||
class Har {
|
||||
static int maxBodyLength = 1024 * 1024 * 4;
|
||||
|
||||
void addEntry(HttpRequest request) {
|
||||
entries.add({
|
||||
"startedDateTime": request.requestTime, // 请求发出的时间(ISO 8601)
|
||||
"time": request.response?.responseTime.difference(request.requestTime).inMilliseconds, // 请求耗时(ms)
|
||||
static List<Map> _entries(List<HttpRequest> list) {
|
||||
return list.map((e) => toHar(e)).toList();
|
||||
}
|
||||
|
||||
static Map toHar(HttpRequest request) {
|
||||
bool isImage = request.response?.contentType == ContentType.image;
|
||||
Map har = {
|
||||
"startedDateTime": request.requestTime.toIso8601String(), // 请求发出的时间(ISO 8601)
|
||||
"time": request.response?.responseTime.difference(request.requestTime).inMilliseconds,
|
||||
"request": {
|
||||
"method": request.method.name, // 请求方法
|
||||
"url": request.requestUrl, // 请求地址
|
||||
@@ -15,27 +23,13 @@ class Hae {
|
||||
"headers": _headers(request), // 请求头
|
||||
"queryString": [], // 请求参数
|
||||
"postData": {
|
||||
"mimeType": request.contentType, // 请求体类型
|
||||
"mimeType": request.headers.contentType, // 请求体类型
|
||||
"text": request.bodyAsString, // 请求体内容
|
||||
},
|
||||
"headersSize": -1, // 请求头大小
|
||||
"bodySize": request.body?.length ?? -1, // 请求体大小
|
||||
},
|
||||
'response': {
|
||||
"status": request.response?.status.code, // 响应状态码
|
||||
"statusText": request.response?.status.reasonPhrase, // 响应状态码描述
|
||||
"httpVersion": request.response?.protocolVersion, // HTTP协议版本
|
||||
"cookies": [], // 响应携带的cookie
|
||||
"headers": _headers(request.response), // 响应头
|
||||
"content": {
|
||||
"size": request.response?.body?.length, // 响应体大小
|
||||
"mimeType": request.response?.contentType, // 响应体类型
|
||||
"text": request.response?.bodyAsString, // 响应体内容
|
||||
},
|
||||
"redirectURL": '', // 重定向地址
|
||||
"headersSize": -1, // 响应头大小
|
||||
"bodySize": request.response?.body?.length ?? -1, // 响应体大小
|
||||
},
|
||||
|
||||
"cache": {},
|
||||
'timings': {
|
||||
'send': 0,
|
||||
@@ -43,19 +37,54 @@ class Hae {
|
||||
'receive': 0,
|
||||
},
|
||||
'serverIPAddress': request.response?.remoteAddress
|
||||
});
|
||||
};
|
||||
|
||||
if (request.response != null) {
|
||||
har['response'] = {
|
||||
"status": request.response?.status.code, // 响应状态码
|
||||
"statusText": request.response?.status.reasonPhrase, // 响应状态码描述
|
||||
"httpVersion": request.response?.protocolVersion, // HTTP协议版本
|
||||
"cookies": [], // 响应携带的cookie
|
||||
"headers": _headers(request.response), // 响应头
|
||||
"content": {
|
||||
"size": isImage ? 0 : request.response?.body?.length, // 响应体大小
|
||||
"mimeType": request.response?.headers.contentType, // 响应体类型
|
||||
"text": isImage ? '' : request.response?.bodyAsString, // 响应体内容
|
||||
},
|
||||
"redirectURL": '', // 重定向地址
|
||||
"headersSize": -1, // 响应头大小
|
||||
"bodySize": request.response?.body?.length ?? -1, // 响应体大小
|
||||
};
|
||||
}
|
||||
return har;
|
||||
}
|
||||
|
||||
void toFile() {
|
||||
static Future<File> writeFile(List<HttpRequest> list, File file) async {
|
||||
var entries = _entries(list);
|
||||
Map har = {};
|
||||
har["log"] = {
|
||||
"version": "1.2",
|
||||
"creator": {"name": "ProxyPin", "version": "1.0.1"},
|
||||
"creator": {"name": "ProxyPin", "version": "1.0.2"},
|
||||
"entries": entries,
|
||||
};
|
||||
var json = jsonEncode(har);
|
||||
return file.writeAsString(json);
|
||||
}
|
||||
|
||||
List<Map> _headers(HttpMessage? message) {
|
||||
//读取文件
|
||||
static Future<List<HttpRequest>> readFile(File file) async {
|
||||
var lines = await file.readAsLines();
|
||||
List<HttpRequest> list = [];
|
||||
|
||||
for (var value in lines) {
|
||||
var har = jsonDecode(value.substring(0, value.length - 1));
|
||||
var request = _toRequest(har);
|
||||
list.add(request);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
static List<Map> _headers(HttpMessage? message) {
|
||||
var headers = <Map<String, String>>[];
|
||||
message?.headers.forEach((name, values) {
|
||||
for (var element in values) {
|
||||
@@ -64,4 +93,31 @@ class Hae {
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
static HttpRequest _toRequest(Map har) {
|
||||
var request = har['request'];
|
||||
var method = request['method'];
|
||||
List headers = request['headers'];
|
||||
|
||||
var httpRequest = HttpRequest(HttpMethod.valueOf(method), request['url'], protocolVersion: request['httpVersion']);
|
||||
httpRequest.body = request['postData']['text']?.toString().codeUnits;
|
||||
for (var element in headers) {
|
||||
httpRequest.headers.add(element['name'], element['value']);
|
||||
}
|
||||
var response = har['response'];
|
||||
HttpResponse? httpResponse;
|
||||
if (response != null && response['status'] != null) {
|
||||
httpResponse = HttpResponse(HttpStatus.newStatus(response['status'], response['statusText']),
|
||||
protocolVersion: response['httpVersion']);
|
||||
httpResponse.body = response['content']['text']?.toString().codeUnits;
|
||||
List responseHeaders = response['headers'];
|
||||
for (var element in responseHeaders) {
|
||||
httpResponse.headers.add(element['name'], element['value']);
|
||||
}
|
||||
}
|
||||
|
||||
httpRequest.response = httpResponse;
|
||||
httpResponse?.request = httpRequest;
|
||||
return httpRequest;
|
||||
}
|
||||
}
|
||||
|
||||
24
pubspec.lock
24
pubspec.lock
@@ -85,10 +85,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.6"
|
||||
date_format:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -498,26 +498,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
|
||||
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "6.1.5"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
|
||||
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.6"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1"
|
||||
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -538,10 +538,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
|
||||
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "3.0.8"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -586,10 +586,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247
|
||||
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.3"
|
||||
sdks:
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
||||
@@ -31,7 +31,6 @@ dependencies:
|
||||
share_plus: ^7.1.0
|
||||
brotli: ^0.6.0
|
||||
installed_apps: ^1.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
void main() {
|
||||
print(DateTime.now().toIso8601String());
|
||||
print(DateTime.now().toString());
|
||||
print(DateTime.now().toUtc().toString());
|
||||
print(Platform.version);
|
||||
print(Platform.localHostname);
|
||||
print(Platform.operatingSystem);
|
||||
|
||||
Reference in New Issue
Block a user