Merge branch 'release/1.1.0'
This commit is contained in:
commit
9d6aaf64b4
|
|
@ -107,3 +107,6 @@ temp/
|
|||
# OS specific
|
||||
.DS_Store
|
||||
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/),
|
||||
此项目遵循 [语义化版本](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
|
||||
|
||||
### 新增
|
||||
|
|
|
|||
|
|
@ -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 网络检查器调试',
|
||||
home: YxNetInspector(
|
||||
config: const YxNetInspectorConfig(
|
||||
showFloatingBall: true,
|
||||
ballSize: 60,
|
||||
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(
|
||||
appBar: AppBar(
|
||||
title: const Text('调试测试'),
|
||||
|
|
@ -55,7 +44,7 @@ class MyDebugApp extends StatelessWidget {
|
|||
statusCode: 200,
|
||||
responseData: const {
|
||||
'title': 'Test Post',
|
||||
'body': 'Test content'
|
||||
'body': 'Test content',
|
||||
},
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,22 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -87,26 +103,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -139,6 +155,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -196,10 +220,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.6"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -212,10 +236,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -238,7 +262,7 @@ packages:
|
|||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.2"
|
||||
version: "1.0.4"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
// ignore_for_file: prefer_constructors_over_static_methods // Singleton pattern requires static access
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/network_log_entry.dart';
|
||||
import '../models/inspector_config.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||
|
||||
/// 网络日志检查器控制器
|
||||
/// 管理网络日志和检查器状态
|
||||
class YxNetInspectorController extends ChangeNotifier {
|
||||
YxNetInspectorController._internal();
|
||||
static YxNetInspectorController? _instance;
|
||||
static YxNetInspectorController get instance {
|
||||
return _instance ??= YxNetInspectorController._internal();
|
||||
}
|
||||
|
||||
YxNetInspectorController._internal();
|
||||
|
||||
/// 配置信息
|
||||
late YxNetInspectorConfig _config;
|
||||
YxNetInspectorConfig get config => _config;
|
||||
|
||||
/// 网络日志列表
|
||||
final List<NetworkLogEntry> _logs = <NetworkLogEntry>[];
|
||||
List<NetworkLogEntry> get logs => List.unmodifiable(_logs);
|
||||
|
||||
/// 统计数据
|
||||
int _requestCount = 0;
|
||||
|
|
@ -33,12 +31,72 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
|
||||
/// 悬浮球显示状态
|
||||
bool _showFloatingBall = true;
|
||||
bool get showFloatingBall => _showFloatingBall && _config.isEnabled;
|
||||
|
||||
/// 初始化控制器配置
|
||||
/// 面板显示状态
|
||||
bool _isPanelVisible = false;
|
||||
bool _isUnlocked = true; // 默认为已解锁
|
||||
|
||||
/// 初始化控制器
|
||||
void initialize(YxNetInspectorConfig config) {
|
||||
_config = config;
|
||||
_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,
|
||||
timestamp: DateTime.now(),
|
||||
isSuccess: false, // 响应到达时会更新
|
||||
status: NetworkRequestStatus.pending, // 初始状态为进行中
|
||||
);
|
||||
|
||||
_logs.insert(0, entry);
|
||||
|
|
@ -76,6 +133,7 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
int? statusCode,
|
||||
dynamic responseData,
|
||||
Duration? duration,
|
||||
Map<String, dynamic>? responseHeaders,
|
||||
}) {
|
||||
if (!_config.isEnabled) return;
|
||||
|
||||
|
|
@ -89,6 +147,7 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
final updatedLog = originalLog.copyWith(
|
||||
statusCode: statusCode,
|
||||
responseData: responseData,
|
||||
responseHeaders: responseHeaders,
|
||||
duration: duration,
|
||||
isSuccess: isSuccess,
|
||||
status: isSuccess
|
||||
|
|
@ -214,6 +273,43 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
}).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() {
|
||||
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';
|
||||
|
||||
/// 网络检查器配置
|
||||
@immutable
|
||||
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;
|
||||
|
||||
|
|
@ -33,18 +48,8 @@ class YxNetInspectorConfig {
|
|||
/// 是否自动隐藏悬浮球
|
||||
final bool autoHide;
|
||||
|
||||
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,
|
||||
});
|
||||
/// 访问检查器面板所需的密码(如果为空则无需密码)
|
||||
final String? password;
|
||||
|
||||
/// 根据当前模式判断检查器是否应该启用
|
||||
bool get isEnabled {
|
||||
|
|
@ -67,6 +72,7 @@ class YxNetInspectorConfig {
|
|||
bool? draggable,
|
||||
bool? showBadge,
|
||||
bool? autoHide,
|
||||
String? password,
|
||||
}) {
|
||||
return YxNetInspectorConfig(
|
||||
showFloatingBall: showFloatingBall ?? this.showFloatingBall,
|
||||
|
|
@ -79,6 +85,7 @@ class YxNetInspectorConfig {
|
|||
draggable: draggable ?? this.draggable,
|
||||
showBadge: showBadge ?? this.showBadge,
|
||||
autoHide: autoHide ?? this.autoHide,
|
||||
password: password ?? this.password,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +101,8 @@ class YxNetInspectorConfig {
|
|||
'initialPosition: $initialPosition, '
|
||||
'draggable: $draggable, '
|
||||
'showBadge: $showBadge, '
|
||||
'autoHide: $autoHide'
|
||||
'autoHide: $autoHide, '
|
||||
'password: ${password != null ? "***" : "null"}'
|
||||
')';
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +119,8 @@ class YxNetInspectorConfig {
|
|||
other.initialPosition == initialPosition &&
|
||||
other.draggable == draggable &&
|
||||
other.showBadge == showBadge &&
|
||||
other.autoHide == autoHide;
|
||||
other.autoHide == autoHide &&
|
||||
other.password == password;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -127,6 +136,7 @@ class YxNetInspectorConfig {
|
|||
draggable,
|
||||
showBadge,
|
||||
autoHide,
|
||||
password,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,40 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 网络检查器主题配置
|
||||
@immutable
|
||||
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 的主色调
|
||||
final Color primaryColor;
|
||||
|
||||
|
|
@ -32,39 +65,6 @@ class YxNetInspectorTheme {
|
|||
/// 悬浮球颜色(如果设置则覆盖配置)
|
||||
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({
|
||||
Color? primaryColor,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 网络请求状态枚举
|
||||
|
|
@ -13,11 +15,29 @@ enum NetworkRequestStatus {
|
|||
}
|
||||
|
||||
/// 网络请求日志条目
|
||||
@immutable
|
||||
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 method;
|
||||
final String url;
|
||||
final Map<String, dynamic>? headers;
|
||||
final Map<String, dynamic>? responseHeaders;
|
||||
final dynamic requestData;
|
||||
final Map<String, dynamic>? queryParameters;
|
||||
final int? statusCode;
|
||||
|
|
@ -28,22 +48,6 @@ class NetworkLogEntry {
|
|||
final bool isSuccess;
|
||||
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 {
|
||||
switch (status) {
|
||||
|
|
@ -70,7 +74,7 @@ class NetworkLogEntry {
|
|||
|
||||
/// 获取预估请求大小
|
||||
int get requestSize {
|
||||
int size = 0;
|
||||
var size = 0;
|
||||
if (requestData != null) {
|
||||
size += requestData.toString().length;
|
||||
}
|
||||
|
|
@ -102,7 +106,7 @@ class NetworkLogEntry {
|
|||
String get formattedDuration {
|
||||
if (duration == null) return '未知';
|
||||
final ms = duration!.inMilliseconds;
|
||||
if (ms < 1000) return '${ms} ms';
|
||||
if (ms < 1000) return '$ms ms';
|
||||
return '${(ms / 1000).toStringAsFixed(1)} s';
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +115,7 @@ class NetworkLogEntry {
|
|||
try {
|
||||
final uri = Uri.parse(url);
|
||||
return '${uri.path}${uri.query.isNotEmpty ? '?${uri.query}' : ''}';
|
||||
} catch (e) {
|
||||
} on Object catch (_) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
|
@ -119,18 +123,45 @@ class NetworkLogEntry {
|
|||
String get hostUrl {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
return '${uri.host}';
|
||||
} catch (e) {
|
||||
return uri.host;
|
||||
} on Object catch (_) {
|
||||
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({
|
||||
String? id,
|
||||
String? method,
|
||||
String? url,
|
||||
Map<String, dynamic>? headers,
|
||||
Map<String, dynamic>? responseHeaders,
|
||||
dynamic requestData,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
int? statusCode,
|
||||
|
|
@ -146,6 +177,7 @@ class NetworkLogEntry {
|
|||
method: method ?? this.method,
|
||||
url: url ?? this.url,
|
||||
headers: headers ?? this.headers,
|
||||
responseHeaders: responseHeaders ?? this.responseHeaders,
|
||||
requestData: requestData ?? this.requestData,
|
||||
queryParameters: queryParameters ?? this.queryParameters,
|
||||
statusCode: statusCode ?? this.statusCode,
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../controller/yx_net_inspector_controller.dart';
|
||||
import '../models/inspector_config.dart';
|
||||
import '../models/inspector_theme.dart';
|
||||
import 'inspector_panel.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';
|
||||
|
||||
/// 悬浮调试球组件
|
||||
class YxFloatingBall extends StatefulWidget {
|
||||
const YxFloatingBall({
|
||||
required this.config, required this.theme, required this.controller, super.key,
|
||||
});
|
||||
final YxNetInspectorConfig config;
|
||||
final YxNetInspectorTheme theme;
|
||||
final YxNetInspectorController controller;
|
||||
|
||||
const YxFloatingBall({
|
||||
super.key,
|
||||
required this.config,
|
||||
required this.theme,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<YxFloatingBall> createState() => _YxFloatingBallState();
|
||||
}
|
||||
|
|
@ -28,8 +23,6 @@ class _YxFloatingBallState extends State<YxFloatingBall>
|
|||
late Animation<double> _opacityAnimation;
|
||||
|
||||
Offset _position = const Offset(20, 200);
|
||||
bool _isExpanded = false;
|
||||
OverlayEntry? _currentOverlayEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -46,167 +39,23 @@ class _YxFloatingBallState extends State<YxFloatingBall>
|
|||
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),
|
||||
);
|
||||
|
||||
_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),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 清理 overlay
|
||||
if (_currentOverlayEntry != null) {
|
||||
_currentOverlayEntry!.remove();
|
||||
_currentOverlayEntry = null;
|
||||
}
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (_isExpanded) {
|
||||
_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() 处理
|
||||
widget.controller.togglePanel();
|
||||
}
|
||||
|
||||
void _onPanStart(DragStartDetails details) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 悬浮调试球配置
|
||||
@immutable
|
||||
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;
|
||||
|
||||
|
|
@ -26,49 +69,6 @@ class YxFloatingBallConfig {
|
|||
/// 悬浮球透明度
|
||||
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({
|
||||
double? size,
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../controller/yx_net_inspector_controller.dart';
|
||||
import '../models/inspector_theme.dart';
|
||||
import '../models/network_log_entry.dart';
|
||||
import 'log_detail_page.dart';
|
||||
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||
import 'package:yx_net_inspector/src/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 {
|
||||
final YxNetInspectorTheme theme;
|
||||
final YxNetInspectorController controller;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const YxInspectorPanel({
|
||||
super.key,
|
||||
required this.theme,
|
||||
required this.controller,
|
||||
required this.onClose,
|
||||
super.key,
|
||||
});
|
||||
final YxNetInspectorTheme theme;
|
||||
final YxNetInspectorController controller;
|
||||
final VoidCallback onClose;
|
||||
|
||||
@override
|
||||
State<YxInspectorPanel> createState() => _YxInspectorPanelState();
|
||||
|
|
@ -52,8 +57,14 @@ class _YxInspectorPanelState extends State<YxInspectorPanel> {
|
|||
});
|
||||
}
|
||||
|
||||
void _toggleFullScreen() {
|
||||
setState(() {
|
||||
_isFullScreen = !_isFullScreen;
|
||||
});
|
||||
}
|
||||
|
||||
List<NetworkLogEntry> get _filteredLogs {
|
||||
List<NetworkLogEntry> logs = widget.controller.logs;
|
||||
var logs = widget.controller.logs;
|
||||
|
||||
// 搜索过滤
|
||||
if (_searchKeyword.isNotEmpty) {
|
||||
|
|
@ -70,497 +81,8 @@ class _YxInspectorPanelState extends State<YxInspectorPanel> {
|
|||
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) {
|
||||
// Note: This logic could be moved to util or DetailPage, but keeping here for now as callback
|
||||
final details = '''
|
||||
网络请求详情
|
||||
=======================
|
||||
|
|
@ -600,7 +122,7 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
|||
} else if (requestData is Map) {
|
||||
try {
|
||||
return requestData.toString();
|
||||
} catch (e) {
|
||||
} on Object catch (_) {
|
||||
return requestData.toString();
|
||||
}
|
||||
} else {
|
||||
|
|
@ -609,7 +131,133 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
|||
}
|
||||
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.year}-${dateTime.month.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')}';
|
||||
return '${dateTime.year}-${dateTime.month.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 'package:flutter/material.dart';
|
||||
import '../models/network_log_entry.dart';
|
||||
import '../models/inspector_theme.dart';
|
||||
import 'package:flutter/services.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 {
|
||||
const YxLogDetailPage({required this.log, required this.theme, super.key});
|
||||
final NetworkLogEntry log;
|
||||
final YxNetInspectorTheme theme;
|
||||
|
||||
const YxLogDetailPage({super.key, required this.log, required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return ColoredBox(
|
||||
color: theme.backgroundColor,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -21,10 +22,21 @@ class YxLogDetailPage extends StatelessWidget {
|
|||
children: [
|
||||
_buildBasicInfoCard(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildCurlCard(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildRequestInfoCard(),
|
||||
const SizedBox(height: 16),
|
||||
if (log.headers != null && log.headers!.isNotEmpty) ...[
|
||||
_buildHeadersCard('请求头 (Request Headers)', log.headers!),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_buildResponseInfoCard(),
|
||||
const SizedBox(height: 16),
|
||||
if (log.responseHeaders != null &&
|
||||
log.responseHeaders!.isNotEmpty) ...[
|
||||
_buildHeadersCard('响应头 (Response Headers)', log.responseHeaders!),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (log.errorMessage != null) ...[
|
||||
_buildErrorInfoCard(),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -372,7 +384,7 @@ class YxLogDetailPage extends StatelessWidget {
|
|||
} else if (requestData is Map) {
|
||||
try {
|
||||
return const JsonEncoder.withIndent(' ').convert(requestData);
|
||||
} catch (e) {
|
||||
} on Object catch (_) {
|
||||
return requestData.toString();
|
||||
}
|
||||
} else {
|
||||
|
|
@ -381,7 +393,129 @@ class YxLogDetailPage extends StatelessWidget {
|
|||
}
|
||||
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.year}-${dateTime.month.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')}';
|
||||
return '${dateTime.year}-${dateTime.month.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 'controller/yx_net_inspector_controller.dart';
|
||||
import 'models/inspector_config.dart';
|
||||
import 'models/inspector_theme.dart';
|
||||
import 'widgets/floating_ball.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/floating_ball.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
||||
|
||||
/// 包装你的应用并提供网络检查功能的主要组件
|
||||
class YxNetInspector extends StatefulWidget {
|
||||
/// 你的应用组件
|
||||
final Widget child;
|
||||
|
||||
/// 检查器配置
|
||||
final YxNetInspectorConfig config;
|
||||
|
||||
/// 检查器主题
|
||||
final YxNetInspectorTheme theme;
|
||||
|
||||
const YxNetInspector({
|
||||
super.key,
|
||||
required this.child,
|
||||
super.key,
|
||||
this.config = const YxNetInspectorConfig(),
|
||||
this.theme = const YxNetInspectorTheme(),
|
||||
});
|
||||
|
|
@ -52,6 +43,15 @@ class YxNetInspector extends StatefulWidget {
|
|||
);
|
||||
}
|
||||
|
||||
/// 你的应用组件
|
||||
final Widget child;
|
||||
|
||||
/// 检查器配置
|
||||
final YxNetInspectorConfig config;
|
||||
|
||||
/// 检查器主题
|
||||
final YxNetInspectorTheme theme;
|
||||
|
||||
@override
|
||||
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,
|
||||
dynamic responseData,
|
||||
Duration? duration,
|
||||
Map<String, dynamic>? responseHeaders,
|
||||
}) {
|
||||
controller.logResponse(
|
||||
id: id,
|
||||
statusCode: statusCode,
|
||||
responseData: responseData,
|
||||
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/models/network_log_entry.dart';
|
||||
export 'src/interceptors/dio_interceptor.dart';
|
||||
export 'src/models/inspector_config.dart';
|
||||
export 'src/models/inspector_theme.dart';
|
||||
export 'src/models/network_log_entry.dart';
|
||||
export 'src/widgets/floating_ball_config.dart';
|
||||
export 'src/yx_net_inspector_app.dart' show YxNetInspector;
|
||||
|
||||
// Dio 拦截器需要单独导入:
|
||||
// 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"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -59,30 +75,38 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -107,6 +131,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -164,18 +196,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -184,6 +232,14 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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.
|
||||
version: 1.0.3
|
||||
version: 1.1.0
|
||||
homepage: https://github.com/your-username/yx_net_inspector
|
||||
|
||||
environment:
|
||||
|
|
@ -10,9 +10,12 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
dio: ^5.9.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
very_good_analysis: ^7.0.0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.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/yx_net_inspector.dart';
|
||||
|
||||
void main() {
|
||||
group('YX Net Inspector 集成测试', () {
|
||||
|
|
@ -9,12 +9,6 @@ void main() {
|
|||
// 创建测试应用
|
||||
await tester.pumpWidget(
|
||||
YxNetInspector(
|
||||
config: const YxNetInspectorConfig(
|
||||
showFloatingBall: true,
|
||||
ballSize: 60.0,
|
||||
showInDebugMode: true,
|
||||
),
|
||||
theme: const YxNetInspectorTheme(),
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('测试应用')),
|
||||
|
|
@ -52,8 +46,8 @@ void main() {
|
|||
statusCode: 200,
|
||||
responseData: {
|
||||
'users': [
|
||||
{'id': 1, 'name': 'John'}
|
||||
]
|
||||
{'id': 1, 'name': 'John'},
|
||||
],
|
||||
},
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
|
|
@ -146,10 +140,6 @@ void main() {
|
|||
testWidgets('悬浮球拖拽和位置测试', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
YxNetInspector(
|
||||
config: const YxNetInspectorConfig(
|
||||
showFloatingBall: true,
|
||||
draggable: true,
|
||||
),
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Container(),
|
||||
|
|
@ -179,13 +169,13 @@ void main() {
|
|||
|
||||
testWidgets('配置禁用时不显示悬浮球', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
YxNetInspector(
|
||||
config: const YxNetInspectorConfig(
|
||||
const YxNetInspector(
|
||||
config: YxNetInspectorConfig(
|
||||
showFloatingBall: false,
|
||||
),
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: const Text('Test App'),
|
||||
body: Text('Test App'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -203,7 +193,6 @@ void main() {
|
|||
testWidgets('主题配置测试', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
YxNetInspector(
|
||||
config: const YxNetInspectorConfig(),
|
||||
theme: const YxNetInspectorTheme(
|
||||
primaryColor: Colors.purple,
|
||||
backgroundColor: Colors.black,
|
||||
|
|
@ -222,7 +211,7 @@ void main() {
|
|||
// 添加一些日志
|
||||
final controller = YxNetInspectorController.instance;
|
||||
controller.logRequest(
|
||||
id: 'test', method: 'GET', url: 'https://example.com');
|
||||
id: 'test', method: 'GET', url: 'https://example.com',);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
|
|
@ -253,7 +242,7 @@ void main() {
|
|||
controller.clearLogs();
|
||||
|
||||
// 添加大量日志
|
||||
for (int i = 0; i < 150; i++) {
|
||||
for (var i = 0; i < 150; i++) {
|
||||
controller.logRequest(
|
||||
id: 'test-$i',
|
||||
method: 'GET',
|
||||
|
|
@ -302,7 +291,7 @@ void main() {
|
|||
controller.clearLogs();
|
||||
|
||||
// 添加超过限制的日志
|
||||
for (int i = 0; i < 20; i++) {
|
||||
for (var i = 0; i < 20; i++) {
|
||||
controller.logRequest(
|
||||
id: 'memory-test-$i',
|
||||
method: 'GET',
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
// 测试入口文件 - 运行所有测试
|
||||
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/yx_net_inspector_controller_test.dart' as controller_test;
|
||||
import 'unit/inspector_config_test.dart' as config_test;
|
||||
|
||||
// Widget测试
|
||||
import 'widget/floating_ball_test.dart' as floating_ball_test;
|
||||
import 'widget/inspector_panel_test.dart' as inspector_panel_test;
|
||||
|
||||
// 集成测试
|
||||
import 'integration/full_workflow_test.dart' as integration_test;
|
||||
|
||||
void main() {
|
||||
group('YX Net Inspector 完整测试套件', () {
|
||||
group('单元测试', () {
|
||||
|
|
@ -26,8 +24,6 @@ void main() {
|
|||
inspector_panel_test.main();
|
||||
});
|
||||
|
||||
group('集成测试', () {
|
||||
integration_test.main();
|
||||
});
|
||||
group('集成测试', 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('应该允许自定义配置', () {
|
||||
const config = YxNetInspectorConfig(
|
||||
showFloatingBall: false,
|
||||
ballSize: 80.0,
|
||||
ballSize: 80,
|
||||
ballColor: Colors.red,
|
||||
showInDebugMode: false,
|
||||
showInReleaseMode: true,
|
||||
|
|
@ -51,8 +51,7 @@ void main() {
|
|||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
|
||||
const configEnabledInDebug = YxNetInspectorConfig(
|
||||
showInDebugMode: true,
|
||||
showInReleaseMode: false,
|
||||
|
||||
);
|
||||
|
||||
const configDisabledInDebug = YxNetInspectorConfig(
|
||||
|
|
@ -75,27 +74,24 @@ void main() {
|
|||
|
||||
test('copyWith 应该正确创建副本', () {
|
||||
const originalConfig = YxNetInspectorConfig(
|
||||
showFloatingBall: true,
|
||||
ballSize: 60.0,
|
||||
maxLogs: 1000,
|
||||
|
||||
);
|
||||
|
||||
final copiedConfig = originalConfig.copyWith(
|
||||
showFloatingBall: false,
|
||||
ballSize: 80.0,
|
||||
ballSize: 80,
|
||||
);
|
||||
|
||||
expect(copiedConfig.showFloatingBall, isFalse);
|
||||
expect(copiedConfig.ballSize, equals(80.0));
|
||||
expect(copiedConfig.maxLogs, equals(1000)); // 未更改的值应该保持原样
|
||||
expect(
|
||||
copiedConfig.showInDebugMode, equals(originalConfig.showInDebugMode));
|
||||
copiedConfig.showInDebugMode, equals(originalConfig.showInDebugMode),);
|
||||
});
|
||||
|
||||
test('toString 应该返回有用的字符串表示', () {
|
||||
const config = YxNetInspectorConfig(
|
||||
showFloatingBall: true,
|
||||
ballSize: 70.0,
|
||||
ballSize: 70,
|
||||
);
|
||||
|
||||
final string = config.toString();
|
||||
|
|
@ -106,21 +102,15 @@ void main() {
|
|||
|
||||
test('相等性比较应该正确工作', () {
|
||||
const config1 = YxNetInspectorConfig(
|
||||
showFloatingBall: true,
|
||||
ballSize: 60.0,
|
||||
maxLogs: 1000,
|
||||
|
||||
);
|
||||
|
||||
const config2 = YxNetInspectorConfig(
|
||||
showFloatingBall: true,
|
||||
ballSize: 60.0,
|
||||
maxLogs: 1000,
|
||||
|
||||
);
|
||||
|
||||
const config3 = YxNetInspectorConfig(
|
||||
showFloatingBall: false, // 不同的值
|
||||
ballSize: 60.0,
|
||||
maxLogs: 1000,
|
||||
);
|
||||
|
||||
expect(config1, equals(config2));
|
||||
|
|
@ -130,9 +120,8 @@ void main() {
|
|||
});
|
||||
|
||||
test('空值参数应该正确处理', () {
|
||||
final config = YxNetInspectorConfig().copyWith(
|
||||
ballColor: null,
|
||||
initialPosition: null,
|
||||
final config = const YxNetInspectorConfig().copyWith(
|
||||
|
||||
);
|
||||
|
||||
expect(config.ballColor, isNull);
|
||||
|
|
@ -141,7 +130,7 @@ void main() {
|
|||
|
||||
test('边界值应该正确处理', () {
|
||||
const config = YxNetInspectorConfig(
|
||||
ballSize: 0.0, // 最小值
|
||||
ballSize: 0, // 最小值
|
||||
maxLogs: 1, // 最小日志数
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||
|
||||
void main() {
|
||||
|
|
@ -12,12 +12,11 @@ void main() {
|
|||
id: 'test-1',
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users?page=1&limit=20',
|
||||
headers: {'Authorization': 'Bearer token'},
|
||||
requestData: null,
|
||||
queryParameters: {'page': '1', 'limit': '20'},
|
||||
headers: const {'Authorization': 'Bearer token'},
|
||||
queryParameters: const {'page': '1', 'limit': '20'},
|
||||
statusCode: 200,
|
||||
responseData: {'users': []},
|
||||
timestamp: DateTime(2024, 1, 1, 12, 0, 0),
|
||||
responseData: const {'users': []},
|
||||
timestamp: DateTime(2024, 1, 1, 12),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
isSuccess: true,
|
||||
);
|
||||
|
|
@ -26,11 +25,11 @@ void main() {
|
|||
id: 'test-2',
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
requestData: {'name': 'Test User'},
|
||||
headers: const {'Content-Type': 'application/json'},
|
||||
requestData: const {'name': 'Test User'},
|
||||
statusCode: 404,
|
||||
errorMessage: '用户不存在',
|
||||
timestamp: DateTime(2024, 1, 1, 12, 5, 0),
|
||||
timestamp: DateTime(2024, 1, 1, 12, 5),
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
isSuccess: false,
|
||||
);
|
||||
|
|
@ -65,7 +64,7 @@ void main() {
|
|||
test('requestSize 应该计算正确的请求大小', () {
|
||||
expect(successfulEntry.requestSize, greaterThan(0));
|
||||
expect(errorEntry.requestSize,
|
||||
greaterThanOrEqualTo(successfulEntry.requestSize));
|
||||
greaterThanOrEqualTo(successfulEntry.requestSize),);
|
||||
});
|
||||
|
||||
test('responseSize 应该计算正确的响应大小', () {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('初始化后应该有正确的默认值', () {
|
||||
final config = YxNetInspectorConfig();
|
||||
const config = YxNetInspectorConfig();
|
||||
controller.initialize(config);
|
||||
|
||||
expect(controller.logs, isEmpty);
|
||||
|
|
@ -33,7 +33,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('logRequest 应该正确添加请求日志', () {
|
||||
controller.initialize(YxNetInspectorConfig());
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
controller.logRequest(
|
||||
id: 'test-1',
|
||||
|
|
@ -50,7 +50,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('logResponse 应该正确更新请求日志', () {
|
||||
controller.initialize(YxNetInspectorConfig());
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
// 先添加请求
|
||||
controller.logRequest(
|
||||
|
|
@ -72,11 +72,11 @@ void main() {
|
|||
expect(controller.logs.first.statusCode, equals(200));
|
||||
expect(controller.logs.first.isSuccess, isTrue);
|
||||
expect(controller.logs.first.duration,
|
||||
equals(const Duration(milliseconds: 500)));
|
||||
equals(const Duration(milliseconds: 500)),);
|
||||
});
|
||||
|
||||
test('logError 应该正确处理错误日志', () {
|
||||
controller.initialize(YxNetInspectorConfig());
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
// 先添加请求
|
||||
controller.logRequest(
|
||||
|
|
@ -101,11 +101,11 @@ void main() {
|
|||
});
|
||||
|
||||
test('clearLogs 应该清空所有日志和统计', () {
|
||||
controller.initialize(YxNetInspectorConfig());
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
// 添加一些日志
|
||||
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);
|
||||
|
||||
expect(controller.logs, isNotEmpty);
|
||||
|
|
@ -122,7 +122,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('悬浮球显示状态应该正确切换', () {
|
||||
controller.initialize(YxNetInspectorConfig(showFloatingBall: true));
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
expect(controller.showFloatingBall, isTrue);
|
||||
|
||||
|
|
@ -137,23 +137,23 @@ void main() {
|
|||
});
|
||||
|
||||
test('getStatistics 应该返回正确的统计信息', () {
|
||||
controller.initialize(YxNetInspectorConfig());
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
// 添加成功请求
|
||||
controller.logRequest(
|
||||
id: 'success', method: 'GET', url: 'https://example.com');
|
||||
id: 'success', method: 'GET', url: 'https://example.com',);
|
||||
controller.logResponse(
|
||||
id: 'success',
|
||||
statusCode: 200,
|
||||
duration: const Duration(milliseconds: 300));
|
||||
duration: const Duration(milliseconds: 300),);
|
||||
|
||||
// 添加失败请求
|
||||
controller.logRequest(
|
||||
id: 'error', method: 'POST', url: 'https://example.com');
|
||||
id: 'error', method: 'POST', url: 'https://example.com',);
|
||||
controller.logError(
|
||||
id: 'error',
|
||||
error: '错误',
|
||||
duration: const Duration(milliseconds: 500));
|
||||
duration: const Duration(milliseconds: 500),);
|
||||
|
||||
final stats = controller.getStatistics();
|
||||
|
||||
|
|
@ -165,12 +165,12 @@ void main() {
|
|||
});
|
||||
|
||||
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(
|
||||
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);
|
||||
|
|
@ -179,15 +179,15 @@ void main() {
|
|||
});
|
||||
|
||||
test('getLogsByStatus 应该正确过滤日志', () {
|
||||
controller.initialize(YxNetInspectorConfig());
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
// 添加成功和失败的请求
|
||||
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.logRequest(
|
||||
id: 'error', method: 'POST', url: 'https://example.com');
|
||||
id: 'error', method: 'POST', url: 'https://example.com',);
|
||||
controller.logError(id: 'error', error: '错误');
|
||||
|
||||
final successLogs = controller.getLogsByStatus(isSuccess: true);
|
||||
|
|
@ -200,16 +200,16 @@ void main() {
|
|||
});
|
||||
|
||||
test('searchLogs 应该正确搜索日志', () {
|
||||
controller.initialize(YxNetInspectorConfig());
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
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(
|
||||
id: 'post-req', method: 'POST', url: 'https://api.example.com/posts');
|
||||
id: 'post-req', method: 'POST', url: 'https://api.example.com/posts',);
|
||||
controller.logRequest(
|
||||
id: 'error-req',
|
||||
method: 'DELETE',
|
||||
url: 'https://api.example.com/user/1');
|
||||
url: 'https://api.example.com/user/1',);
|
||||
controller.logError(id: 'error-req', error: '用户不存在');
|
||||
|
||||
final userLogs = controller.searchLogs('user');
|
||||
|
|
@ -222,12 +222,12 @@ void main() {
|
|||
});
|
||||
|
||||
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(
|
||||
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));
|
||||
|
|
@ -236,13 +236,12 @@ void main() {
|
|||
});
|
||||
|
||||
test('配置未启用时不应该记录日志', () {
|
||||
controller.initialize(YxNetInspectorConfig(
|
||||
controller.initialize(const YxNetInspectorConfig(
|
||||
showInDebugMode: false,
|
||||
showInReleaseMode: false,
|
||||
));
|
||||
),);
|
||||
|
||||
controller.logRequest(
|
||||
id: 'test', method: 'GET', url: 'https://example.com');
|
||||
id: 'test', method: 'GET', url: 'https://example.com',);
|
||||
|
||||
expect(controller.logs, isEmpty);
|
||||
expect(controller.requestCount, equals(0));
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:flutter/material.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/models/inspector_config.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/floating_ball.dart';
|
||||
|
||||
void main() {
|
||||
group('YxFloatingBall Widget Tests', () {
|
||||
|
|
@ -61,9 +61,9 @@ void main() {
|
|||
testWidgets('应该显示请求数量徽章', (WidgetTester tester) async {
|
||||
// 添加一些请求
|
||||
controller.logRequest(
|
||||
id: 'test-1', method: 'GET', url: 'https://example.com');
|
||||
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||
controller.logRequest(
|
||||
id: 'test-2', method: 'POST', url: 'https://example.com');
|
||||
id: 'test-2', method: 'POST', url: 'https://example.com',);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
|
@ -86,7 +86,7 @@ void main() {
|
|||
testWidgets('应该显示错误数量徽章', (WidgetTester tester) async {
|
||||
// 添加请求和错误
|
||||
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: '网络错误');
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
|
@ -181,7 +181,7 @@ void main() {
|
|||
|
||||
// 添加一些请求
|
||||
controller.logRequest(
|
||||
id: 'test-1', method: 'GET', url: 'https://example.com');
|
||||
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
|
@ -221,7 +221,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('自定义大小应该正确应用', (WidgetTester tester) async {
|
||||
final customSizeConfig = config.copyWith(ballSize: 80.0);
|
||||
final customSizeConfig = config.copyWith(ballSize: 80);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:flutter/material.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/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/inspector_log_list.dart';
|
||||
|
||||
void main() {
|
||||
group('YxInspectorPanel Widget Tests', () {
|
||||
|
|
@ -34,20 +35,25 @@ void main() {
|
|||
),
|
||||
);
|
||||
|
||||
// 验证面板基本组件
|
||||
expect(find.text('网络检查器'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.network_check), findsOneWidget);
|
||||
// 验证面板基本组件 (标题在非全屏模式已移除以节省空间)
|
||||
// Title removed from non-fullscreen mode to save space
|
||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('应该显示统计信息', (WidgetTester tester) async {
|
||||
// 添加一些测试数据
|
||||
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.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: '网络错误');
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
|
@ -65,12 +71,15 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// 验证统计信息
|
||||
expect(find.text('总计'), findsOneWidget);
|
||||
expect(find.text('成功'), findsOneWidget);
|
||||
expect(find.text('失败'), findsOneWidget);
|
||||
expect(find.text('成功率'), findsOneWidget);
|
||||
expect(find.text('2'), findsOneWidget); // 总请求数
|
||||
expect(find.text('1'), findsWidgets); // 成功和失败各1个
|
||||
// 验证统计信息
|
||||
expect(find.text('总请求: 2'), findsOneWidget);
|
||||
expect(find.text('错误: 1'), findsOneWidget);
|
||||
|
||||
// Removed old fields
|
||||
// expect(find.text('总计'), findsOneWidget);
|
||||
// expect(find.text('成功'), findsOneWidget);
|
||||
// expect(find.text('失败'), findsOneWidget);
|
||||
// expect(find.text('成功率'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('应该显示搜索栏', (WidgetTester tester) async {
|
||||
|
|
@ -89,16 +98,24 @@ void main() {
|
|||
// 验证搜索相关组件
|
||||
expect(find.byType(TextField), 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 {
|
||||
// 添加测试数据
|
||||
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(
|
||||
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(
|
||||
MaterialApp(
|
||||
|
|
@ -125,11 +142,17 @@ void main() {
|
|||
testWidgets('错误过滤器应该正常工作', (WidgetTester tester) async {
|
||||
// 添加成功和失败的请求
|
||||
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.logRequest(
|
||||
id: 'error', method: 'POST', url: 'https://example.com');
|
||||
id: 'error',
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
);
|
||||
controller.logError(id: 'error', error: '网络错误');
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
|
@ -146,19 +169,21 @@ void main() {
|
|||
|
||||
await tester.pump();
|
||||
|
||||
// 点击错误过滤器
|
||||
await tester.tap(find.byType(FilterChip));
|
||||
// 点击错误过滤器 (It's a TextButton now)
|
||||
await tester.tap(find.text('仅错误'));
|
||||
await tester.pump();
|
||||
|
||||
// 验证过滤器被激活
|
||||
final filterChip = tester.widget<FilterChip>(find.byType(FilterChip));
|
||||
expect(filterChip.selected, isTrue);
|
||||
// 验证过滤器被激活 - Text changes to "显示全部"
|
||||
expect(find.text('显示全部'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('清空日志按钮应该正常工作', (WidgetTester tester) async {
|
||||
// 添加一些日志
|
||||
controller.logRequest(
|
||||
id: 'test', method: 'GET', url: 'https://example.com');
|
||||
id: 'test',
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
|
@ -204,7 +229,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('关闭按钮应该调用回调', (WidgetTester tester) async {
|
||||
bool closeCalled = false;
|
||||
var closeCalled = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
|
@ -272,7 +297,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// 验证日志条目显示
|
||||
expect(find.byType(Card), findsWidgets);
|
||||
expect(find.byType(InspectorLogItem), findsWidgets);
|
||||
expect(find.text('GET'), findsOneWidget);
|
||||
});
|
||||
|
||||
|
|
@ -300,7 +325,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// 点击日志条目
|
||||
await tester.tap(find.byType(InkWell).first);
|
||||
await tester.tap(find.byType(InspectorLogItem).first);
|
||||
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