Merge branch 'release/1.1.0'
This commit is contained in:
commit
9d6aaf64b4
|
|
@ -107,3 +107,6 @@ temp/
|
||||||
# OS specific
|
# OS specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# macOS metadata files
|
||||||
|
._*
|
||||||
|
|
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,6 +5,19 @@
|
||||||
格式基于 [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
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
include: package:very_good_analysis/analysis_options.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
public_member_api_docs: false
|
||||||
|
sort_pub_dependencies: false
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
errors:
|
||||||
|
cascade_invocations: ignore
|
||||||
|
inference_failure_on_collection_literal: ignore
|
||||||
|
lines_longer_than_80_chars: ignore # Too strict for existing code
|
||||||
|
|
||||||
|
|
@ -14,19 +14,8 @@ class MyDebugApp extends StatelessWidget {
|
||||||
title: 'YX 网络检查器调试',
|
title: 'YX 网络检查器调试',
|
||||||
home: YxNetInspector(
|
home: YxNetInspector(
|
||||||
config: const YxNetInspectorConfig(
|
config: const YxNetInspectorConfig(
|
||||||
showFloatingBall: true,
|
|
||||||
ballSize: 60,
|
|
||||||
ballColor: Colors.blue,
|
ballColor: Colors.blue,
|
||||||
),
|
),
|
||||||
theme: const YxNetInspectorTheme(
|
|
||||||
primaryColor: Colors.blue,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
textColor: Colors.black87,
|
|
||||||
successColor: Colors.green,
|
|
||||||
errorColor: Colors.red,
|
|
||||||
warningColor: Colors.orange,
|
|
||||||
cardColor: Colors.white,
|
|
||||||
),
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('调试测试'),
|
title: const Text('调试测试'),
|
||||||
|
|
@ -55,7 +44,7 @@ class MyDebugApp extends StatelessWidget {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
responseData: const {
|
responseData: const {
|
||||||
'title': 'Test Post',
|
'title': 'Test Post',
|
||||||
'body': 'Test content'
|
'body': 'Test content',
|
||||||
},
|
},
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,22 @@ 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:
|
||||||
|
|
@ -87,26 +103,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.9"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -139,6 +155,14 @@ 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:
|
||||||
|
|
@ -196,10 +220,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.6"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -212,10 +236,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -238,7 +262,7 @@ packages:
|
||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "1.0.2"
|
version: "1.0.4"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.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,24 +1,22 @@
|
||||||
|
// ignore_for_file: prefer_constructors_over_static_methods // Singleton pattern requires static access
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../models/network_log_entry.dart';
|
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||||
import '../models/inspector_config.dart';
|
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||||
|
|
||||||
/// 网络日志检查器控制器
|
/// 网络日志检查器控制器
|
||||||
/// 管理网络日志和检查器状态
|
/// 管理网络日志和检查器状态
|
||||||
class YxNetInspectorController extends ChangeNotifier {
|
class YxNetInspectorController extends ChangeNotifier {
|
||||||
|
YxNetInspectorController._internal();
|
||||||
static YxNetInspectorController? _instance;
|
static YxNetInspectorController? _instance;
|
||||||
static YxNetInspectorController get instance {
|
static YxNetInspectorController get instance {
|
||||||
return _instance ??= YxNetInspectorController._internal();
|
return _instance ??= YxNetInspectorController._internal();
|
||||||
}
|
}
|
||||||
|
|
||||||
YxNetInspectorController._internal();
|
|
||||||
|
|
||||||
/// 配置信息
|
/// 配置信息
|
||||||
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;
|
||||||
|
|
@ -33,12 +31,72 @@ class YxNetInspectorController extends ChangeNotifier {
|
||||||
|
|
||||||
/// 悬浮球显示状态
|
/// 悬浮球显示状态
|
||||||
bool _showFloatingBall = true;
|
bool _showFloatingBall = true;
|
||||||
bool get showFloatingBall => _showFloatingBall && _config.isEnabled;
|
|
||||||
|
|
||||||
/// 初始化控制器配置
|
/// 面板显示状态
|
||||||
|
bool _isPanelVisible = false;
|
||||||
|
bool _isUnlocked = true; // 默认为已解锁
|
||||||
|
|
||||||
|
/// 初始化控制器
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 显示面板
|
||||||
|
void showPanel() {
|
||||||
|
_isPanelVisible = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 隐藏面板
|
||||||
|
void hidePanel() {
|
||||||
|
_isPanelVisible = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换面板显示状态
|
||||||
|
void togglePanel() {
|
||||||
|
_isPanelVisible = !_isPanelVisible;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 记录网络请求
|
/// 记录网络请求
|
||||||
|
|
@ -61,7 +119,6 @@ class YxNetInspectorController extends ChangeNotifier {
|
||||||
queryParameters: queryParameters,
|
queryParameters: queryParameters,
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
isSuccess: false, // 响应到达时会更新
|
isSuccess: false, // 响应到达时会更新
|
||||||
status: NetworkRequestStatus.pending, // 初始状态为进行中
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_logs.insert(0, entry);
|
_logs.insert(0, entry);
|
||||||
|
|
@ -76,6 +133,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -89,6 +147,7 @@ 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
|
||||||
|
|
@ -214,6 +273,43 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,22 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// 网络检查器配置
|
/// 网络检查器配置
|
||||||
|
@immutable
|
||||||
class YxNetInspectorConfig {
|
class YxNetInspectorConfig {
|
||||||
|
const YxNetInspectorConfig({
|
||||||
|
this.showFloatingBall = true,
|
||||||
|
this.ballSize = 60.0,
|
||||||
|
this.ballColor,
|
||||||
|
this.showInDebugMode = true,
|
||||||
|
this.showInReleaseMode = false,
|
||||||
|
this.maxLogs = 1000,
|
||||||
|
this.initialPosition,
|
||||||
|
this.draggable = true,
|
||||||
|
this.showBadge = true,
|
||||||
|
this.autoHide = false,
|
||||||
|
this.password,
|
||||||
|
});
|
||||||
|
|
||||||
/// 是否显示悬浮球
|
/// 是否显示悬浮球
|
||||||
final bool showFloatingBall;
|
final bool showFloatingBall;
|
||||||
|
|
||||||
|
|
@ -33,18 +48,8 @@ class YxNetInspectorConfig {
|
||||||
/// 是否自动隐藏悬浮球
|
/// 是否自动隐藏悬浮球
|
||||||
final bool autoHide;
|
final bool autoHide;
|
||||||
|
|
||||||
const YxNetInspectorConfig({
|
/// 访问检查器面板所需的密码(如果为空则无需密码)
|
||||||
this.showFloatingBall = true,
|
final String? password;
|
||||||
this.ballSize = 60.0,
|
|
||||||
this.ballColor,
|
|
||||||
this.showInDebugMode = true,
|
|
||||||
this.showInReleaseMode = false,
|
|
||||||
this.maxLogs = 1000,
|
|
||||||
this.initialPosition,
|
|
||||||
this.draggable = true,
|
|
||||||
this.showBadge = true,
|
|
||||||
this.autoHide = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 根据当前模式判断检查器是否应该启用
|
/// 根据当前模式判断检查器是否应该启用
|
||||||
bool get isEnabled {
|
bool get isEnabled {
|
||||||
|
|
@ -67,6 +72,7 @@ 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,
|
||||||
|
|
@ -79,6 +85,7 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +101,8 @@ class YxNetInspectorConfig {
|
||||||
'initialPosition: $initialPosition, '
|
'initialPosition: $initialPosition, '
|
||||||
'draggable: $draggable, '
|
'draggable: $draggable, '
|
||||||
'showBadge: $showBadge, '
|
'showBadge: $showBadge, '
|
||||||
'autoHide: $autoHide'
|
'autoHide: $autoHide, '
|
||||||
|
'password: ${password != null ? "***" : "null"}'
|
||||||
')';
|
')';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,7 +119,8 @@ 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
|
||||||
|
|
@ -127,6 +136,7 @@ class YxNetInspectorConfig {
|
||||||
draggable,
|
draggable,
|
||||||
showBadge,
|
showBadge,
|
||||||
autoHide,
|
autoHide,
|
||||||
|
password,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,40 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// 网络检查器主题配置
|
/// 网络检查器主题配置
|
||||||
|
@immutable
|
||||||
class YxNetInspectorTheme {
|
class YxNetInspectorTheme {
|
||||||
|
const YxNetInspectorTheme({
|
||||||
|
this.primaryColor = Colors.blue,
|
||||||
|
this.backgroundColor = Colors.white,
|
||||||
|
this.textColor = Colors.black87,
|
||||||
|
this.secondaryTextColor = Colors.grey,
|
||||||
|
this.errorColor = Colors.red,
|
||||||
|
this.successColor = Colors.green,
|
||||||
|
this.warningColor = Colors.orange,
|
||||||
|
this.borderColor = const Color(0xFFE0E0E0),
|
||||||
|
this.cardColor = Colors.white,
|
||||||
|
this.floatingBallColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 创建暗色主题
|
||||||
|
factory YxNetInspectorTheme.dark() {
|
||||||
|
return const YxNetInspectorTheme(
|
||||||
|
primaryColor: Colors.blueAccent,
|
||||||
|
backgroundColor: Color(0xFF121212),
|
||||||
|
textColor: Colors.white,
|
||||||
|
errorColor: Colors.redAccent,
|
||||||
|
successColor: Colors.greenAccent,
|
||||||
|
warningColor: Colors.orangeAccent,
|
||||||
|
borderColor: Color(0xFF333333),
|
||||||
|
cardColor: Color(0xFF1E1E1E),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建亮色主题(默认)
|
||||||
|
factory YxNetInspectorTheme.light() {
|
||||||
|
return const YxNetInspectorTheme();
|
||||||
|
}
|
||||||
|
|
||||||
/// 检查器 UI 的主色调
|
/// 检查器 UI 的主色调
|
||||||
final Color primaryColor;
|
final Color primaryColor;
|
||||||
|
|
||||||
|
|
@ -32,39 +65,6 @@ class YxNetInspectorTheme {
|
||||||
/// 悬浮球颜色(如果设置则覆盖配置)
|
/// 悬浮球颜色(如果设置则覆盖配置)
|
||||||
final Color? floatingBallColor;
|
final Color? floatingBallColor;
|
||||||
|
|
||||||
const YxNetInspectorTheme({
|
|
||||||
this.primaryColor = Colors.blue,
|
|
||||||
this.backgroundColor = Colors.white,
|
|
||||||
this.textColor = Colors.black87,
|
|
||||||
this.secondaryTextColor = Colors.grey,
|
|
||||||
this.errorColor = Colors.red,
|
|
||||||
this.successColor = Colors.green,
|
|
||||||
this.warningColor = Colors.orange,
|
|
||||||
this.borderColor = const Color(0xFFE0E0E0),
|
|
||||||
this.cardColor = Colors.white,
|
|
||||||
this.floatingBallColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 创建暗色主题
|
|
||||||
factory YxNetInspectorTheme.dark() {
|
|
||||||
return const YxNetInspectorTheme(
|
|
||||||
primaryColor: Colors.blueAccent,
|
|
||||||
backgroundColor: Color(0xFF121212),
|
|
||||||
textColor: Colors.white,
|
|
||||||
secondaryTextColor: Colors.grey,
|
|
||||||
errorColor: Colors.redAccent,
|
|
||||||
successColor: Colors.greenAccent,
|
|
||||||
warningColor: Colors.orangeAccent,
|
|
||||||
borderColor: Color(0xFF333333),
|
|
||||||
cardColor: Color(0xFF1E1E1E),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建亮色主题(默认)
|
|
||||||
factory YxNetInspectorTheme.light() {
|
|
||||||
return const YxNetInspectorTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建一个带有更新值的副本
|
/// 创建一个带有更新值的副本
|
||||||
YxNetInspectorTheme copyWith({
|
YxNetInspectorTheme copyWith({
|
||||||
Color? primaryColor,
|
Color? primaryColor,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// 网络请求状态枚举
|
/// 网络请求状态枚举
|
||||||
|
|
@ -13,11 +15,29 @@ enum NetworkRequestStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 网络请求日志条目
|
/// 网络请求日志条目
|
||||||
|
@immutable
|
||||||
class NetworkLogEntry {
|
class NetworkLogEntry {
|
||||||
|
const NetworkLogEntry({
|
||||||
|
required this.id,
|
||||||
|
required this.method,
|
||||||
|
required this.url,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.isSuccess,
|
||||||
|
this.headers,
|
||||||
|
this.responseHeaders,
|
||||||
|
this.requestData,
|
||||||
|
this.queryParameters,
|
||||||
|
this.statusCode,
|
||||||
|
this.responseData,
|
||||||
|
this.errorMessage,
|
||||||
|
this.duration,
|
||||||
|
this.status = NetworkRequestStatus.pending,
|
||||||
|
});
|
||||||
final String id;
|
final String id;
|
||||||
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;
|
||||||
|
|
@ -28,22 +48,6 @@ class NetworkLogEntry {
|
||||||
final bool isSuccess;
|
final bool isSuccess;
|
||||||
final NetworkRequestStatus status;
|
final NetworkRequestStatus status;
|
||||||
|
|
||||||
NetworkLogEntry({
|
|
||||||
required this.id,
|
|
||||||
required this.method,
|
|
||||||
required this.url,
|
|
||||||
this.headers,
|
|
||||||
this.requestData,
|
|
||||||
this.queryParameters,
|
|
||||||
this.statusCode,
|
|
||||||
this.responseData,
|
|
||||||
this.errorMessage,
|
|
||||||
required this.timestamp,
|
|
||||||
this.duration,
|
|
||||||
required this.isSuccess,
|
|
||||||
this.status = NetworkRequestStatus.pending,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 根据请求状态获取状态颜色
|
/// 根据请求状态获取状态颜色
|
||||||
Color get statusColor {
|
Color get statusColor {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
@ -70,7 +74,7 @@ class NetworkLogEntry {
|
||||||
|
|
||||||
/// 获取预估请求大小
|
/// 获取预估请求大小
|
||||||
int get requestSize {
|
int get requestSize {
|
||||||
int size = 0;
|
var size = 0;
|
||||||
if (requestData != null) {
|
if (requestData != null) {
|
||||||
size += requestData.toString().length;
|
size += requestData.toString().length;
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +106,7 @@ class NetworkLogEntry {
|
||||||
String get formattedDuration {
|
String get formattedDuration {
|
||||||
if (duration == null) return '未知';
|
if (duration == null) return '未知';
|
||||||
final ms = duration!.inMilliseconds;
|
final ms = duration!.inMilliseconds;
|
||||||
if (ms < 1000) return '${ms} ms';
|
if (ms < 1000) return '$ms ms';
|
||||||
return '${(ms / 1000).toStringAsFixed(1)} s';
|
return '${(ms / 1000).toStringAsFixed(1)} s';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,7 +115,7 @@ class NetworkLogEntry {
|
||||||
try {
|
try {
|
||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
return '${uri.path}${uri.query.isNotEmpty ? '?${uri.query}' : ''}';
|
return '${uri.path}${uri.query.isNotEmpty ? '?${uri.query}' : ''}';
|
||||||
} catch (e) {
|
} on Object catch (_) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,18 +123,45 @@ class NetworkLogEntry {
|
||||||
String get hostUrl {
|
String get hostUrl {
|
||||||
try {
|
try {
|
||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
return '${uri.host}';
|
return uri.host;
|
||||||
} catch (e) {
|
} on Object catch (_) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 生成 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,
|
||||||
|
|
@ -146,6 +177,7 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../controller/yx_net_inspector_controller.dart';
|
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
||||||
import '../models/inspector_config.dart';
|
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||||
import '../models/inspector_theme.dart';
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
import 'inspector_panel.dart';
|
|
||||||
|
|
||||||
/// 悬浮调试球组件
|
/// 悬浮调试球组件
|
||||||
class YxFloatingBall extends StatefulWidget {
|
class YxFloatingBall extends StatefulWidget {
|
||||||
|
const YxFloatingBall({
|
||||||
|
required this.config, required this.theme, required this.controller, super.key,
|
||||||
|
});
|
||||||
final YxNetInspectorConfig config;
|
final YxNetInspectorConfig config;
|
||||||
final YxNetInspectorTheme theme;
|
final YxNetInspectorTheme theme;
|
||||||
final YxNetInspectorController controller;
|
final YxNetInspectorController controller;
|
||||||
|
|
||||||
const YxFloatingBall({
|
|
||||||
super.key,
|
|
||||||
required this.config,
|
|
||||||
required this.theme,
|
|
||||||
required this.controller,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<YxFloatingBall> createState() => _YxFloatingBallState();
|
State<YxFloatingBall> createState() => _YxFloatingBallState();
|
||||||
}
|
}
|
||||||
|
|
@ -28,8 +23,6 @@ class _YxFloatingBallState extends State<YxFloatingBall>
|
||||||
late Animation<double> _opacityAnimation;
|
late Animation<double> _opacityAnimation;
|
||||||
|
|
||||||
Offset _position = const Offset(20, 200);
|
Offset _position = const Offset(20, 200);
|
||||||
bool _isExpanded = false;
|
|
||||||
OverlayEntry? _currentOverlayEntry;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -46,167 +39,23 @@ class _YxFloatingBallState extends State<YxFloatingBall>
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
|
_scaleAnimation = Tween<double>(begin: 1, end: 1.2).animate(
|
||||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||||
);
|
);
|
||||||
|
|
||||||
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(
|
_opacityAnimation = Tween<double>(begin: 1, end: 0.8).animate(
|
||||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// 清理 overlay
|
|
||||||
if (_currentOverlayEntry != null) {
|
|
||||||
_currentOverlayEntry!.remove();
|
|
||||||
_currentOverlayEntry = null;
|
|
||||||
}
|
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTap() {
|
void _onTap() {
|
||||||
if (_isExpanded) {
|
widget.controller.togglePanel();
|
||||||
_hideInspectorPanel();
|
|
||||||
} else {
|
|
||||||
_showInspectorPanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showInspectorPanel() {
|
|
||||||
setState(() {
|
|
||||||
_isExpanded = true;
|
|
||||||
});
|
|
||||||
_animationController.forward();
|
|
||||||
|
|
||||||
// 优先使用 Overlay,如果失败则使用 Navigator
|
|
||||||
_showInspectorOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showInspectorOverlay() {
|
|
||||||
// 延迟到下一帧执行,确保Widget树已经完全构建
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_tryShowOverlay();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _tryShowOverlay() {
|
|
||||||
// 尝试找到 Overlay
|
|
||||||
OverlayState? overlay;
|
|
||||||
try {
|
|
||||||
overlay = Overlay.of(context, rootOverlay: true);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('YxNetInspector: 根 Overlay 查找失败: $e');
|
|
||||||
// 如果 Overlay.of 失败,尝试手动查找
|
|
||||||
overlay = _findOverlayInContext(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlay == null) {
|
|
||||||
// 如果仍然找不到 Overlay,使用备选方案
|
|
||||||
_showInspectorDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final overlayEntry = OverlayEntry(
|
|
||||||
builder: (context) => Material(
|
|
||||||
color: Colors.black54,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
// 背景遮罩
|
|
||||||
GestureDetector(
|
|
||||||
onTap: _hideInspectorPanel,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.transparent,
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 检查器面板
|
|
||||||
Center(
|
|
||||||
child: YxInspectorPanel(
|
|
||||||
theme: widget.theme,
|
|
||||||
controller: widget.controller,
|
|
||||||
onClose: _hideInspectorPanel,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
overlay.insert(overlayEntry);
|
|
||||||
_currentOverlayEntry = overlayEntry;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('YxNetInspector: 插入 OverlayEntry 失败: $e');
|
|
||||||
// 重置状态
|
|
||||||
setState(() {
|
|
||||||
_isExpanded = false;
|
|
||||||
});
|
|
||||||
_animationController.reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayState? _findOverlayInContext(BuildContext context) {
|
|
||||||
OverlayState? overlayState;
|
|
||||||
context.visitAncestorElements((element) {
|
|
||||||
if (element.widget is Overlay) {
|
|
||||||
overlayState = Overlay.of(element);
|
|
||||||
return false; // 停止遍历
|
|
||||||
}
|
|
||||||
return true; // 继续向上查找
|
|
||||||
});
|
|
||||||
return overlayState;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showInspectorDialog() {
|
|
||||||
// 作为最后的备选方案,创建一个自定义的全屏Overlay
|
|
||||||
// 这里我们不依赖Navigator,而是手动管理显示状态
|
|
||||||
_createCustomOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _createCustomOverlay() {
|
|
||||||
// 如果所有 Overlay 方法都失败,我们显示一个简单的调试信息
|
|
||||||
// 并重置状态,避免悬浮球卡在展开状态
|
|
||||||
|
|
||||||
// 显示一个简单的 SnackBar 或 print 提示
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) {
|
|
||||||
try {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('网络检查器暂时无法显示,请检查应用结构'),
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// 如果 ScaffoldMessenger 也不可用,只打印日志
|
|
||||||
debugPrint('YxNetInspector: 无法显示检查器面板 - 请确保在 MaterialApp 内使用');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置状态
|
|
||||||
setState(() {
|
|
||||||
_isExpanded = false;
|
|
||||||
});
|
|
||||||
_animationController.reverse();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _hideInspectorPanel() {
|
|
||||||
setState(() {
|
|
||||||
_isExpanded = false;
|
|
||||||
});
|
|
||||||
_animationController.reverse();
|
|
||||||
|
|
||||||
// 如果有 overlay,移除它
|
|
||||||
if (_currentOverlayEntry != null) {
|
|
||||||
_currentOverlayEntry!.remove();
|
|
||||||
_currentOverlayEntry = null;
|
|
||||||
}
|
|
||||||
// 注意:如果使用了 Navigator 的 PageRouteBuilder,
|
|
||||||
// 对话框关闭会由 onClose 回调中的 Navigator.pop() 处理
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPanStart(DragStartDetails details) {
|
void _onPanStart(DragStartDetails details) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,50 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// 悬浮调试球配置
|
/// 悬浮调试球配置
|
||||||
|
@immutable
|
||||||
class YxFloatingBallConfig {
|
class YxFloatingBallConfig {
|
||||||
|
const YxFloatingBallConfig({
|
||||||
|
this.size = 60.0,
|
||||||
|
this.position = const Offset(20, 200),
|
||||||
|
this.draggable = true,
|
||||||
|
this.showBadge = true,
|
||||||
|
this.autoHide = false,
|
||||||
|
this.autoHideDuration = const Duration(seconds: 5),
|
||||||
|
this.color,
|
||||||
|
this.opacity = 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 创建小型悬浮球配置
|
||||||
|
factory YxFloatingBallConfig.small({Offset? position, Color? color}) {
|
||||||
|
return YxFloatingBallConfig(
|
||||||
|
size: 40,
|
||||||
|
position: position ?? const Offset(20, 200),
|
||||||
|
color: color,
|
||||||
|
showBadge: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建大型悬浮球配置
|
||||||
|
factory YxFloatingBallConfig.large({Offset? position, Color? color}) {
|
||||||
|
return YxFloatingBallConfig(
|
||||||
|
size: 80,
|
||||||
|
position: position ?? const Offset(20, 200),
|
||||||
|
color: color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建最小化悬浮球配置
|
||||||
|
factory YxFloatingBallConfig.minimal({Offset? position, Color? color}) {
|
||||||
|
return YxFloatingBallConfig(
|
||||||
|
size: 50,
|
||||||
|
position: position ?? const Offset(20, 200),
|
||||||
|
color: color,
|
||||||
|
showBadge: false,
|
||||||
|
draggable: false,
|
||||||
|
autoHide: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 悬浮球大小
|
/// 悬浮球大小
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
|
|
@ -26,49 +69,6 @@ class YxFloatingBallConfig {
|
||||||
/// 悬浮球透明度
|
/// 悬浮球透明度
|
||||||
final double opacity;
|
final double opacity;
|
||||||
|
|
||||||
const YxFloatingBallConfig({
|
|
||||||
this.size = 60.0,
|
|
||||||
this.position = const Offset(20, 200),
|
|
||||||
this.draggable = true,
|
|
||||||
this.showBadge = true,
|
|
||||||
this.autoHide = false,
|
|
||||||
this.autoHideDuration = const Duration(seconds: 5),
|
|
||||||
this.color,
|
|
||||||
this.opacity = 1.0,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 创建小型悬浮球配置
|
|
||||||
factory YxFloatingBallConfig.small({Offset? position, Color? color}) {
|
|
||||||
return YxFloatingBallConfig(
|
|
||||||
size: 40.0,
|
|
||||||
position: position ?? const Offset(20, 200),
|
|
||||||
color: color,
|
|
||||||
showBadge: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建大型悬浮球配置
|
|
||||||
factory YxFloatingBallConfig.large({Offset? position, Color? color}) {
|
|
||||||
return YxFloatingBallConfig(
|
|
||||||
size: 80.0,
|
|
||||||
position: position ?? const Offset(20, 200),
|
|
||||||
color: color,
|
|
||||||
showBadge: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建最小化悬浮球配置
|
|
||||||
factory YxFloatingBallConfig.minimal({Offset? position, Color? color}) {
|
|
||||||
return YxFloatingBallConfig(
|
|
||||||
size: 50.0,
|
|
||||||
position: position ?? const Offset(20, 200),
|
|
||||||
color: color,
|
|
||||||
showBadge: false,
|
|
||||||
draggable: false,
|
|
||||||
autoHide: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建一个带有更新值的副本
|
/// 创建一个带有更新值的副本
|
||||||
YxFloatingBallConfig copyWith({
|
YxFloatingBallConfig copyWith({
|
||||||
double? size,
|
double? size,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../controller/yx_net_inspector_controller.dart';
|
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
||||||
import '../models/inspector_theme.dart';
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
import '../models/network_log_entry.dart';
|
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||||
import 'log_detail_page.dart';
|
import 'package:yx_net_inspector/src/widgets/log_detail_page.dart';
|
||||||
|
import 'package:yx_net_inspector/src/widgets/panel/inspector_detail_header.dart';
|
||||||
|
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_search_bar.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 {
|
||||||
final YxNetInspectorTheme theme;
|
|
||||||
final YxNetInspectorController controller;
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
const YxInspectorPanel({
|
const YxInspectorPanel({
|
||||||
super.key,
|
|
||||||
required this.theme,
|
required this.theme,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
|
super.key,
|
||||||
});
|
});
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
final YxNetInspectorController controller;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<YxInspectorPanel> createState() => _YxInspectorPanelState();
|
State<YxInspectorPanel> createState() => _YxInspectorPanelState();
|
||||||
|
|
@ -52,8 +57,14 @@ class _YxInspectorPanelState extends State<YxInspectorPanel> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _toggleFullScreen() {
|
||||||
|
setState(() {
|
||||||
|
_isFullScreen = !_isFullScreen;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
List<NetworkLogEntry> get _filteredLogs {
|
List<NetworkLogEntry> get _filteredLogs {
|
||||||
List<NetworkLogEntry> logs = widget.controller.logs;
|
var logs = widget.controller.logs;
|
||||||
|
|
||||||
// 搜索过滤
|
// 搜索过滤
|
||||||
if (_searchKeyword.isNotEmpty) {
|
if (_searchKeyword.isNotEmpty) {
|
||||||
|
|
@ -70,497 +81,8 @@ class _YxInspectorPanelState extends State<YxInspectorPanel> {
|
||||||
return logs;
|
return logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: widget.controller,
|
|
||||||
builder: (context, child) {
|
|
||||||
Widget content;
|
|
||||||
|
|
||||||
if (_showDetailPage && _selectedLog != null) {
|
|
||||||
// 显示详情页面
|
|
||||||
content = Column(
|
|
||||||
children: [
|
|
||||||
_buildDetailHeader(),
|
|
||||||
Expanded(
|
|
||||||
child: YxLogDetailPage(
|
|
||||||
log: _selectedLog!,
|
|
||||||
theme: widget.theme,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 显示主列表页面
|
|
||||||
content = Column(
|
|
||||||
children: [
|
|
||||||
_buildHeader(),
|
|
||||||
_buildStatistics(),
|
|
||||||
_buildSearchBar(),
|
|
||||||
Expanded(child: _buildLogList()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isFullScreen) {
|
|
||||||
// 全屏模式 - 考虑状态栏高度
|
|
||||||
return Material(
|
|
||||||
color: Colors.black.withValues(alpha: 0.5),
|
|
||||||
child: SafeArea(
|
|
||||||
child: Container(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: double.infinity,
|
|
||||||
color: widget.theme.backgroundColor,
|
|
||||||
child: content,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 对话框模式
|
|
||||||
return Dialog(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: Container(
|
|
||||||
width: MediaQuery.of(context).size.width * 0.9,
|
|
||||||
height: MediaQuery.of(context).size.height * 0.8,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: widget.theme.backgroundColor,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: content,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDetailHeader() {
|
|
||||||
final actions = [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => _copyLogDetails(_selectedLog!),
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.copy,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
tooltip: '复制详情',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
_isFullScreen = !_isFullScreen;
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
_isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
tooltip: _isFullScreen ? '退出全屏' : '全屏显示',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: widget.onClose,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
tooltip: '关闭',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return _isFullScreen
|
|
||||||
? AppBar(
|
|
||||||
title: Text('请求详情'),
|
|
||||||
backgroundColor: widget.theme.primaryColor,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
// excludeHeaderSemantics: false
|
|
||||||
leading: IconButton(
|
|
||||||
onPressed: _hideLogDetail,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: false,
|
|
||||||
actions: actions,
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: widget.theme.primaryColor,
|
|
||||||
borderRadius: _isFullScreen
|
|
||||||
? BorderRadius.zero
|
|
||||||
: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _hideLogDetail,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
tooltip: '返回列表',
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'请求详情',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
...actions
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeader() {
|
|
||||||
final actions = [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_isFullScreen = !_isFullScreen;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
_isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
tooltip: _isFullScreen ? '退出全屏' : '全屏',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
widget.controller.clearLogs();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.clear_all, color: Colors.white),
|
|
||||||
tooltip: '清空日志',
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: widget.onClose,
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
return _isFullScreen
|
|
||||||
? AppBar(
|
|
||||||
title: Text('网络检查器'),
|
|
||||||
backgroundColor: widget.theme.primaryColor,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
centerTitle: false,
|
|
||||||
actions: actions,
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: widget.theme.primaryColor,
|
|
||||||
borderRadius: _isFullScreen
|
|
||||||
? BorderRadius.zero
|
|
||||||
: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
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
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatistics() {
|
|
||||||
final stats = widget.controller.getStatistics();
|
|
||||||
final totalRequests = stats['totalRequests'] as int;
|
|
||||||
final errorRequests = stats['errorRequests'] as int;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: widget.theme.backgroundColor,
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: widget.theme.secondaryTextColor.withValues(alpha: 0.2),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// 简化统计 - 只显示关键信息
|
|
||||||
Text(
|
|
||||||
'总请求: $totalRequests',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: widget.theme.textColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
if (errorRequests > 0) ...[
|
|
||||||
Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
size: 16,
|
|
||||||
color: widget.theme.errorColor,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'错误: $errorRequests',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: widget.theme.errorColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
Icon(
|
|
||||||
Icons.check_circle_outline,
|
|
||||||
size: 16,
|
|
||||||
color: widget.theme.successColor,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'全部成功',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: widget.theme.successColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const Spacer(),
|
|
||||||
// 快速过滤按钮
|
|
||||||
if (errorRequests > 0)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_showOnlyErrors = !_showOnlyErrors;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
_showOnlyErrors ? Icons.clear : Icons.filter_list,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
label: Text(_showOnlyErrors ? '显示全部' : '仅错误'),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: widget.theme.errorColor,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
minimumSize: Size.zero,
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSearchBar() {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: _searchController,
|
|
||||||
style: TextStyle(color: widget.theme.textColor),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '搜索请求...',
|
|
||||||
hintStyle: TextStyle(color: widget.theme.secondaryTextColor),
|
|
||||||
prefixIcon: Icon(
|
|
||||||
Icons.search,
|
|
||||||
color: widget.theme.secondaryTextColor,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: widget.theme.borderColor),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_searchKeyword = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLogList() {
|
|
||||||
final logs = _filteredLogs;
|
|
||||||
|
|
||||||
if (logs.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.inbox, size: 64, color: widget.theme.secondaryTextColor),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'未找到网络请求',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: widget.theme.secondaryTextColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: logs.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final log = logs[index];
|
|
||||||
return _buildLogItem(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLogItem(NetworkLogEntry log) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: widget.theme.cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: Border.all(
|
|
||||||
color: log.status == NetworkRequestStatus.failed
|
|
||||||
? widget.theme.errorColor.withValues(alpha: 0.3)
|
|
||||||
: Colors.transparent,
|
|
||||||
width: log.status == NetworkRequestStatus.failed ? 1 : 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
_showLogDetail(log);
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// URL和状态
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
log.displayUrl,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: widget.theme.textColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
// 状态指示器
|
|
||||||
Container(
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: log.statusColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
|
|
||||||
// 方法标签
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getMethodColor(log.method)
|
|
||||||
.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
log.method,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _getMethodColor(log.method),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
|
|
||||||
Text(
|
|
||||||
'${log.statusCode ?? "?"} ${log.formattedDuration}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: log.statusColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (log.errorMessage != null) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
size: 12,
|
|
||||||
color: widget.theme.errorColor,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Spacer(),
|
|
||||||
// 时间
|
|
||||||
Text(
|
|
||||||
log.formattedTime.contains(' ')
|
|
||||||
? log.formattedTime.split(' ')[1]
|
|
||||||
: log.formattedTime, // 只显示时间部分
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: widget.theme.secondaryTextColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getMethodColor(String method) {
|
|
||||||
switch (method.toUpperCase()) {
|
|
||||||
case 'GET':
|
|
||||||
return Colors.blue;
|
|
||||||
case 'POST':
|
|
||||||
return Colors.green;
|
|
||||||
case 'PUT':
|
|
||||||
return Colors.orange;
|
|
||||||
case 'DELETE':
|
|
||||||
return Colors.red;
|
|
||||||
case 'PATCH':
|
|
||||||
return Colors.purple;
|
|
||||||
default:
|
|
||||||
return widget.theme.secondaryTextColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _copyLogDetails(NetworkLogEntry log) {
|
void _copyLogDetails(NetworkLogEntry log) {
|
||||||
|
// Note: This logic could be moved to util or DetailPage, but keeping here for now as callback
|
||||||
final details = '''
|
final details = '''
|
||||||
网络请求详情
|
网络请求详情
|
||||||
=======================
|
=======================
|
||||||
|
|
@ -600,7 +122,7 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
||||||
} else if (requestData is Map) {
|
} else if (requestData is Map) {
|
||||||
try {
|
try {
|
||||||
return requestData.toString();
|
return requestData.toString();
|
||||||
} catch (e) {
|
} on Object catch (_) {
|
||||||
return requestData.toString();
|
return requestData.toString();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -609,7 +131,133 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDateTime(DateTime dateTime) {
|
String _formatDateTime(DateTime dateTime) {
|
||||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} '
|
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-'
|
||||||
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}';
|
'${dateTime.day.toString().padLeft(2, '0')} '
|
||||||
|
'${dateTime.hour.toString().padLeft(2, '0')}:'
|
||||||
|
'${dateTime.minute.toString().padLeft(2, '0')}:'
|
||||||
|
'${dateTime.second.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListenableBuilder(
|
||||||
|
listenable: widget.controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
// 如果未解锁,显示密码解锁界面
|
||||||
|
if (!widget.controller.isUnlocked) {
|
||||||
|
return Center(
|
||||||
|
child: PasswordDialog(
|
||||||
|
theme: widget.theme,
|
||||||
|
onUnlock: widget.controller.unlock,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget content;
|
||||||
|
|
||||||
|
if (_showDetailPage && _selectedLog != null) {
|
||||||
|
// 显示详情页面
|
||||||
|
content = Column(
|
||||||
|
children: [
|
||||||
|
InspectorDetailHeader(
|
||||||
|
theme: widget.theme,
|
||||||
|
isFullScreen: _isFullScreen,
|
||||||
|
onToggleFullScreen: _toggleFullScreen,
|
||||||
|
onClose: widget.onClose,
|
||||||
|
onBack: _hideLogDetail,
|
||||||
|
onCopy: () => _copyLogDetails(_selectedLog!),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: YxLogDetailPage(
|
||||||
|
log: _selectedLog!,
|
||||||
|
theme: widget.theme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 显示主列表页面
|
||||||
|
final stats = widget.controller.getStatistics();
|
||||||
|
content = Column(
|
||||||
|
children: [
|
||||||
|
InspectorHeader(
|
||||||
|
theme: widget.theme,
|
||||||
|
isFullScreen: _isFullScreen,
|
||||||
|
onToggleFullScreen: _toggleFullScreen,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
InspectorStatistics(
|
||||||
|
theme: widget.theme,
|
||||||
|
totalRequests: stats['totalRequests'] as int? ?? 0,
|
||||||
|
errorRequests: stats['errorRequests'] as int? ?? 0,
|
||||||
|
showOnlyErrors: _showOnlyErrors,
|
||||||
|
onToggleFilter: () {
|
||||||
|
setState(() {
|
||||||
|
_showOnlyErrors = !_showOnlyErrors;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
InspectorSearchBar(
|
||||||
|
theme: widget.theme,
|
||||||
|
controller: _searchController,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchKeyword = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: InspectorLogList(
|
||||||
|
logs: _filteredLogs,
|
||||||
|
theme: widget.theme,
|
||||||
|
onLogTap: _showLogDetail,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isFullScreen) {
|
||||||
|
// 全屏模式 - 考虑状态栏高度
|
||||||
|
return Material(
|
||||||
|
color: Colors.black.withValues(alpha: 0.5),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: double.infinity,
|
||||||
|
color: widget.theme.backgroundColor,
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 对话框模式
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
height: MediaQuery.of(context).size.height * 0.8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.theme.backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../models/network_log_entry.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../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';
|
||||||
|
|
||||||
/// 网络日志详情页面
|
/// 网络日志详情页面
|
||||||
class YxLogDetailPage extends StatelessWidget {
|
class YxLogDetailPage extends StatelessWidget {
|
||||||
|
const YxLogDetailPage({required this.log, required this.theme, super.key});
|
||||||
final NetworkLogEntry log;
|
final NetworkLogEntry log;
|
||||||
final YxNetInspectorTheme theme;
|
final YxNetInspectorTheme theme;
|
||||||
|
|
||||||
const YxLogDetailPage({super.key, required this.log, required this.theme});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return ColoredBox(
|
||||||
color: theme.backgroundColor,
|
color: theme.backgroundColor,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -21,10 +22,21 @@ 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),
|
||||||
|
|
@ -372,7 +384,7 @@ class YxLogDetailPage extends StatelessWidget {
|
||||||
} else if (requestData is Map) {
|
} else if (requestData is Map) {
|
||||||
try {
|
try {
|
||||||
return const JsonEncoder.withIndent(' ').convert(requestData);
|
return const JsonEncoder.withIndent(' ').convert(requestData);
|
||||||
} catch (e) {
|
} on Object catch (_) {
|
||||||
return requestData.toString();
|
return requestData.toString();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -381,7 +393,129 @@ class YxLogDetailPage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDateTime(DateTime dateTime) {
|
String _formatDateTime(DateTime dateTime) {
|
||||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} '
|
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-'
|
||||||
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}:${dateTime.second.toString().padLeft(2, '0')}';
|
'${dateTime.day.toString().padLeft(2, '0')} '
|
||||||
|
'${dateTime.hour.toString().padLeft(2, '0')}:'
|
||||||
|
'${dateTime.minute.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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
|
|
||||||
|
class InspectorDetailHeader extends StatelessWidget {
|
||||||
|
const InspectorDetailHeader({
|
||||||
|
required this.theme,
|
||||||
|
required this.isFullScreen,
|
||||||
|
required this.onToggleFullScreen,
|
||||||
|
required this.onClose,
|
||||||
|
required this.onBack,
|
||||||
|
required this.onCopy,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
final bool isFullScreen;
|
||||||
|
final VoidCallback onToggleFullScreen;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
final VoidCallback onBack;
|
||||||
|
final VoidCallback onCopy;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final actions = [
|
||||||
|
IconButton(
|
||||||
|
onPressed: onCopy,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.copy,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
tooltip: '复制详情',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onToggleFullScreen,
|
||||||
|
icon: Icon(
|
||||||
|
isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
tooltip: isFullScreen ? '退出全屏' : '全屏显示',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onClose,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
tooltip: '关闭',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return isFullScreen
|
||||||
|
? AppBar(
|
||||||
|
backgroundColor: theme.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
// excludeHeaderSemantics: false
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: onBack,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: actions,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
borderRadius: isFullScreen
|
||||||
|
? BorderRadius.zero
|
||||||
|
: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: onBack,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
tooltip: '返回列表',
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'请求详情',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
...actions,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
|
|
||||||
|
class InspectorHeader extends StatelessWidget {
|
||||||
|
const InspectorHeader({
|
||||||
|
required this.theme,
|
||||||
|
required this.isFullScreen,
|
||||||
|
required this.onToggleFullScreen,
|
||||||
|
required this.onClearLogs,
|
||||||
|
required this.onClose,
|
||||||
|
this.onCopyLogs,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
final bool isFullScreen;
|
||||||
|
final VoidCallback onToggleFullScreen;
|
||||||
|
final VoidCallback onClearLogs;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
final VoidCallback? onCopyLogs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final actions = [
|
||||||
|
IconButton(
|
||||||
|
onPressed: onToggleFullScreen,
|
||||||
|
icon: Icon(
|
||||||
|
isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
tooltip: isFullScreen ? '退出全屏' : '全屏',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onClearLogs,
|
||||||
|
icon: const Icon(Icons.clear_all, color: Colors.white),
|
||||||
|
tooltip: '清空日志',
|
||||||
|
),
|
||||||
|
if (onCopyLogs != null)
|
||||||
|
IconButton(
|
||||||
|
onPressed: onCopyLogs,
|
||||||
|
icon: const Icon(Icons.copy, color: Colors.white),
|
||||||
|
tooltip: '复制所有日志',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onClose,
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return isFullScreen
|
||||||
|
? AppBar(
|
||||||
|
title: const Text('网络检查器'),
|
||||||
|
backgroundColor: theme.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
centerTitle: false,
|
||||||
|
actions: actions,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
borderRadius: isFullScreen
|
||||||
|
? BorderRadius.zero
|
||||||
|
: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
...actions,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
|
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||||
|
|
||||||
|
class InspectorLogList extends StatelessWidget {
|
||||||
|
const InspectorLogList({
|
||||||
|
required this.logs,
|
||||||
|
required this.theme,
|
||||||
|
required this.onLogTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<NetworkLogEntry> logs;
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
final ValueChanged<NetworkLogEntry> onLogTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (logs.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.inbox, size: 64, color: theme.secondaryTextColor),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'未找到网络请求',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: theme.secondaryTextColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: logs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final log = logs[index];
|
||||||
|
return InspectorLogItem(
|
||||||
|
log: log,
|
||||||
|
theme: theme,
|
||||||
|
onTap: () => onLogTap(log),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InspectorLogItem extends StatelessWidget {
|
||||||
|
const InspectorLogItem({
|
||||||
|
required this.log,
|
||||||
|
required this.theme,
|
||||||
|
required this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final NetworkLogEntry log;
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
Color _getMethodColor(String method) {
|
||||||
|
switch (method.toUpperCase()) {
|
||||||
|
case 'GET':
|
||||||
|
return Colors.blue;
|
||||||
|
case 'POST':
|
||||||
|
return Colors.green;
|
||||||
|
case 'PUT':
|
||||||
|
return Colors.orange;
|
||||||
|
case 'DELETE':
|
||||||
|
return Colors.red;
|
||||||
|
case 'PATCH':
|
||||||
|
return Colors.purple;
|
||||||
|
default:
|
||||||
|
return theme.secondaryTextColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(
|
||||||
|
color: log.status == NetworkRequestStatus.failed
|
||||||
|
? theme.errorColor.withValues(alpha: 0.3)
|
||||||
|
: Colors.transparent,
|
||||||
|
width: log.status == NetworkRequestStatus.failed ? 1 : 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// URL和状态
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
log.displayUrl,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: theme.textColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// 状态指示器
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: log.statusColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
// 方法标签
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getMethodColor(log.method)
|
||||||
|
.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
log.method,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _getMethodColor(log.method),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'${log.statusCode ?? "?"} ${log.formattedDuration}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: log.statusColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (log.errorMessage != null) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 12,
|
||||||
|
color: theme.errorColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
// 时间 - Fix: Use split safely or standard formatting
|
||||||
|
Text(
|
||||||
|
log.formattedTime.contains(' ')
|
||||||
|
? log.formattedTime.split(' ')[1]
|
||||||
|
: log.formattedTime,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: theme.secondaryTextColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
|
|
||||||
|
class InspectorSearchBar extends StatelessWidget {
|
||||||
|
const InspectorSearchBar({
|
||||||
|
required this.theme,
|
||||||
|
required this.controller,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
final TextEditingController controller;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
style: TextStyle(color: theme.textColor),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: '搜索请求...',
|
||||||
|
hintStyle: TextStyle(color: theme.secondaryTextColor),
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: theme.secondaryTextColor,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: theme.borderColor),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
|
|
||||||
|
class InspectorStatistics extends StatelessWidget {
|
||||||
|
const InspectorStatistics({
|
||||||
|
required this.theme,
|
||||||
|
required this.totalRequests,
|
||||||
|
required this.errorRequests,
|
||||||
|
required this.showOnlyErrors,
|
||||||
|
required this.onToggleFilter,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
final int totalRequests;
|
||||||
|
final int errorRequests;
|
||||||
|
final bool showOnlyErrors;
|
||||||
|
final VoidCallback onToggleFilter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.backgroundColor,
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: theme.secondaryTextColor.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 简化统计 - 只显示关键信息
|
||||||
|
Text(
|
||||||
|
'总请求: $totalRequests',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.textColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
if (errorRequests > 0) ...[
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 16,
|
||||||
|
color: theme.errorColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'错误: $errorRequests',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.errorColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle_outline,
|
||||||
|
size: 16,
|
||||||
|
color: theme.successColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'全部成功',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.successColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
// 快速过滤按钮
|
||||||
|
if (errorRequests > 0)
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: onToggleFilter,
|
||||||
|
icon: Icon(
|
||||||
|
showOnlyErrors ? Icons.clear : Icons.filter_list,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: Text(showOnlyErrors ? '显示全部' : '仅错误'),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: theme.errorColor,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
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('解锁'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,15 @@
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'controller/yx_net_inspector_controller.dart';
|
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
||||||
import 'models/inspector_config.dart';
|
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||||
import 'models/inspector_theme.dart';
|
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||||
import 'widgets/floating_ball.dart';
|
import 'package:yx_net_inspector/src/widgets/floating_ball.dart';
|
||||||
|
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
||||||
|
|
||||||
/// 包装你的应用并提供网络检查功能的主要组件
|
/// 包装你的应用并提供网络检查功能的主要组件
|
||||||
class YxNetInspector extends StatefulWidget {
|
class YxNetInspector extends StatefulWidget {
|
||||||
/// 你的应用组件
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
/// 检查器配置
|
|
||||||
final YxNetInspectorConfig config;
|
|
||||||
|
|
||||||
/// 检查器主题
|
|
||||||
final YxNetInspectorTheme theme;
|
|
||||||
|
|
||||||
const YxNetInspector({
|
const YxNetInspector({
|
||||||
super.key,
|
|
||||||
required this.child,
|
required this.child,
|
||||||
|
super.key,
|
||||||
this.config = const YxNetInspectorConfig(),
|
this.config = const YxNetInspectorConfig(),
|
||||||
this.theme = const YxNetInspectorTheme(),
|
this.theme = const YxNetInspectorTheme(),
|
||||||
});
|
});
|
||||||
|
|
@ -52,6 +43,15 @@ class YxNetInspector extends StatefulWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 你的应用组件
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// 检查器配置
|
||||||
|
final YxNetInspectorConfig config;
|
||||||
|
|
||||||
|
/// 检查器主题
|
||||||
|
final YxNetInspectorTheme theme;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<YxNetInspector> createState() => _YxNetInspectorState();
|
State<YxNetInspector> createState() => _YxNetInspectorState();
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +97,44 @@ class _YxNetInspectorState extends State<YxNetInspector> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 检查器面板覆盖层
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
if (!_controller.isPanelVisible) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Overlay(
|
||||||
|
initialEntries: [
|
||||||
|
OverlayEntry(
|
||||||
|
builder: (context) => Stack(
|
||||||
|
children: [
|
||||||
|
// 背景遮罩
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _controller.hidePanel,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black54,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 检查器面板
|
||||||
|
Center(
|
||||||
|
child: YxInspectorPanel(
|
||||||
|
theme: widget.theme,
|
||||||
|
controller: _controller,
|
||||||
|
onClose: _controller.hidePanel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -133,12 +171,14 @@ 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,12 +1,11 @@
|
||||||
library yx_net_inspector;
|
|
||||||
|
|
||||||
// 核心导出
|
// 核心导出
|
||||||
export 'src/yx_net_inspector_app.dart' show YxNetInspector;
|
|
||||||
export 'src/controller/yx_net_inspector_controller.dart';
|
export 'src/controller/yx_net_inspector_controller.dart';
|
||||||
export 'src/models/network_log_entry.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/widgets/floating_ball_config.dart';
|
export 'src/widgets/floating_ball_config.dart';
|
||||||
|
export 'src/yx_net_inspector_app.dart' show YxNetInspector;
|
||||||
|
|
||||||
// Dio 拦截器需要单独导入:
|
// Dio 拦截器需要单独导入:
|
||||||
// import 'package:yx_net_inspector/src/interceptors/dio_interceptor.dart';
|
// import 'package:yx_net_inspector/src/interceptors/dio_interceptor.dart';
|
||||||
|
|
|
||||||
78
pubspec.lock
78
pubspec.lock
|
|
@ -41,6 +41,22 @@ 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:
|
||||||
|
|
@ -59,30 +75,38 @@ 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:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.9"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -107,6 +131,14 @@ 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:
|
||||||
|
|
@ -164,18 +196,34 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
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:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
|
very_good_analysis:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: very_good_analysis
|
||||||
|
sha256: "62d2b86d183fb81b2edc22913d9f155d26eb5cf3855173adb1f59fac85035c63"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -184,6 +232,14 @@ 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.7.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.0.3
|
version: 1.1.0
|
||||||
homepage: https://github.com/your-username/yx_net_inspector
|
homepage: https://github.com/your-username/yx_net_inspector
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -10,9 +10,12 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
dio: ^5.9.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
very_good_analysis: ^7.0.0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:yx_net_inspector/yx_net_inspector.dart';
|
|
||||||
import 'package:yx_net_inspector/src/widgets/floating_ball.dart';
|
import 'package:yx_net_inspector/src/widgets/floating_ball.dart';
|
||||||
|
import 'package:yx_net_inspector/yx_net_inspector.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('YX Net Inspector 集成测试', () {
|
group('YX Net Inspector 集成测试', () {
|
||||||
|
|
@ -9,12 +9,6 @@ void main() {
|
||||||
// 创建测试应用
|
// 创建测试应用
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
YxNetInspector(
|
YxNetInspector(
|
||||||
config: const YxNetInspectorConfig(
|
|
||||||
showFloatingBall: true,
|
|
||||||
ballSize: 60.0,
|
|
||||||
showInDebugMode: true,
|
|
||||||
),
|
|
||||||
theme: const YxNetInspectorTheme(),
|
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
appBar: AppBar(title: const Text('测试应用')),
|
appBar: AppBar(title: const Text('测试应用')),
|
||||||
|
|
@ -52,8 +46,8 @@ void main() {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
responseData: {
|
responseData: {
|
||||||
'users': [
|
'users': [
|
||||||
{'id': 1, 'name': 'John'}
|
{'id': 1, 'name': 'John'},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
);
|
);
|
||||||
|
|
@ -146,10 +140,6 @@ void main() {
|
||||||
testWidgets('悬浮球拖拽和位置测试', (WidgetTester tester) async {
|
testWidgets('悬浮球拖拽和位置测试', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
YxNetInspector(
|
YxNetInspector(
|
||||||
config: const YxNetInspectorConfig(
|
|
||||||
showFloatingBall: true,
|
|
||||||
draggable: true,
|
|
||||||
),
|
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: Container(),
|
body: Container(),
|
||||||
|
|
@ -179,13 +169,13 @@ void main() {
|
||||||
|
|
||||||
testWidgets('配置禁用时不显示悬浮球', (WidgetTester tester) async {
|
testWidgets('配置禁用时不显示悬浮球', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
YxNetInspector(
|
const YxNetInspector(
|
||||||
config: const YxNetInspectorConfig(
|
config: YxNetInspectorConfig(
|
||||||
showFloatingBall: false,
|
showFloatingBall: false,
|
||||||
),
|
),
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: const Text('Test App'),
|
body: Text('Test App'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -203,7 +193,6 @@ void main() {
|
||||||
testWidgets('主题配置测试', (WidgetTester tester) async {
|
testWidgets('主题配置测试', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
YxNetInspector(
|
YxNetInspector(
|
||||||
config: const YxNetInspectorConfig(),
|
|
||||||
theme: const YxNetInspectorTheme(
|
theme: const YxNetInspectorTheme(
|
||||||
primaryColor: Colors.purple,
|
primaryColor: Colors.purple,
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
|
|
@ -222,7 +211,7 @@ void main() {
|
||||||
// 添加一些日志
|
// 添加一些日志
|
||||||
final controller = YxNetInspectorController.instance;
|
final controller = YxNetInspectorController.instance;
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test', method: 'GET', url: 'https://example.com');
|
id: 'test', method: 'GET', url: 'https://example.com',);
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
|
|
@ -253,7 +242,7 @@ void main() {
|
||||||
controller.clearLogs();
|
controller.clearLogs();
|
||||||
|
|
||||||
// 添加大量日志
|
// 添加大量日志
|
||||||
for (int i = 0; i < 150; i++) {
|
for (var i = 0; i < 150; i++) {
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-$i',
|
id: 'test-$i',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
@ -302,7 +291,7 @@ void main() {
|
||||||
controller.clearLogs();
|
controller.clearLogs();
|
||||||
|
|
||||||
// 添加超过限制的日志
|
// 添加超过限制的日志
|
||||||
for (int i = 0; i < 20; i++) {
|
for (var i = 0; i < 20; i++) {
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'memory-test-$i',
|
id: 'memory-test-$i',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
// 测试入口文件 - 运行所有测试
|
// 测试入口文件 - 运行所有测试
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
// 集成测试
|
||||||
|
import 'integration/full_workflow_test.dart' as integration_test;
|
||||||
|
import 'unit/inspector_config_test.dart' as config_test;
|
||||||
// 单元测试
|
// 单元测试
|
||||||
import 'unit/network_log_entry_test.dart' as network_log_entry_test;
|
import 'unit/network_log_entry_test.dart' as network_log_entry_test;
|
||||||
import 'unit/yx_net_inspector_controller_test.dart' as controller_test;
|
import 'unit/yx_net_inspector_controller_test.dart' as controller_test;
|
||||||
import 'unit/inspector_config_test.dart' as config_test;
|
|
||||||
|
|
||||||
// Widget测试
|
// Widget测试
|
||||||
import 'widget/floating_ball_test.dart' as floating_ball_test;
|
import 'widget/floating_ball_test.dart' as floating_ball_test;
|
||||||
import 'widget/inspector_panel_test.dart' as inspector_panel_test;
|
import 'widget/inspector_panel_test.dart' as inspector_panel_test;
|
||||||
|
|
||||||
// 集成测试
|
|
||||||
import 'integration/full_workflow_test.dart' as integration_test;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('YX Net Inspector 完整测试套件', () {
|
group('YX Net Inspector 完整测试套件', () {
|
||||||
group('单元测试', () {
|
group('单元测试', () {
|
||||||
|
|
@ -26,8 +24,6 @@ void main() {
|
||||||
inspector_panel_test.main();
|
inspector_panel_test.main();
|
||||||
});
|
});
|
||||||
|
|
||||||
group('集成测试', () {
|
group('集成测试', integration_test.main);
|
||||||
integration_test.main();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
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}) {}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ void main() {
|
||||||
test('应该允许自定义配置', () {
|
test('应该允许自定义配置', () {
|
||||||
const config = YxNetInspectorConfig(
|
const config = YxNetInspectorConfig(
|
||||||
showFloatingBall: false,
|
showFloatingBall: false,
|
||||||
ballSize: 80.0,
|
ballSize: 80,
|
||||||
ballColor: Colors.red,
|
ballColor: Colors.red,
|
||||||
showInDebugMode: false,
|
showInDebugMode: false,
|
||||||
showInReleaseMode: true,
|
showInReleaseMode: true,
|
||||||
|
|
@ -51,8 +51,7 @@ void main() {
|
||||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
|
||||||
const configEnabledInDebug = YxNetInspectorConfig(
|
const configEnabledInDebug = YxNetInspectorConfig(
|
||||||
showInDebugMode: true,
|
|
||||||
showInReleaseMode: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const configDisabledInDebug = YxNetInspectorConfig(
|
const configDisabledInDebug = YxNetInspectorConfig(
|
||||||
|
|
@ -75,27 +74,24 @@ void main() {
|
||||||
|
|
||||||
test('copyWith 应该正确创建副本', () {
|
test('copyWith 应该正确创建副本', () {
|
||||||
const originalConfig = YxNetInspectorConfig(
|
const originalConfig = YxNetInspectorConfig(
|
||||||
showFloatingBall: true,
|
|
||||||
ballSize: 60.0,
|
|
||||||
maxLogs: 1000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final copiedConfig = originalConfig.copyWith(
|
final copiedConfig = originalConfig.copyWith(
|
||||||
showFloatingBall: false,
|
showFloatingBall: false,
|
||||||
ballSize: 80.0,
|
ballSize: 80,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(copiedConfig.showFloatingBall, isFalse);
|
expect(copiedConfig.showFloatingBall, isFalse);
|
||||||
expect(copiedConfig.ballSize, equals(80.0));
|
expect(copiedConfig.ballSize, equals(80.0));
|
||||||
expect(copiedConfig.maxLogs, equals(1000)); // 未更改的值应该保持原样
|
expect(copiedConfig.maxLogs, equals(1000)); // 未更改的值应该保持原样
|
||||||
expect(
|
expect(
|
||||||
copiedConfig.showInDebugMode, equals(originalConfig.showInDebugMode));
|
copiedConfig.showInDebugMode, equals(originalConfig.showInDebugMode),);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toString 应该返回有用的字符串表示', () {
|
test('toString 应该返回有用的字符串表示', () {
|
||||||
const config = YxNetInspectorConfig(
|
const config = YxNetInspectorConfig(
|
||||||
showFloatingBall: true,
|
ballSize: 70,
|
||||||
ballSize: 70.0,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final string = config.toString();
|
final string = config.toString();
|
||||||
|
|
@ -106,21 +102,15 @@ void main() {
|
||||||
|
|
||||||
test('相等性比较应该正确工作', () {
|
test('相等性比较应该正确工作', () {
|
||||||
const config1 = YxNetInspectorConfig(
|
const config1 = YxNetInspectorConfig(
|
||||||
showFloatingBall: true,
|
|
||||||
ballSize: 60.0,
|
|
||||||
maxLogs: 1000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const config2 = YxNetInspectorConfig(
|
const config2 = YxNetInspectorConfig(
|
||||||
showFloatingBall: true,
|
|
||||||
ballSize: 60.0,
|
|
||||||
maxLogs: 1000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const config3 = YxNetInspectorConfig(
|
const config3 = YxNetInspectorConfig(
|
||||||
showFloatingBall: false, // 不同的值
|
showFloatingBall: false, // 不同的值
|
||||||
ballSize: 60.0,
|
|
||||||
maxLogs: 1000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(config1, equals(config2));
|
expect(config1, equals(config2));
|
||||||
|
|
@ -130,9 +120,8 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('空值参数应该正确处理', () {
|
test('空值参数应该正确处理', () {
|
||||||
final config = YxNetInspectorConfig().copyWith(
|
final config = const YxNetInspectorConfig().copyWith(
|
||||||
ballColor: null,
|
|
||||||
initialPosition: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(config.ballColor, isNull);
|
expect(config.ballColor, isNull);
|
||||||
|
|
@ -141,7 +130,7 @@ void main() {
|
||||||
|
|
||||||
test('边界值应该正确处理', () {
|
test('边界值应该正确处理', () {
|
||||||
const config = YxNetInspectorConfig(
|
const config = YxNetInspectorConfig(
|
||||||
ballSize: 0.0, // 最小值
|
ballSize: 0, // 最小值
|
||||||
maxLogs: 1, // 最小日志数
|
maxLogs: 1, // 最小日志数
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -12,12 +12,11 @@ void main() {
|
||||||
id: 'test-1',
|
id: 'test-1',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://api.example.com/users?page=1&limit=20',
|
url: 'https://api.example.com/users?page=1&limit=20',
|
||||||
headers: {'Authorization': 'Bearer token'},
|
headers: const {'Authorization': 'Bearer token'},
|
||||||
requestData: null,
|
queryParameters: const {'page': '1', 'limit': '20'},
|
||||||
queryParameters: {'page': '1', 'limit': '20'},
|
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
responseData: {'users': []},
|
responseData: const {'users': []},
|
||||||
timestamp: DateTime(2024, 1, 1, 12, 0, 0),
|
timestamp: DateTime(2024, 1, 1, 12),
|
||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
isSuccess: true,
|
isSuccess: true,
|
||||||
);
|
);
|
||||||
|
|
@ -26,11 +25,11 @@ void main() {
|
||||||
id: 'test-2',
|
id: 'test-2',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://api.example.com/users',
|
url: 'https://api.example.com/users',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: const {'Content-Type': 'application/json'},
|
||||||
requestData: {'name': 'Test User'},
|
requestData: const {'name': 'Test User'},
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
errorMessage: '用户不存在',
|
errorMessage: '用户不存在',
|
||||||
timestamp: DateTime(2024, 1, 1, 12, 5, 0),
|
timestamp: DateTime(2024, 1, 1, 12, 5),
|
||||||
duration: const Duration(milliseconds: 1500),
|
duration: const Duration(milliseconds: 1500),
|
||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
);
|
);
|
||||||
|
|
@ -65,7 +64,7 @@ void main() {
|
||||||
test('requestSize 应该计算正确的请求大小', () {
|
test('requestSize 应该计算正确的请求大小', () {
|
||||||
expect(successfulEntry.requestSize, greaterThan(0));
|
expect(successfulEntry.requestSize, greaterThan(0));
|
||||||
expect(errorEntry.requestSize,
|
expect(errorEntry.requestSize,
|
||||||
greaterThanOrEqualTo(successfulEntry.requestSize));
|
greaterThanOrEqualTo(successfulEntry.requestSize),);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('responseSize 应该计算正确的响应大小', () {
|
test('responseSize 应该计算正确的响应大小', () {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('初始化后应该有正确的默认值', () {
|
test('初始化后应该有正确的默认值', () {
|
||||||
final config = YxNetInspectorConfig();
|
const config = YxNetInspectorConfig();
|
||||||
controller.initialize(config);
|
controller.initialize(config);
|
||||||
|
|
||||||
expect(controller.logs, isEmpty);
|
expect(controller.logs, isEmpty);
|
||||||
|
|
@ -33,7 +33,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('logRequest 应该正确添加请求日志', () {
|
test('logRequest 应该正确添加请求日志', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-1',
|
id: 'test-1',
|
||||||
|
|
@ -50,7 +50,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('logResponse 应该正确更新请求日志', () {
|
test('logResponse 应该正确更新请求日志', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
// 先添加请求
|
// 先添加请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
|
|
@ -72,11 +72,11 @@ void main() {
|
||||||
expect(controller.logs.first.statusCode, equals(200));
|
expect(controller.logs.first.statusCode, equals(200));
|
||||||
expect(controller.logs.first.isSuccess, isTrue);
|
expect(controller.logs.first.isSuccess, isTrue);
|
||||||
expect(controller.logs.first.duration,
|
expect(controller.logs.first.duration,
|
||||||
equals(const Duration(milliseconds: 500)));
|
equals(const Duration(milliseconds: 500)),);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('logError 应该正确处理错误日志', () {
|
test('logError 应该正确处理错误日志', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
// 先添加请求
|
// 先添加请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
|
|
@ -101,11 +101,11 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clearLogs 应该清空所有日志和统计', () {
|
test('clearLogs 应该清空所有日志和统计', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
// 添加一些日志
|
// 添加一些日志
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-1', method: 'GET', url: 'https://example.com');
|
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||||
controller.logResponse(id: 'test-1', statusCode: 200);
|
controller.logResponse(id: 'test-1', statusCode: 200);
|
||||||
|
|
||||||
expect(controller.logs, isNotEmpty);
|
expect(controller.logs, isNotEmpty);
|
||||||
|
|
@ -122,7 +122,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('悬浮球显示状态应该正确切换', () {
|
test('悬浮球显示状态应该正确切换', () {
|
||||||
controller.initialize(YxNetInspectorConfig(showFloatingBall: true));
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
expect(controller.showFloatingBall, isTrue);
|
expect(controller.showFloatingBall, isTrue);
|
||||||
|
|
||||||
|
|
@ -137,23 +137,23 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getStatistics 应该返回正确的统计信息', () {
|
test('getStatistics 应该返回正确的统计信息', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
// 添加成功请求
|
// 添加成功请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'success', method: 'GET', url: 'https://example.com');
|
id: 'success', method: 'GET', url: 'https://example.com',);
|
||||||
controller.logResponse(
|
controller.logResponse(
|
||||||
id: 'success',
|
id: 'success',
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
duration: const Duration(milliseconds: 300));
|
duration: const Duration(milliseconds: 300),);
|
||||||
|
|
||||||
// 添加失败请求
|
// 添加失败请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'error', method: 'POST', url: 'https://example.com');
|
id: 'error', method: 'POST', url: 'https://example.com',);
|
||||||
controller.logError(
|
controller.logError(
|
||||||
id: 'error',
|
id: 'error',
|
||||||
error: '错误',
|
error: '错误',
|
||||||
duration: const Duration(milliseconds: 500));
|
duration: const Duration(milliseconds: 500),);
|
||||||
|
|
||||||
final stats = controller.getStatistics();
|
final stats = controller.getStatistics();
|
||||||
|
|
||||||
|
|
@ -165,12 +165,12 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getRecentLogs 应该返回指定数量的最近日志', () {
|
test('getRecentLogs 应该返回指定数量的最近日志', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
// 添加多个日志
|
// 添加多个日志
|
||||||
for (int i = 0; i < 10; i++) {
|
for (var i = 0; i < 10; i++) {
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-$i', method: 'GET', url: 'https://example.com/$i');
|
id: 'test-$i', method: 'GET', url: 'https://example.com/$i',);
|
||||||
}
|
}
|
||||||
|
|
||||||
final recentLogs = controller.getRecentLogs(count: 5);
|
final recentLogs = controller.getRecentLogs(count: 5);
|
||||||
|
|
@ -179,15 +179,15 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getLogsByStatus 应该正确过滤日志', () {
|
test('getLogsByStatus 应该正确过滤日志', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
// 添加成功和失败的请求
|
// 添加成功和失败的请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'success', method: 'GET', url: 'https://example.com');
|
id: 'success', method: 'GET', url: 'https://example.com',);
|
||||||
controller.logResponse(id: 'success', statusCode: 200);
|
controller.logResponse(id: 'success', statusCode: 200);
|
||||||
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'error', method: 'POST', url: 'https://example.com');
|
id: 'error', method: 'POST', url: 'https://example.com',);
|
||||||
controller.logError(id: 'error', error: '错误');
|
controller.logError(id: 'error', error: '错误');
|
||||||
|
|
||||||
final successLogs = controller.getLogsByStatus(isSuccess: true);
|
final successLogs = controller.getLogsByStatus(isSuccess: true);
|
||||||
|
|
@ -200,16 +200,16 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('searchLogs 应该正确搜索日志', () {
|
test('searchLogs 应该正确搜索日志', () {
|
||||||
controller.initialize(YxNetInspectorConfig());
|
controller.initialize(const YxNetInspectorConfig());
|
||||||
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'user-req', method: 'GET', url: 'https://api.example.com/users');
|
id: 'user-req', method: 'GET', url: 'https://api.example.com/users',);
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'post-req', method: 'POST', url: 'https://api.example.com/posts');
|
id: 'post-req', method: 'POST', url: 'https://api.example.com/posts',);
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'error-req',
|
id: 'error-req',
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: 'https://api.example.com/user/1');
|
url: 'https://api.example.com/user/1',);
|
||||||
controller.logError(id: 'error-req', error: '用户不存在');
|
controller.logError(id: 'error-req', error: '用户不存在');
|
||||||
|
|
||||||
final userLogs = controller.searchLogs('user');
|
final userLogs = controller.searchLogs('user');
|
||||||
|
|
@ -222,12 +222,12 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('日志数量应该受到maxLogs限制', () {
|
test('日志数量应该受到maxLogs限制', () {
|
||||||
controller.initialize(YxNetInspectorConfig(maxLogs: 5));
|
controller.initialize(const YxNetInspectorConfig(maxLogs: 5));
|
||||||
|
|
||||||
// 添加超过限制的日志
|
// 添加超过限制的日志
|
||||||
for (int i = 0; i < 10; i++) {
|
for (var i = 0; i < 10; i++) {
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-$i', method: 'GET', url: 'https://example.com/$i');
|
id: 'test-$i', method: 'GET', url: 'https://example.com/$i',);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(controller.logs.length, equals(5));
|
expect(controller.logs.length, equals(5));
|
||||||
|
|
@ -236,13 +236,12 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('配置未启用时不应该记录日志', () {
|
test('配置未启用时不应该记录日志', () {
|
||||||
controller.initialize(YxNetInspectorConfig(
|
controller.initialize(const YxNetInspectorConfig(
|
||||||
showInDebugMode: false,
|
showInDebugMode: false,
|
||||||
showInReleaseMode: false,
|
),);
|
||||||
));
|
|
||||||
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test', method: 'GET', url: 'https://example.com');
|
id: 'test', method: 'GET', url: 'https://example.com',);
|
||||||
|
|
||||||
expect(controller.logs, isEmpty);
|
expect(controller.logs, isEmpty);
|
||||||
expect(controller.requestCount, equals(0));
|
expect(controller.requestCount, equals(0));
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/floating_ball.dart';
|
|
||||||
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.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_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/floating_ball.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('YxFloatingBall Widget Tests', () {
|
group('YxFloatingBall Widget Tests', () {
|
||||||
|
|
@ -61,9 +61,9 @@ void main() {
|
||||||
testWidgets('应该显示请求数量徽章', (WidgetTester tester) async {
|
testWidgets('应该显示请求数量徽章', (WidgetTester tester) async {
|
||||||
// 添加一些请求
|
// 添加一些请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-1', method: 'GET', url: 'https://example.com');
|
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-2', method: 'POST', url: 'https://example.com');
|
id: 'test-2', method: 'POST', url: 'https://example.com',);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -86,7 +86,7 @@ void main() {
|
||||||
testWidgets('应该显示错误数量徽章', (WidgetTester tester) async {
|
testWidgets('应该显示错误数量徽章', (WidgetTester tester) async {
|
||||||
// 添加请求和错误
|
// 添加请求和错误
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-1', method: 'GET', url: 'https://example.com');
|
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||||
controller.logError(id: 'test-1', error: '网络错误');
|
controller.logError(id: 'test-1', error: '网络错误');
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
|
@ -181,7 +181,7 @@ void main() {
|
||||||
|
|
||||||
// 添加一些请求
|
// 添加一些请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test-1', method: 'GET', url: 'https://example.com');
|
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -221,7 +221,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('自定义大小应该正确应用', (WidgetTester tester) async {
|
testWidgets('自定义大小应该正确应用', (WidgetTester tester) async {
|
||||||
final customSizeConfig = config.copyWith(ballSize: 80.0);
|
final customSizeConfig = config.copyWith(ballSize: 80);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
|
||||||
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.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_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/panel/inspector_log_list.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('YxInspectorPanel Widget Tests', () {
|
group('YxInspectorPanel Widget Tests', () {
|
||||||
|
|
@ -34,20 +35,25 @@ void main() {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 验证面板基本组件
|
// 验证面板基本组件 (标题在非全屏模式已移除以节省空间)
|
||||||
expect(find.text('网络检查器'), findsOneWidget);
|
// Title removed from non-fullscreen mode to save space
|
||||||
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', method: 'GET', url: 'https://example.com');
|
id: 'test-1',
|
||||||
|
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', method: 'POST', url: 'https://example.com');
|
id: 'test-2',
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://example.com',
|
||||||
|
);
|
||||||
controller.logError(id: 'test-2', error: '网络错误');
|
controller.logError(id: 'test-2', error: '网络错误');
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
|
@ -65,12 +71,15 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 验证统计信息
|
// 验证统计信息
|
||||||
expect(find.text('总计'), findsOneWidget);
|
// 验证统计信息
|
||||||
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('2'), findsOneWidget); // 总请求数
|
// Removed old fields
|
||||||
expect(find.text('1'), findsWidgets); // 成功和失败各1个
|
// expect(find.text('总计'), findsOneWidget);
|
||||||
|
// expect(find.text('成功'), findsOneWidget);
|
||||||
|
// expect(find.text('失败'), findsOneWidget);
|
||||||
|
// expect(find.text('成功率'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('应该显示搜索栏', (WidgetTester tester) async {
|
testWidgets('应该显示搜索栏', (WidgetTester tester) async {
|
||||||
|
|
@ -89,16 +98,24 @@ void main() {
|
||||||
// 验证搜索相关组件
|
// 验证搜索相关组件
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
expect(find.text('搜索请求...'), findsOneWidget);
|
expect(find.text('搜索请求...'), findsOneWidget);
|
||||||
expect(find.byType(FilterChip), findsOneWidget);
|
expect(find.text('搜索请求...'), findsOneWidget);
|
||||||
expect(find.text('仅显示错误'), findsOneWidget);
|
// FilterChip was removed/moved
|
||||||
|
// expect(find.byType(FilterChip), findsOneWidget);
|
||||||
|
// expect(find.text('仅显示错误'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('搜索功能应该正常工作', (WidgetTester tester) async {
|
testWidgets('搜索功能应该正常工作', (WidgetTester tester) async {
|
||||||
// 添加测试数据
|
// 添加测试数据
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'user-1', method: 'GET', url: 'https://api.example.com/users');
|
id: 'user-1',
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://api.example.com/users',
|
||||||
|
);
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'post-1', method: 'POST', url: 'https://api.example.com/posts');
|
id: 'post-1',
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://api.example.com/posts',
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -125,11 +142,17 @@ void main() {
|
||||||
testWidgets('错误过滤器应该正常工作', (WidgetTester tester) async {
|
testWidgets('错误过滤器应该正常工作', (WidgetTester tester) async {
|
||||||
// 添加成功和失败的请求
|
// 添加成功和失败的请求
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'success', method: 'GET', url: 'https://example.com');
|
id: 'success',
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://example.com',
|
||||||
|
);
|
||||||
controller.logResponse(id: 'success', statusCode: 200);
|
controller.logResponse(id: 'success', statusCode: 200);
|
||||||
|
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'error', method: 'POST', url: 'https://example.com');
|
id: 'error',
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://example.com',
|
||||||
|
);
|
||||||
controller.logError(id: 'error', error: '网络错误');
|
controller.logError(id: 'error', error: '网络错误');
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
|
@ -146,19 +169,21 @@ void main() {
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 点击错误过滤器
|
// 点击错误过滤器 (It's a TextButton now)
|
||||||
await tester.tap(find.byType(FilterChip));
|
await tester.tap(find.text('仅错误'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 验证过滤器被激活
|
// 验证过滤器被激活 - Text changes to "显示全部"
|
||||||
final filterChip = tester.widget<FilterChip>(find.byType(FilterChip));
|
expect(find.text('显示全部'), findsOneWidget);
|
||||||
expect(filterChip.selected, isTrue);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('清空日志按钮应该正常工作', (WidgetTester tester) async {
|
testWidgets('清空日志按钮应该正常工作', (WidgetTester tester) async {
|
||||||
// 添加一些日志
|
// 添加一些日志
|
||||||
controller.logRequest(
|
controller.logRequest(
|
||||||
id: 'test', method: 'GET', url: 'https://example.com');
|
id: 'test',
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://example.com',
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -204,7 +229,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('关闭按钮应该调用回调', (WidgetTester tester) async {
|
testWidgets('关闭按钮应该调用回调', (WidgetTester tester) async {
|
||||||
bool closeCalled = false;
|
var closeCalled = false;
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -272,7 +297,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 验证日志条目显示
|
// 验证日志条目显示
|
||||||
expect(find.byType(Card), findsWidgets);
|
expect(find.byType(InspectorLogItem), findsWidgets);
|
||||||
expect(find.text('GET'), findsOneWidget);
|
expect(find.text('GET'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -300,7 +325,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 点击日志条目
|
// 点击日志条目
|
||||||
await tester.tap(find.byType(InkWell).first);
|
await tester.tap(find.byType(InspectorLogItem).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// 验证导航到详情页(这里主要验证没有报错)
|
// 验证导航到详情页(这里主要验证没有报错)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
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