Compare commits
No commits in common. "bcca8432f9abaf2a6f225dd3f55f942e55a274e1" and "095e9cc464f8a3d11ad83e84f5ef58a3c91ad7f7" have entirely different histories.
bcca8432f9
...
095e9cc464
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,19 +5,6 @@
|
||||||
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
此项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html) 规范。
|
此项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html) 规范。
|
||||||
|
|
||||||
## [1.1.0] - 2026-01-20
|
|
||||||
|
|
||||||
### 新增 (New)
|
|
||||||
- **密码保护 (Password Protection)**: 支持设置 `password` 为面板添加安全锁,防止非授权查看。
|
|
||||||
- **内置拦截器 (Built-in Interceptor)**: 新增 `YxNetInspectorDioInterceptor`,一行代码集成 Dio。
|
|
||||||
- **cURL 支持**: 详情页新增 cURL 命令生成与复制功能。
|
|
||||||
- **Headers 详情**: 详情页新增独立的请求/响应 Headers 展示卡片。
|
|
||||||
- **API 增强**: `logResponse` 新增 `responseHeaders` 参数。
|
|
||||||
|
|
||||||
### 优化 (Improvements)
|
|
||||||
- 优化长文本展示,支持自动换行与滚动。
|
|
||||||
- 修复非全屏模式下 Header 溢出问题。
|
|
||||||
|
|
||||||
## [1.0.0] - 2024-12-20
|
## [1.0.0] - 2024-12-20
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
|
||||||
|
|
@ -41,22 +41,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
dio:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dio
|
|
||||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "5.9.0"
|
|
||||||
dio_web_adapter:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dio_web_adapter
|
|
||||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.1"
|
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -155,14 +139,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
mime:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: mime
|
|
||||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -262,7 +238,7 @@ packages:
|
||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.4"
|
version: "1.0.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.8.0-0 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.18.0-18.0.pre.54"
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,11 @@ class YxNetInspectorController extends ChangeNotifier {
|
||||||
|
|
||||||
/// 配置信息
|
/// 配置信息
|
||||||
late YxNetInspectorConfig _config;
|
late YxNetInspectorConfig _config;
|
||||||
|
YxNetInspectorConfig get config => _config;
|
||||||
|
|
||||||
/// 网络日志列表
|
/// 网络日志列表
|
||||||
final List<NetworkLogEntry> _logs = <NetworkLogEntry>[];
|
final List<NetworkLogEntry> _logs = <NetworkLogEntry>[];
|
||||||
|
List<NetworkLogEntry> get logs => List.unmodifiable(_logs);
|
||||||
|
|
||||||
/// 统计数据
|
/// 统计数据
|
||||||
int _requestCount = 0;
|
int _requestCount = 0;
|
||||||
|
|
@ -31,54 +33,16 @@ class YxNetInspectorController extends ChangeNotifier {
|
||||||
|
|
||||||
/// 悬浮球显示状态
|
/// 悬浮球显示状态
|
||||||
bool _showFloatingBall = true;
|
bool _showFloatingBall = true;
|
||||||
|
bool get showFloatingBall => _showFloatingBall && _config.isEnabled;
|
||||||
|
|
||||||
/// 面板显示状态
|
/// 面板显示状态
|
||||||
bool _isPanelVisible = false;
|
bool _isPanelVisible = false;
|
||||||
bool _isUnlocked = true; // 默认为已解锁
|
bool get isPanelVisible => _isPanelVisible && _config.isEnabled;
|
||||||
|
|
||||||
/// 初始化控制器
|
/// 初始化控制器配置
|
||||||
void initialize(YxNetInspectorConfig config) {
|
void initialize(YxNetInspectorConfig config) {
|
||||||
_config = config;
|
_config = config;
|
||||||
_showFloatingBall = config.showFloatingBall;
|
_showFloatingBall = config.showFloatingBall;
|
||||||
|
|
||||||
// 如果配置了密码,初始化为锁定状态
|
|
||||||
if (config.password != null && config.password!.isNotEmpty) {
|
|
||||||
_isUnlocked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取当前配置
|
|
||||||
YxNetInspectorConfig get config => _config;
|
|
||||||
|
|
||||||
/// 获取日志列表
|
|
||||||
List<NetworkLogEntry> get logs => List.unmodifiable(_logs);
|
|
||||||
|
|
||||||
/// 获取悬浮球可见性
|
|
||||||
bool get showFloatingBall => _showFloatingBall;
|
|
||||||
|
|
||||||
/// 获取面板可见性
|
|
||||||
bool get isPanelVisible => _isPanelVisible;
|
|
||||||
|
|
||||||
/// 获取是否已解锁
|
|
||||||
bool get isUnlocked => _isUnlocked;
|
|
||||||
|
|
||||||
/// 尝试解锁
|
|
||||||
/// 返回是否成功
|
|
||||||
Future<bool> unlock(String password) async {
|
|
||||||
if (_config.password == password) {
|
|
||||||
_isUnlocked = true;
|
|
||||||
notifyListeners();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 锁定检查器
|
|
||||||
void lock() {
|
|
||||||
if (_config.password != null) {
|
|
||||||
_isUnlocked = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示面板
|
/// 显示面板
|
||||||
|
|
@ -133,7 +97,6 @@ class YxNetInspectorController extends ChangeNotifier {
|
||||||
int? statusCode,
|
int? statusCode,
|
||||||
dynamic responseData,
|
dynamic responseData,
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
Map<String, dynamic>? responseHeaders,
|
|
||||||
}) {
|
}) {
|
||||||
if (!_config.isEnabled) return;
|
if (!_config.isEnabled) return;
|
||||||
|
|
||||||
|
|
@ -147,7 +110,6 @@ class YxNetInspectorController extends ChangeNotifier {
|
||||||
final updatedLog = originalLog.copyWith(
|
final updatedLog = originalLog.copyWith(
|
||||||
statusCode: statusCode,
|
statusCode: statusCode,
|
||||||
responseData: responseData,
|
responseData: responseData,
|
||||||
responseHeaders: responseHeaders,
|
|
||||||
duration: duration,
|
duration: duration,
|
||||||
isSuccess: isSuccess,
|
isSuccess: isSuccess,
|
||||||
status: isSuccess
|
status: isSuccess
|
||||||
|
|
@ -273,43 +235,6 @@ class YxNetInspectorController extends ChangeNotifier {
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取所有日志的字符串表示 (用于导出/分享)
|
|
||||||
String getAllLogsAsString() {
|
|
||||||
final buffer = StringBuffer();
|
|
||||||
buffer.writeln('YxNetInspector Exported Logs');
|
|
||||||
buffer.writeln('Generated at: ${DateTime.now()}');
|
|
||||||
buffer.writeln('Total Requests: $requestCount');
|
|
||||||
buffer.writeln('Success: $successCount, Failed: $_errorCount');
|
|
||||||
buffer.writeln('----------------------------------------');
|
|
||||||
|
|
||||||
for (final log in _logs) {
|
|
||||||
buffer.writeln('[${log.method}] ${log.url}');
|
|
||||||
buffer.writeln('Status: ${log.statusCode ?? "N/A"}');
|
|
||||||
buffer.writeln('Time: ${log.timestamp}');
|
|
||||||
buffer.writeln('Duration: ${log.formattedDuration}');
|
|
||||||
|
|
||||||
if (log.headers != null && log.headers!.isNotEmpty) {
|
|
||||||
buffer.writeln('Headers: ${log.headers}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.requestData != null) {
|
|
||||||
buffer.writeln('Request Data: ${log.requestData}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.responseData != null) {
|
|
||||||
buffer.writeln('Response Data: ${log.responseData}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.errorMessage != null) {
|
|
||||||
buffer.writeln('Error: ${log.errorMessage}');
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.writeln('----------------------------------------');
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 限制日志数量
|
/// 限制日志数量
|
||||||
void _trimLogs() {
|
void _trimLogs() {
|
||||||
if (_logs.length > _config.maxLogs) {
|
if (_logs.length > _config.maxLogs) {
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:yx_net_inspector/src/yx_net_inspector_app.dart';
|
|
||||||
|
|
||||||
/// 网络检查器 Dio 拦截器
|
|
||||||
///
|
|
||||||
/// 用于自动记录 Dio 请求和响应日志到 YxNetInspector
|
|
||||||
class YxNetInspectorDioInterceptor extends Interceptor {
|
|
||||||
@override
|
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
|
||||||
// 记录请求
|
|
||||||
YxNetInspectorGlobal.logRequest(
|
|
||||||
id: options.hashCode.toString(),
|
|
||||||
method: options.method,
|
|
||||||
url: options.uri.toString(),
|
|
||||||
headers: options.headers,
|
|
||||||
requestData: options.data,
|
|
||||||
queryParameters: options.queryParameters,
|
|
||||||
);
|
|
||||||
handler.next(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onResponse(
|
|
||||||
Response<dynamic> response, ResponseInterceptorHandler handler,) {
|
|
||||||
// 记录响应
|
|
||||||
try {
|
|
||||||
// 计算请求耗时(如果可能)
|
|
||||||
// 注意:Dio 的 RequestOptions 不带时间戳,我们这里可能依赖 Controller 内部的计时,
|
|
||||||
// 或者我们可以扩展 RequestOptions 来携带时间戳 (extra)
|
|
||||||
final requestOptions = response.requestOptions;
|
|
||||||
|
|
||||||
YxNetInspectorGlobal.logResponse(
|
|
||||||
id: requestOptions.hashCode.toString(),
|
|
||||||
statusCode: response.statusCode,
|
|
||||||
responseData: response.data,
|
|
||||||
responseHeaders: response.headers.map,
|
|
||||||
// 这里没有准确的耗时,因为 logRequest 记录了开始时间。
|
|
||||||
// Controller 会根据 ID 自动计算耗时:Duration = Now - StartTime
|
|
||||||
);
|
|
||||||
} on Object catch (_) {
|
|
||||||
// 忽略日志记录错误,避免影响业务
|
|
||||||
}
|
|
||||||
handler.next(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
|
||||||
// 记录错误
|
|
||||||
try {
|
|
||||||
final requestOptions = err.requestOptions;
|
|
||||||
|
|
||||||
YxNetInspectorGlobal.logError(
|
|
||||||
id: requestOptions.hashCode.toString(),
|
|
||||||
error: err.message ?? err.toString(),
|
|
||||||
statusCode: err.response?.statusCode,
|
|
||||||
);
|
|
||||||
} on Object catch (_) {
|
|
||||||
// 忽略日志记录错误
|
|
||||||
}
|
|
||||||
handler.next(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,6 @@ class YxNetInspectorConfig {
|
||||||
this.draggable = true,
|
this.draggable = true,
|
||||||
this.showBadge = true,
|
this.showBadge = true,
|
||||||
this.autoHide = false,
|
this.autoHide = false,
|
||||||
this.password,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 是否显示悬浮球
|
/// 是否显示悬浮球
|
||||||
|
|
@ -48,9 +47,6 @@ class YxNetInspectorConfig {
|
||||||
/// 是否自动隐藏悬浮球
|
/// 是否自动隐藏悬浮球
|
||||||
final bool autoHide;
|
final bool autoHide;
|
||||||
|
|
||||||
/// 访问检查器面板所需的密码(如果为空则无需密码)
|
|
||||||
final String? password;
|
|
||||||
|
|
||||||
/// 根据当前模式判断检查器是否应该启用
|
/// 根据当前模式判断检查器是否应该启用
|
||||||
bool get isEnabled {
|
bool get isEnabled {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
|
@ -72,7 +68,6 @@ class YxNetInspectorConfig {
|
||||||
bool? draggable,
|
bool? draggable,
|
||||||
bool? showBadge,
|
bool? showBadge,
|
||||||
bool? autoHide,
|
bool? autoHide,
|
||||||
String? password,
|
|
||||||
}) {
|
}) {
|
||||||
return YxNetInspectorConfig(
|
return YxNetInspectorConfig(
|
||||||
showFloatingBall: showFloatingBall ?? this.showFloatingBall,
|
showFloatingBall: showFloatingBall ?? this.showFloatingBall,
|
||||||
|
|
@ -85,7 +80,6 @@ class YxNetInspectorConfig {
|
||||||
draggable: draggable ?? this.draggable,
|
draggable: draggable ?? this.draggable,
|
||||||
showBadge: showBadge ?? this.showBadge,
|
showBadge: showBadge ?? this.showBadge,
|
||||||
autoHide: autoHide ?? this.autoHide,
|
autoHide: autoHide ?? this.autoHide,
|
||||||
password: password ?? this.password,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,8 +95,7 @@ class YxNetInspectorConfig {
|
||||||
'initialPosition: $initialPosition, '
|
'initialPosition: $initialPosition, '
|
||||||
'draggable: $draggable, '
|
'draggable: $draggable, '
|
||||||
'showBadge: $showBadge, '
|
'showBadge: $showBadge, '
|
||||||
'autoHide: $autoHide, '
|
'autoHide: $autoHide'
|
||||||
'password: ${password != null ? "***" : "null"}'
|
|
||||||
')';
|
')';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,8 +112,7 @@ class YxNetInspectorConfig {
|
||||||
other.initialPosition == initialPosition &&
|
other.initialPosition == initialPosition &&
|
||||||
other.draggable == draggable &&
|
other.draggable == draggable &&
|
||||||
other.showBadge == showBadge &&
|
other.showBadge == showBadge &&
|
||||||
other.autoHide == autoHide &&
|
other.autoHide == autoHide;
|
||||||
other.password == password;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -136,7 +128,6 @@ class YxNetInspectorConfig {
|
||||||
draggable,
|
draggable,
|
||||||
showBadge,
|
showBadge,
|
||||||
autoHide,
|
autoHide,
|
||||||
password,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// 网络请求状态枚举
|
/// 网络请求状态枚举
|
||||||
|
|
@ -24,7 +22,6 @@ class NetworkLogEntry {
|
||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
required this.isSuccess,
|
required this.isSuccess,
|
||||||
this.headers,
|
this.headers,
|
||||||
this.responseHeaders,
|
|
||||||
this.requestData,
|
this.requestData,
|
||||||
this.queryParameters,
|
this.queryParameters,
|
||||||
this.statusCode,
|
this.statusCode,
|
||||||
|
|
@ -37,7 +34,6 @@ class NetworkLogEntry {
|
||||||
final String method;
|
final String method;
|
||||||
final String url;
|
final String url;
|
||||||
final Map<String, dynamic>? headers;
|
final Map<String, dynamic>? headers;
|
||||||
final Map<String, dynamic>? responseHeaders;
|
|
||||||
final dynamic requestData;
|
final dynamic requestData;
|
||||||
final Map<String, dynamic>? queryParameters;
|
final Map<String, dynamic>? queryParameters;
|
||||||
final int? statusCode;
|
final int? statusCode;
|
||||||
|
|
@ -129,39 +125,12 @@ class NetworkLogEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 生成 cURL 命令用于调试 (跨平台兼容: macOS/Linux/Windows)
|
|
||||||
String toCurlCommand() {
|
|
||||||
final buffer = StringBuffer('curl -X $method "$url"');
|
|
||||||
headers?.forEach((key, value) {
|
|
||||||
// 转义双引号以兼容所有平台
|
|
||||||
final escapedValue = value.toString().replaceAll('"', r'\"');
|
|
||||||
buffer.write(' -H "$key: $escapedValue"');
|
|
||||||
});
|
|
||||||
if (requestData != null) {
|
|
||||||
String body;
|
|
||||||
if (requestData is String) {
|
|
||||||
body = requestData as String;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
body = jsonEncode(requestData);
|
|
||||||
} on Object catch (_) {
|
|
||||||
body = requestData.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 转义双引号和反斜杠
|
|
||||||
final escapedBody = body.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
|
|
||||||
buffer.write(' -d "$escapedBody"');
|
|
||||||
}
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建副本并更新字段
|
/// 创建副本并更新字段
|
||||||
NetworkLogEntry copyWith({
|
NetworkLogEntry copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? method,
|
String? method,
|
||||||
String? url,
|
String? url,
|
||||||
Map<String, dynamic>? headers,
|
Map<String, dynamic>? headers,
|
||||||
Map<String, dynamic>? responseHeaders,
|
|
||||||
dynamic requestData,
|
dynamic requestData,
|
||||||
Map<String, dynamic>? queryParameters,
|
Map<String, dynamic>? queryParameters,
|
||||||
int? statusCode,
|
int? statusCode,
|
||||||
|
|
@ -177,7 +146,6 @@ class NetworkLogEntry {
|
||||||
method: method ?? this.method,
|
method: method ?? this.method,
|
||||||
url: url ?? this.url,
|
url: url ?? this.url,
|
||||||
headers: headers ?? this.headers,
|
headers: headers ?? this.headers,
|
||||||
responseHeaders: responseHeaders ?? this.responseHeaders,
|
|
||||||
requestData: requestData ?? this.requestData,
|
requestData: requestData ?? this.requestData,
|
||||||
queryParameters: queryParameters ?? this.queryParameters,
|
queryParameters: queryParameters ?? this.queryParameters,
|
||||||
statusCode: statusCode ?? this.statusCode,
|
statusCode: statusCode ?? this.statusCode,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import 'package:yx_net_inspector/src/widgets/panel/inspector_header.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/panel/inspector_log_list.dart';
|
import 'package:yx_net_inspector/src/widgets/panel/inspector_log_list.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/panel/inspector_search_bar.dart';
|
import 'package:yx_net_inspector/src/widgets/panel/inspector_search_bar.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/panel/inspector_statistics.dart';
|
import 'package:yx_net_inspector/src/widgets/panel/inspector_statistics.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/panel/password_dialog.dart';
|
|
||||||
|
|
||||||
/// 网络检查器面板组件
|
/// 网络检查器面板组件
|
||||||
class YxInspectorPanel extends StatefulWidget {
|
class YxInspectorPanel extends StatefulWidget {
|
||||||
|
|
@ -143,16 +142,6 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: widget.controller,
|
listenable: widget.controller,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
// 如果未解锁,显示密码解锁界面
|
|
||||||
if (!widget.controller.isUnlocked) {
|
|
||||||
return Center(
|
|
||||||
child: PasswordDialog(
|
|
||||||
theme: widget.theme,
|
|
||||||
onUnlock: widget.controller.unlock,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget content;
|
Widget content;
|
||||||
|
|
||||||
if (_showDetailPage && _selectedLog != null) {
|
if (_showDetailPage && _selectedLog != null) {
|
||||||
|
|
@ -185,17 +174,6 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
||||||
isFullScreen: _isFullScreen,
|
isFullScreen: _isFullScreen,
|
||||||
onToggleFullScreen: _toggleFullScreen,
|
onToggleFullScreen: _toggleFullScreen,
|
||||||
onClearLogs: widget.controller.clearLogs,
|
onClearLogs: widget.controller.clearLogs,
|
||||||
onCopyLogs: () {
|
|
||||||
final logs = widget.controller.getAllLogsAsString();
|
|
||||||
Clipboard.setData(ClipboardData(text: logs));
|
|
||||||
if (!context.mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Logs copied to clipboard'),
|
|
||||||
duration: Duration(milliseconds: 1500),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onClose: widget.onClose,
|
onClose: widget.onClose,
|
||||||
),
|
),
|
||||||
InspectorStatistics(
|
InspectorStatistics(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||||
|
|
||||||
|
|
@ -22,21 +21,10 @@ class YxLogDetailPage extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
_buildBasicInfoCard(context),
|
_buildBasicInfoCard(context),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildCurlCard(context),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildRequestInfoCard(),
|
_buildRequestInfoCard(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (log.headers != null && log.headers!.isNotEmpty) ...[
|
|
||||||
_buildHeadersCard('请求头 (Request Headers)', log.headers!),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
_buildResponseInfoCard(),
|
_buildResponseInfoCard(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (log.responseHeaders != null &&
|
|
||||||
log.responseHeaders!.isNotEmpty) ...[
|
|
||||||
_buildHeadersCard('响应头 (Response Headers)', log.responseHeaders!),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
if (log.errorMessage != null) ...[
|
if (log.errorMessage != null) ...[
|
||||||
_buildErrorInfoCard(),
|
_buildErrorInfoCard(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -399,123 +387,4 @@ class YxLogDetailPage extends StatelessWidget {
|
||||||
'${dateTime.minute.toString().padLeft(2, '0')}:'
|
'${dateTime.minute.toString().padLeft(2, '0')}:'
|
||||||
'${dateTime.second.toString().padLeft(2, '0')}';
|
'${dateTime.second.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeadersCard(String title, Map<String, dynamic> headers) {
|
|
||||||
return Card(
|
|
||||||
color: theme.cardColor,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.list_alt, color: theme.primaryColor),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: theme.textColor,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
constraints: const BoxConstraints(maxHeight: 200),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.primaryColor.withValues(alpha: 0.05),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: SelectableText(
|
|
||||||
headers.entries.map((e) => '${e.key}: ${e.value}').join('\n'),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: theme.textColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCurlCard(BuildContext context) {
|
|
||||||
final curlCommand = log.toCurlCommand();
|
|
||||||
return Card(
|
|
||||||
color: theme.cardColor,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.terminal, color: theme.warningColor),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'cURL 命令',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: theme.textColor,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.copy, color: theme.primaryColor, size: 20),
|
|
||||||
tooltip: '复制 cURL',
|
|
||||||
onPressed: () {
|
|
||||||
Clipboard.setData(ClipboardData(text: curlCommand));
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('cURL 已复制到剪贴板'),
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
constraints: const BoxConstraints(maxHeight: 150),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.warningColor.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: SelectableText(
|
|
||||||
curlCommand,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: theme.textColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class InspectorDetailHeader extends StatelessWidget {
|
||||||
|
|
||||||
return isFullScreen
|
return isFullScreen
|
||||||
? AppBar(
|
? AppBar(
|
||||||
|
title: const Text('请求详情'),
|
||||||
backgroundColor: theme.primaryColor,
|
backgroundColor: theme.primaryColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
// excludeHeaderSemantics: false
|
// excludeHeaderSemantics: false
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ class InspectorHeader extends StatelessWidget {
|
||||||
required this.onToggleFullScreen,
|
required this.onToggleFullScreen,
|
||||||
required this.onClearLogs,
|
required this.onClearLogs,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
this.onCopyLogs,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -17,7 +16,6 @@ class InspectorHeader extends StatelessWidget {
|
||||||
final VoidCallback onToggleFullScreen;
|
final VoidCallback onToggleFullScreen;
|
||||||
final VoidCallback onClearLogs;
|
final VoidCallback onClearLogs;
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
final VoidCallback? onCopyLogs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -35,12 +33,6 @@ class InspectorHeader extends StatelessWidget {
|
||||||
icon: const Icon(Icons.clear_all, color: Colors.white),
|
icon: const Icon(Icons.clear_all, color: Colors.white),
|
||||||
tooltip: '清空日志',
|
tooltip: '清空日志',
|
||||||
),
|
),
|
||||||
if (onCopyLogs != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: onCopyLogs,
|
|
||||||
icon: const Icon(Icons.copy, color: Colors.white),
|
|
||||||
tooltip: '复制所有日志',
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: onClose,
|
onPressed: onClose,
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
|
@ -68,6 +60,17 @@ class InspectorHeader extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
const Icon(Icons.network_check, color: Colors.white, size: 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'网络检查器',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
...actions,
|
...actions,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
|
||||||
|
|
||||||
/// 密码输入对话框
|
|
||||||
class PasswordDialog extends StatefulWidget {
|
|
||||||
const PasswordDialog({
|
|
||||||
required this.onUnlock,
|
|
||||||
required this.theme,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 解锁回调
|
|
||||||
final Future<bool> Function(String password) onUnlock;
|
|
||||||
|
|
||||||
/// 主题
|
|
||||||
final YxNetInspectorTheme theme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PasswordDialog> createState() => _PasswordDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PasswordDialogState extends State<PasswordDialog> {
|
|
||||||
final _passwordController = TextEditingController();
|
|
||||||
bool _isLoading = false;
|
|
||||||
String? _errorMessage;
|
|
||||||
bool _obscureText = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_passwordController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleUnlock() async {
|
|
||||||
final password = _passwordController.text;
|
|
||||||
if (password.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_errorMessage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
final success = await widget.onUnlock(password);
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
if (!success) {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
_errorMessage = '密码错误,请重试';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If success, the dialog (or lock screen) will typically be removed by the parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: widget.theme.backgroundColor,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.1),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 10),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
width: 300,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.lock_outline,
|
|
||||||
size: 48,
|
|
||||||
color: widget.theme.primaryColor,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'安全访问',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: widget.theme.textColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'请输入密码以查看网络日志',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: widget.theme.secondaryTextColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
TextField(
|
|
||||||
controller: _passwordController,
|
|
||||||
obscureText: _obscureText,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '输入密码',
|
|
||||||
errorText: _errorMessage,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
prefixIcon: const Icon(Icons.vpn_key),
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
_obscureText ? Icons.visibility : Icons.visibility_off,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_obscureText = !_obscureText;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onSubmitted: (_) => _handleUnlock(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _isLoading ? null : _handleUnlock,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: widget.theme.primaryColor,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: _isLoading
|
|
||||||
? const SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('解锁'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,9 +7,9 @@ import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
||||||
|
|
||||||
/// 包装你的应用并提供网络检查功能的主要组件
|
/// 包装你的应用并提供网络检查功能的主要组件
|
||||||
class YxNetInspector extends StatefulWidget {
|
class YxNetInspector extends StatefulWidget {
|
||||||
|
|
||||||
const YxNetInspector({
|
const YxNetInspector({
|
||||||
required this.child,
|
required this.child, super.key,
|
||||||
super.key,
|
|
||||||
this.config = const YxNetInspectorConfig(),
|
this.config = const YxNetInspectorConfig(),
|
||||||
this.theme = const YxNetInspectorTheme(),
|
this.theme = const YxNetInspectorTheme(),
|
||||||
});
|
});
|
||||||
|
|
@ -42,7 +42,6 @@ class YxNetInspector extends StatefulWidget {
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 你的应用组件
|
/// 你的应用组件
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
|
@ -171,14 +170,12 @@ class YxNetInspectorGlobal {
|
||||||
int? statusCode,
|
int? statusCode,
|
||||||
dynamic responseData,
|
dynamic responseData,
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
Map<String, dynamic>? responseHeaders,
|
|
||||||
}) {
|
}) {
|
||||||
controller.logResponse(
|
controller.logResponse(
|
||||||
id: id,
|
id: id,
|
||||||
statusCode: statusCode,
|
statusCode: statusCode,
|
||||||
responseData: responseData,
|
responseData: responseData,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
responseHeaders: responseHeaders,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
// 核心导出
|
// 核心导出
|
||||||
export 'src/controller/yx_net_inspector_controller.dart';
|
export 'src/controller/yx_net_inspector_controller.dart';
|
||||||
export 'src/interceptors/dio_interceptor.dart';
|
|
||||||
export 'src/models/inspector_config.dart';
|
export 'src/models/inspector_config.dart';
|
||||||
export 'src/models/inspector_theme.dart';
|
export 'src/models/inspector_theme.dart';
|
||||||
export 'src/models/network_log_entry.dart';
|
export 'src/models/network_log_entry.dart';
|
||||||
|
|
|
||||||
48
pubspec.lock
48
pubspec.lock
|
|
@ -41,22 +41,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
dio:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: dio
|
|
||||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "5.9.0"
|
|
||||||
dio_web_adapter:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dio_web_adapter
|
|
||||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.1"
|
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -75,14 +59,6 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
http_parser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_parser
|
|
||||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.2"
|
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -131,14 +107,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
mime:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: mime
|
|
||||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -200,14 +168,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.6"
|
||||||
typed_data:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: typed_data
|
|
||||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -232,14 +192,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "15.0.0"
|
||||||
web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: web
|
|
||||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.1"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0-0 <4.0.0"
|
dart: ">=3.8.0-0 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.18.0-18.0.pre.54"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: yx_net_inspector
|
name: yx_net_inspector
|
||||||
description: A powerful network inspector with floating debug ball for Flutter apps. Monitor HTTP requests, responses, and debug network issues in real-time.
|
description: A powerful network inspector with floating debug ball for Flutter apps. Monitor HTTP requests, responses, and debug network issues in real-time.
|
||||||
version: 1.1.0
|
version: 1.0.4
|
||||||
homepage: https://github.com/your-username/yx_net_inspector
|
homepage: https://github.com/your-username/yx_net_inspector
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -10,7 +10,6 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
dio: ^5.9.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
|
||||||
import 'package:yx_net_inspector/src/interceptors/dio_interceptor.dart';
|
|
||||||
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('YxNetInspectorDioInterceptor Tests', () {
|
|
||||||
late YxNetInspectorController controller;
|
|
||||||
late Dio dio;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
controller = YxNetInspectorController.instance;
|
|
||||||
// Initialize controller with logging enabled (debug mode)
|
|
||||||
controller.initialize(const YxNetInspectorConfig());
|
|
||||||
|
|
||||||
dio = Dio();
|
|
||||||
// Add our inspector interceptor FIRST
|
|
||||||
dio.interceptors.add(YxNetInspectorDioInterceptor());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should log request and response upon success', () async {
|
|
||||||
final initialLogCount = controller.logs.length;
|
|
||||||
|
|
||||||
// Add a mock interceptor to return success response immediately
|
|
||||||
dio.interceptors.add(
|
|
||||||
InterceptorsWrapper(
|
|
||||||
onRequest: (options, handler) {
|
|
||||||
// Allow request to proceed (so our inspector sees it)
|
|
||||||
handler.next(options);
|
|
||||||
},
|
|
||||||
onError: (e, handler) => handler.next(e),
|
|
||||||
onResponse: (e, handler) => handler.next(e),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mock Adapter to avoid real network
|
|
||||||
dio.httpClientAdapter = _MockAdapter((options) {
|
|
||||||
return ResponseBody.fromString(
|
|
||||||
'{"message": "success"}',
|
|
||||||
200,
|
|
||||||
headers: {
|
|
||||||
Headers.contentTypeHeader: [Headers.jsonContentType],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await dio.get<dynamic>('https://example.com/api/test');
|
|
||||||
|
|
||||||
expect(controller.logs.length, greaterThan(initialLogCount));
|
|
||||||
final latestLog = controller.logs.first;
|
|
||||||
|
|
||||||
expect(latestLog.url, 'https://example.com/api/test');
|
|
||||||
expect(latestLog.method, 'GET');
|
|
||||||
expect(latestLog.statusCode, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should log error upon failure', () async {
|
|
||||||
final initialLogCount = controller.logs.length;
|
|
||||||
|
|
||||||
// Mock Adapter to throw error
|
|
||||||
dio.httpClientAdapter = _MockAdapter((options) {
|
|
||||||
throw DioException(
|
|
||||||
requestOptions: options,
|
|
||||||
error: 'Network Error',
|
|
||||||
type: DioExceptionType.connectionError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dio.post<dynamic>('https://example.com/api/fail',
|
|
||||||
data: {'foo': 'bar'},);
|
|
||||||
} on Object catch (_) {
|
|
||||||
// Expected
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(controller.logs.length, greaterThan(initialLogCount));
|
|
||||||
final latestLog = controller.logs.first;
|
|
||||||
|
|
||||||
expect(latestLog.url, 'https://example.com/api/fail');
|
|
||||||
expect(latestLog.method, 'POST');
|
|
||||||
expect(latestLog.isSuccess, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MockAdapter implements HttpClientAdapter {
|
|
||||||
_MockAdapter(this._mockResponse);
|
|
||||||
final ResponseBody Function(RequestOptions options) _mockResponse;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ResponseBody> fetch(
|
|
||||||
RequestOptions options,
|
|
||||||
Stream<List<int>>? requestStream,
|
|
||||||
Future<void>? cancelFuture,
|
|
||||||
) async {
|
|
||||||
return _mockResponse(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void close({bool force = false}) {}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart
|
||||||
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/panel/inspector_log_list.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('YxInspectorPanel Widget Tests', () {
|
group('YxInspectorPanel Widget Tests', () {
|
||||||
|
|
@ -35,25 +34,20 @@ void main() {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 验证面板基本组件 (标题在非全屏模式已移除以节省空间)
|
// 验证面板基本组件
|
||||||
// Title removed from non-fullscreen mode to save space
|
expect(find.text('网络检查器'), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.network_check), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
expect(find.byIcon(Icons.close), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('应该显示统计信息', (WidgetTester tester) async {
|
testWidgets('应该显示统计信息', (WidgetTester tester) async {
|
||||||
// 添加一些测试数据
|
// 添加一些测试数据
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-1',
|
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||||
method: 'GET',
|
|
||||||
url: 'https://example.com',
|
|
||||||
);
|
|
||||||
controller.logResponse(id: 'test-1', statusCode: 200);
|
controller.logResponse(id: 'test-1', statusCode: 200);
|
||||||
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-2',
|
id: 'test-2', method: 'POST', url: 'https://example.com',);
|
||||||
method: 'POST',
|
|
||||||
url: 'https://example.com',
|
|
||||||
);
|
|
||||||
controller.logError(id: 'test-2', error: '网络错误');
|
controller.logError(id: 'test-2', error: '网络错误');
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
|
@ -71,15 +65,12 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 验证统计信息
|
// 验证统计信息
|
||||||
// 验证统计信息
|
expect(find.text('总计'), findsOneWidget);
|
||||||
expect(find.text('总请求: 2'), findsOneWidget);
|
expect(find.text('成功'), findsOneWidget);
|
||||||
expect(find.text('错误: 1'), findsOneWidget);
|
expect(find.text('失败'), findsOneWidget);
|
||||||
|
expect(find.text('成功率'), findsOneWidget);
|
||||||
// Removed old fields
|
expect(find.text('2'), findsOneWidget); // 总请求数
|
||||||
// expect(find.text('总计'), findsOneWidget);
|
expect(find.text('1'), findsWidgets); // 成功和失败各1个
|
||||||
// expect(find.text('成功'), findsOneWidget);
|
|
||||||
// expect(find.text('失败'), findsOneWidget);
|
|
||||||
// expect(find.text('成功率'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('应该显示搜索栏', (WidgetTester tester) async {
|
testWidgets('应该显示搜索栏', (WidgetTester tester) async {
|
||||||
|
|
@ -98,24 +89,16 @@ void main() {
|
||||||
// 验证搜索相关组件
|
// 验证搜索相关组件
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
expect(find.text('搜索请求...'), findsOneWidget);
|
expect(find.text('搜索请求...'), findsOneWidget);
|
||||||
expect(find.text('搜索请求...'), findsOneWidget);
|
expect(find.byType(FilterChip), findsOneWidget);
|
||||||
// FilterChip was removed/moved
|
expect(find.text('仅显示错误'), findsOneWidget);
|
||||||
// expect(find.byType(FilterChip), findsOneWidget);
|
|
||||||
// expect(find.text('仅显示错误'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('搜索功能应该正常工作', (WidgetTester tester) async {
|
testWidgets('搜索功能应该正常工作', (WidgetTester tester) async {
|
||||||
// 添加测试数据
|
// 添加测试数据
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'user-1',
|
id: 'user-1', method: 'GET', url: 'https://api.example.com/users',);
|
||||||
method: 'GET',
|
|
||||||
url: 'https://api.example.com/users',
|
|
||||||
);
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'post-1',
|
id: 'post-1', method: 'POST', url: 'https://api.example.com/posts',);
|
||||||
method: 'POST',
|
|
||||||
url: 'https://api.example.com/posts',
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -142,17 +125,11 @@ void main() {
|
||||||
testWidgets('错误过滤器应该正常工作', (WidgetTester tester) async {
|
testWidgets('错误过滤器应该正常工作', (WidgetTester tester) async {
|
||||||
// 添加成功和失败的请求
|
// 添加成功和失败的请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'success',
|
id: 'success', method: 'GET', url: 'https://example.com',);
|
||||||
method: 'GET',
|
|
||||||
url: 'https://example.com',
|
|
||||||
);
|
|
||||||
controller.logResponse(id: 'success', statusCode: 200);
|
controller.logResponse(id: 'success', statusCode: 200);
|
||||||
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'error',
|
id: 'error', method: 'POST', url: 'https://example.com',);
|
||||||
method: 'POST',
|
|
||||||
url: 'https://example.com',
|
|
||||||
);
|
|
||||||
controller.logError(id: 'error', error: '网络错误');
|
controller.logError(id: 'error', error: '网络错误');
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
|
@ -169,21 +146,19 @@ void main() {
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 点击错误过滤器 (It's a TextButton now)
|
// 点击错误过滤器
|
||||||
await tester.tap(find.text('仅错误'));
|
await tester.tap(find.byType(FilterChip));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 验证过滤器被激活 - Text changes to "显示全部"
|
// 验证过滤器被激活
|
||||||
expect(find.text('显示全部'), findsOneWidget);
|
final filterChip = tester.widget<FilterChip>(find.byType(FilterChip));
|
||||||
|
expect(filterChip.selected, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('清空日志按钮应该正常工作', (WidgetTester tester) async {
|
testWidgets('清空日志按钮应该正常工作', (WidgetTester tester) async {
|
||||||
// 添加一些日志
|
// 添加一些日志
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test',
|
id: 'test', method: 'GET', url: 'https://example.com',);
|
||||||
method: 'GET',
|
|
||||||
url: 'https://example.com',
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -297,7 +272,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 验证日志条目显示
|
// 验证日志条目显示
|
||||||
expect(find.byType(InspectorLogItem), findsWidgets);
|
expect(find.byType(Card), findsWidgets);
|
||||||
expect(find.text('GET'), findsOneWidget);
|
expect(find.text('GET'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -325,7 +300,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 点击日志条目
|
// 点击日志条目
|
||||||
await tester.tap(find.byType(InspectorLogItem).first);
|
await tester.tap(find.byType(InkWell).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// 验证导航到详情页(这里主要验证没有报错)
|
// 验证导航到详情页(这里主要验证没有报错)
|
||||||
|
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
|
||||||
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
|
||||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
|
||||||
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
|
||||||
import 'package:yx_net_inspector/src/widgets/panel/password_dialog.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('Password Protection Tests', () {
|
|
||||||
late YxNetInspectorController controller;
|
|
||||||
const theme = YxNetInspectorTheme();
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
controller = YxNetInspectorController.instance;
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Panel should be unlocked by default (no password)',
|
|
||||||
(WidgetTester tester) async {
|
|
||||||
controller.initialize(const YxNetInspectorConfig());
|
|
||||||
|
|
||||||
expect(controller.isUnlocked, isTrue);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: YxInspectorPanel(
|
|
||||||
theme: theme,
|
|
||||||
controller: controller,
|
|
||||||
onClose: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(find.byType(PasswordDialog), findsNothing);
|
|
||||||
expect(find.byIcon(Icons.close), findsOneWidget); // Normal panel content
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Panel should be locked if password is set',
|
|
||||||
(WidgetTester tester) async {
|
|
||||||
controller.initialize(const YxNetInspectorConfig(
|
|
||||||
password: '123',
|
|
||||||
),);
|
|
||||||
|
|
||||||
expect(controller.isUnlocked, isFalse);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: YxInspectorPanel(
|
|
||||||
theme: theme,
|
|
||||||
controller: controller,
|
|
||||||
onClose: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(find.byType(PasswordDialog), findsOneWidget);
|
|
||||||
expect(find.text('安全访问'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.byIcon(Icons.close), findsNothing,); // Should hide panel content
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Entering correct password should unlock panel',
|
|
||||||
(WidgetTester tester) async {
|
|
||||||
controller.initialize(const YxNetInspectorConfig(
|
|
||||||
password: '123',
|
|
||||||
),);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: YxInspectorPanel(
|
|
||||||
theme: theme,
|
|
||||||
controller: controller,
|
|
||||||
onClose: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enter correct password
|
|
||||||
await tester.enterText(find.byType(TextField), '123');
|
|
||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
|
||||||
await tester.pump(); // Start async unlock
|
|
||||||
await tester.pump(); // Rebuild with unlocked state
|
|
||||||
|
|
||||||
expect(controller.isUnlocked, isTrue);
|
|
||||||
expect(find.byType(PasswordDialog), findsNothing);
|
|
||||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Entering wrong password should show error',
|
|
||||||
(WidgetTester tester) async {
|
|
||||||
controller.initialize(const YxNetInspectorConfig(
|
|
||||||
password: '123',
|
|
||||||
),);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: YxInspectorPanel(
|
|
||||||
theme: theme,
|
|
||||||
controller: controller,
|
|
||||||
onClose: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enter wrong password
|
|
||||||
await tester.enterText(find.byType(TextField), '000');
|
|
||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
|
||||||
await tester.pump(); // Start async unlock
|
|
||||||
await tester.pump(); // Rebuild
|
|
||||||
|
|
||||||
expect(controller.isUnlocked, isFalse);
|
|
||||||
expect(find.byType(PasswordDialog), findsOneWidget);
|
|
||||||
expect(find.text('密码错误,请重试'), findsOneWidget);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue