mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-10 00:44:12 +08:00
面板
This commit is contained in:
210
lib/main.dart
210
lib/main.dart
@@ -2,13 +2,13 @@ import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:network/network/bin/server.dart';
|
||||
import 'package:network/ui/widgets.dart';
|
||||
import 'package:network/ui/left.dart';
|
||||
import 'package:network/ui/panel.dart';
|
||||
|
||||
import 'network/channel.dart';
|
||||
import 'network/handler.dart';
|
||||
import 'network/http/http.dart';
|
||||
import 'network/util/AttributeKeys.dart';
|
||||
import 'ui/components.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@@ -39,33 +39,109 @@ class NetworkHomePage extends StatefulWidget {
|
||||
State<NetworkHomePage> createState() => _NetworkHomePagePageState();
|
||||
}
|
||||
|
||||
class _NetworkHomePagePageState extends State<NetworkHomePage> implements EventListener {
|
||||
LinkedHashMap<HostAndPort, List<HttpRequest>> containerMap = LinkedHashMap<HostAndPort, List<HttpRequest>>();
|
||||
class DomainWidget extends StatefulWidget {
|
||||
final _DomainWidgetState _state = _DomainWidgetState();
|
||||
final NetworkTabController panel;
|
||||
|
||||
Set<String> expandedHosts = <String>{};
|
||||
DomainWidget({super.key, required this.panel});
|
||||
|
||||
@override
|
||||
void onRequest(Channel channel, HttpRequest request) {
|
||||
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY);
|
||||
var list = containerMap[hostAndPort];
|
||||
if (list == null) {
|
||||
list = [request];
|
||||
setState(() {
|
||||
containerMap;
|
||||
});
|
||||
containerMap[hostAndPort] = list;
|
||||
} else {
|
||||
list.add(request);
|
||||
}
|
||||
void add(Channel channel, HttpRequest request) {
|
||||
_state.add(channel, request);
|
||||
}
|
||||
|
||||
void addResponse(Channel channel, HttpResponse response) {
|
||||
_state.addResponse(channel, response);
|
||||
}
|
||||
|
||||
void clean() {
|
||||
_state.clean();
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Channel channel, HttpResponse response) {}
|
||||
State<StatefulWidget> createState() {
|
||||
return _state;
|
||||
}
|
||||
}
|
||||
|
||||
class _DomainWidgetState extends State<DomainWidget> {
|
||||
LinkedHashMap<HostAndPort, ValueNotifier<HeaderBody>> containerMap =
|
||||
LinkedHashMap<HostAndPort, ValueNotifier<HeaderBody>>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var map = containerMap.values.map((e) => ValueListenableBuilder<HeaderBody>(
|
||||
valueListenable: e,
|
||||
builder: (context, value, child) {
|
||||
return _show(value);
|
||||
}));
|
||||
|
||||
return ListView(children: map.toList());
|
||||
}
|
||||
|
||||
///添加请求
|
||||
void add(Channel channel, HttpRequest request) {
|
||||
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY);
|
||||
ValueNotifier<HeaderBody>? valueNotifier = containerMap[hostAndPort];
|
||||
var listURI = RowURI(request, widget.panel);
|
||||
if (valueNotifier != null) {
|
||||
valueNotifier.value.addBody(channel.id, listURI);
|
||||
valueNotifier.value = valueNotifier.value.copy();
|
||||
return;
|
||||
}
|
||||
|
||||
var headerBody = HeaderBody(hostAndPort.url);
|
||||
valueNotifier = ValueNotifier<HeaderBody>(headerBody);
|
||||
headerBody.addBody(channel.id, listURI);
|
||||
|
||||
setState(() {
|
||||
containerMap[hostAndPort] = valueNotifier!;
|
||||
});
|
||||
}
|
||||
|
||||
///添加响应
|
||||
void addResponse(Channel channel, HttpResponse response) {
|
||||
HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY);
|
||||
ValueNotifier<HeaderBody>? valueNotifier = containerMap[hostAndPort];
|
||||
if (valueNotifier != null && valueNotifier.value.getBody(channel.id) != null) {
|
||||
var body = valueNotifier.value.getBody(channel.id);
|
||||
body?.add(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void clean() {
|
||||
setState(() {
|
||||
containerMap.forEach((key, value) {
|
||||
value.dispose();
|
||||
});
|
||||
containerMap.clear();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _show(Widget widget) {
|
||||
return AnimatedOpacity(opacity: 1.0, duration: const Duration(seconds: 2), child: widget);
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkHomePagePageState extends State<NetworkHomePage> implements EventListener {
|
||||
late DomainWidget domainWidget;
|
||||
final NetworkTabController panel = NetworkTabController();
|
||||
|
||||
@override
|
||||
void onRequest(Channel channel, HttpRequest request) {
|
||||
domainWidget.add(channel, request);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Channel channel, HttpResponse response) {
|
||||
domainWidget.addResponse(channel, response);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print("initState");
|
||||
super.initState();
|
||||
domainWidget = DomainWidget(panel: panel);
|
||||
start(listener: this);
|
||||
}
|
||||
|
||||
@@ -73,102 +149,14 @@ class _NetworkHomePagePageState extends State<NetworkHomePage> implements EventL
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(onPressed: () => domainWidget.clean(), icon: const Icon(Icons.cleaning_services)),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Row(children: [
|
||||
SizedBox(width: 420, child: ListView(children: _buildHosts())),
|
||||
SizedBox(width: 420, child: domainWidget),
|
||||
const Spacer(),
|
||||
Expanded(flex: 100, child: NetworkTabController()),
|
||||
Expanded(flex: 100, child: Visibility(visible: true, child: domainWidget.panel)),
|
||||
]));
|
||||
}
|
||||
|
||||
Widget _row(HostAndPort host) {
|
||||
bool selected = expandedHosts.contains(host.url);
|
||||
return ListTile(
|
||||
leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 16),
|
||||
dense: true,
|
||||
selected: selected,
|
||||
horizontalTitleGap: 0,
|
||||
visualDensity: const VisualDensity(vertical: -3.6),
|
||||
title: Text(host.url, textAlign: TextAlign.left),
|
||||
onTap: () {
|
||||
if (!expandedHosts.remove(host.url)) {
|
||||
expandedHosts.add(host.url);
|
||||
}
|
||||
setState(() {
|
||||
expandedHosts;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _buildHosts() {
|
||||
print(containerMap.keys);
|
||||
List<Widget> list = [];
|
||||
for (var host in containerMap.keys) {
|
||||
list.add(_row(host));
|
||||
|
||||
if (expandedHosts.contains(host.url)) {
|
||||
containerMap[host]?.forEach((element) {
|
||||
print(element);
|
||||
list.add(ListURI(leading: Icons.html, text: '${element.method.name} ${Uri.parse(element.uri).path}'));
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
// return [
|
||||
// ListTile(
|
||||
// leading: const Icon(Icons.arrow_drop_down),
|
||||
// dense: true,
|
||||
// title: const Text('https://dmall.com', textAlign: TextAlign.left),
|
||||
// selected: true,
|
||||
// onTap: () {}),
|
||||
// const ListURI(leading: Icons.image_outlined, text: 'POST /private/browser/stats/haha'),
|
||||
// const ListURI(leading: Icons.javascript_sharp, text: 'GET /private/browser/stats/haha'),
|
||||
// const ListURI(leading: Icons.css, text: 'GET /private/browser/stats/haha'),
|
||||
// ListTile(
|
||||
// leading: const Icon(
|
||||
// size: 15,
|
||||
// Icons.document_scanner,
|
||||
// color: Colors.red,
|
||||
// ),
|
||||
// title: const Text('POST /private/browser/stats', textAlign: TextAlign.left),
|
||||
// textColor: Colors.red,
|
||||
// trailing: const Icon(Icons.chevron_right),
|
||||
// visualDensity: const VisualDensity(vertical: -4),
|
||||
// dense: true,
|
||||
// contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50.0),
|
||||
// onTap: () {}),
|
||||
// const Divider(),
|
||||
// ListTile(
|
||||
// leading: const Icon(Icons.expand_more),
|
||||
// dense: true,
|
||||
// title: const Text('https://baidu.com', textAlign: TextAlign.left),
|
||||
// onTap: () {}),
|
||||
// _buildRow(),
|
||||
// const Divider(),
|
||||
// ];
|
||||
}
|
||||
|
||||
Widget _buildRow() {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
return Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 16),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RowURI(leading: Icons.image, text: "POST /private/browser/stats/private/browser/stats"),
|
||||
RowURI(leading: Icons.javascript_sharp, text: "GET /private/browser/stats.js"),
|
||||
RowURI(leading: Icons.css, text: "GET /openapi/documents/oristartguid.css"),
|
||||
],
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import '../channel.dart';
|
||||
import '../handler.dart';
|
||||
import '../http/codec.dart';
|
||||
import '../util/logger.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
start();
|
||||
@@ -15,6 +16,7 @@ Future<void> start({EventListener? listener}) async {
|
||||
..initChannel((channel) {
|
||||
channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(), HttpChannelHandler(listener: listener));
|
||||
});
|
||||
log.i("listen on $port");
|
||||
await server.bind().then((value) => {setSystemProxy(port)});
|
||||
}
|
||||
|
||||
@@ -27,8 +29,8 @@ void setSystemProxy(int port) {
|
||||
// .then((ProcessResult results) {
|
||||
// print(results.stdout);
|
||||
// });
|
||||
// Process.run('networksetup', ['-setwebproxy', 'Wi-Fi', '127.0.0.1', port.toString()]).then((ProcessResult results) {
|
||||
// print(results.stdout);
|
||||
// });
|
||||
Process.run('networksetup', ['-setwebproxy', 'Wi-Fi', '127.0.0.1', port.toString()]).then((ProcessResult results) {
|
||||
print(results.stdout);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
var hostAndPort = getHostAndPort(httpRequest);
|
||||
clientChannel.putAttribute(AttributeKeys.HOST_KEY, hostAndPort);
|
||||
|
||||
var proxyHandler = HttpResponseProxyHandler(clientChannel);
|
||||
var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener);
|
||||
var proxyChannel = await HttpClients.connect(hostAndPort, proxyHandler);
|
||||
|
||||
//https代理新建连接请求
|
||||
@@ -104,14 +104,17 @@ class HttpResponseProxyHandler extends ChannelHandler<HttpResponse> {
|
||||
/// 排除的后缀 不打印日志
|
||||
final Set<String> excludeContent = HashSet.from(["javascript", "text/css", "application/font-woff", "image"]);
|
||||
|
||||
HttpResponseProxyHandler(this.clientChannel);
|
||||
EventListener? listener;
|
||||
|
||||
HttpResponseProxyHandler(this.clientChannel, {this.listener});
|
||||
|
||||
@override
|
||||
void channelRead(Channel channel, HttpResponse msg) {
|
||||
String contentType = msg.headers.contentType;
|
||||
if (excludeContent.every((element) => !contentType.contains(element))) {
|
||||
// log.i("[${clientChannel.id}] Response ${String.fromCharCodes(msg.body ?? [])}");
|
||||
// log.i("[${clientChannel.id}] Response ${ String.fromCharCodes(msg.body ?? [])}");
|
||||
}
|
||||
listener?.onResponse(clientChannel, msg);
|
||||
//发送给客户端
|
||||
clientChannel.write(msg);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'http_headers.dart';
|
||||
|
||||
///定义HTTP消息的接口,为HttpRequest和HttpResponse提供公共属性。
|
||||
@@ -12,10 +15,17 @@ abstract class HttpMessage {
|
||||
HttpMessage(this.protocolVersion);
|
||||
|
||||
String get bodyAsString {
|
||||
if (body == null) {
|
||||
if (body == null || body?.isEmpty == true) {
|
||||
return "";
|
||||
}
|
||||
return String.fromCharCodes(body!);
|
||||
try {
|
||||
if (headers.isGzip) {
|
||||
return utf8.decode(gzip.decode(body!));
|
||||
}
|
||||
return utf8.decode(body!);
|
||||
} catch (e) {
|
||||
return String.fromCharCodes(body!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +142,10 @@ class HttpStatus {
|
||||
|
||||
HttpStatus(this.code, this.reasonPhrase);
|
||||
|
||||
bool isSuccessful() {
|
||||
return code >= 200 && code < 300;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HttpResponseStatus{code: $code, reasonPhrase: $reasonPhrase}';
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:core';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:basic_utils/basic_utils.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
var securityContext = await CertificateManager.getCertificateContext('www.baidu.com');
|
||||
@@ -64,7 +65,7 @@ class CertificateManager {
|
||||
var csr = X509Utils.generateRsaCsrPem(x509Subject, caPriKey, serverPubKey as RSAPublicKey, san: [host]);
|
||||
|
||||
Map<String, String> issuer = Map.from(_caCert.tbsCertificate!.subject);
|
||||
var csrPem = X509Utils.generateSelfSignedCertificate(caPriKey, csr, 3650, sans: [host], issuer: issuer);
|
||||
var csrPem = X509Utils.generateSelfSignedCertificate(caPriKey, csr, 365, sans: [host], issuer: issuer);
|
||||
return csrPem;
|
||||
}
|
||||
|
||||
@@ -73,13 +74,13 @@ class CertificateManager {
|
||||
return;
|
||||
}
|
||||
//从项目目录加入ca根证书
|
||||
var caPem = await File('assets/certs/ca.crt').readAsString();
|
||||
var caPem = await rootBundle.loadString('assets/certs/ca.crt');
|
||||
_caCert = X509Utils.x509CertificateFromPem(caPem);
|
||||
//根据CA证书subject来动态生成目标服务器证书的issuer和subject
|
||||
|
||||
//从项目目录加入ca私钥
|
||||
var privateBytes = await File('assets/certs/ca_private.der').readAsBytes();
|
||||
_caPriKey = CryptoUtils.rsaPrivateKeyFromDERBytes(privateBytes);
|
||||
var privateBytes = await rootBundle.load('assets/certs/ca_private.der');
|
||||
_caPriKey = CryptoUtils.rsaPrivateKeyFromDERBytes(privateBytes.buffer.asUint8List());
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ListURI extends StatefulWidget {
|
||||
final IconData? leading;
|
||||
final String text;
|
||||
final Color? color;
|
||||
final IconData trailing;
|
||||
|
||||
const ListURI(
|
||||
{Key? key, this.leading, required this.text, this.color = Colors.green, this.trailing = Icons.chevron_right})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<ListURI> createState() => _ListURIState();
|
||||
}
|
||||
|
||||
class _ListURIState extends State<ListURI> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(widget.leading, size: 15, color: widget.color),
|
||||
title: Text(widget.text, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
trailing: Icon(widget.trailing),
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50.0),
|
||||
onTap: () {});
|
||||
}
|
||||
}
|
||||
|
||||
class RowURI extends StatefulWidget {
|
||||
final IconData? leading;
|
||||
final String text;
|
||||
final IconData trailing;
|
||||
|
||||
const RowURI({Key? key, this.leading, required this.text, this.trailing = Icons.arrow_right}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _RowURIState();
|
||||
}
|
||||
}
|
||||
|
||||
class _RowURIState extends State<RowURI> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(children: [
|
||||
TextButton.icon(
|
||||
icon: Icon(widget.leading, size: 16),
|
||||
onPressed: () {
|
||||
print("hello");
|
||||
},
|
||||
label: Text(widget.text, style: const TextStyle(color: Colors.black87)),
|
||||
),
|
||||
const Positioned(right: 10, child: Icon(Icons.chevron_right))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class IconText extends StatefulWidget {
|
||||
const IconText({Key? key, this.leading, required this.text, this.color, this.trailing}) : super(key: key);
|
||||
|
||||
final Widget? leading;
|
||||
final String text;
|
||||
final Color? color;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
State<IconText> createState() => _IconTextState();
|
||||
}
|
||||
|
||||
class _IconTextState extends State<IconText> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
widget.leading ?? const SizedBox(),
|
||||
Text(widget.text),
|
||||
widget.trailing ?? const SizedBox(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
112
lib/ui/left.dart
Normal file
112
lib/ui/left.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:network/network/http/http.dart';
|
||||
import 'package:network/ui/panel.dart';
|
||||
|
||||
///标题和内容布局 标题是域名 内容是域名下请求
|
||||
class HeaderBody extends StatefulWidget {
|
||||
final Map<String, RowURI> map = HashMap<String, RowURI>();
|
||||
|
||||
final String header;
|
||||
final Queue<RowURI> _body = Queue<RowURI>();
|
||||
final bool selected;
|
||||
|
||||
HeaderBody(this.header, {Key? key, this.selected = false}) : super(key: key);
|
||||
|
||||
///添加请求
|
||||
void addBody(String key, RowURI widget) {
|
||||
_body.addFirst(widget);
|
||||
map[key] = widget;
|
||||
}
|
||||
|
||||
RowURI? getBody(String key) {
|
||||
return map[key];
|
||||
}
|
||||
|
||||
//复制
|
||||
HeaderBody copy() {
|
||||
var headerBody = HeaderBody(header, selected: selected);
|
||||
headerBody._body.addAll(_body);
|
||||
headerBody.map.addAll(map);
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _HeaderBodyState();
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderBodyState extends State<HeaderBody> {
|
||||
late bool selected;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selected = widget.selected;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
_hostWidget(widget.header),
|
||||
Visibility(visible: selected, child: Column(children: widget._body.toList()))
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _hostWidget(String title) {
|
||||
return ListTile(
|
||||
leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 16),
|
||||
dense: true,
|
||||
selected: selected,
|
||||
horizontalTitleGap: 0,
|
||||
visualDensity: const VisualDensity(vertical: -3.6),
|
||||
title: Text(title, textAlign: TextAlign.left),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selected = !selected;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RowURI extends StatefulWidget {
|
||||
final Color? color;
|
||||
final HttpRequest request;
|
||||
HttpResponse? response;
|
||||
final NetworkTabController panel;
|
||||
|
||||
final _RowURIState _state = _RowURIState();
|
||||
|
||||
RowURI(this.request, this.panel, {Key? key, this.color = Colors.green}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RowURI> createState() => _state;
|
||||
|
||||
void add(HttpResponse response) {
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
|
||||
class _RowURIState extends State<RowURI> {
|
||||
bool selected = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var request = widget.request;
|
||||
var leading = widget.response == null ? Icons.http : Icons.http;
|
||||
var title = '${request.method.name} ${Uri.parse(request.uri).path}';
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(leading, size: 15, color: widget.color),
|
||||
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 50.0),
|
||||
onTap: () {
|
||||
selected = !selected;
|
||||
widget.panel.change(widget.request, widget.response);
|
||||
});
|
||||
}
|
||||
}
|
||||
136
lib/ui/panel.dart
Normal file
136
lib/ui/panel.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:network/network/http/http.dart';
|
||||
|
||||
class NetworkTabController extends StatefulWidget {
|
||||
final tabs = <Tab>[
|
||||
const Tab(child: Text('General', style: TextStyle(fontSize: 18))),
|
||||
const Tab(child: Text('Request', style: TextStyle(fontSize: 18))),
|
||||
const Tab(child: Text('Response', style: TextStyle(fontSize: 18))),
|
||||
const Tab(child: Text('Cookies', style: TextStyle(fontSize: 18))),
|
||||
];
|
||||
|
||||
final _NetworkTabState _state = _NetworkTabState();
|
||||
HttpRequest? request;
|
||||
HttpResponse? response;
|
||||
|
||||
NetworkTabController({super.key});
|
||||
|
||||
void change(HttpRequest request, HttpResponse? response) {
|
||||
_state.setState(() {
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _state;
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkTabState extends State<NetworkTabController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.request == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: widget.tabs.length,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: TabBar(tabs: widget.tabs)),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
general(),
|
||||
request(),
|
||||
response(),
|
||||
ListView(children: const [Text("Cookies")])
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget general() {
|
||||
return ExpansionTile(
|
||||
title: const Text("General", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
initiallyExpanded: true,
|
||||
childrenPadding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Row(children: [
|
||||
const Expanded(flex: 1, child: SelectableText("Request URL:")),
|
||||
Expanded(flex: 4, child: SelectableText(widget.request?.uri ?? ''))
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
Row(children: [
|
||||
const Expanded(flex: 1, child: SelectableText("Status Code:")),
|
||||
Expanded(flex: 4, child: SelectableText(widget.response?.status.code.toString() ?? ''))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
Widget request() {
|
||||
var headers = <Widget>[];
|
||||
widget.request?.headers.forEach((name, value) {
|
||||
headers.add(Row(children: [
|
||||
Expanded(flex: 2, child: SelectableText('$name:')),
|
||||
Expanded(flex: 4, child: SelectableText(value)),
|
||||
const SizedBox(height: 20),
|
||||
]));
|
||||
});
|
||||
|
||||
ExpansionTile? bodyWidgets;
|
||||
if (widget.request?.body?.isNotEmpty == true) {
|
||||
bodyWidgets = ExpansionTile(
|
||||
title: const Text("Request Body", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
initiallyExpanded: true,
|
||||
shape: const Border(),
|
||||
childrenPadding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
SelectableText.rich(
|
||||
TextSpan(text: widget.request?.bodyAsString, style: const TextStyle(color: Colors.black)))
|
||||
]);
|
||||
}
|
||||
|
||||
return ListView(children: [
|
||||
ExpansionTile(
|
||||
title: const Text("Request Headers", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
initiallyExpanded: true,
|
||||
shape: const Border(),
|
||||
childrenPadding: const EdgeInsets.all(20),
|
||||
children: headers),
|
||||
const Divider(),
|
||||
bodyWidgets ?? const SizedBox()
|
||||
]);
|
||||
}
|
||||
|
||||
Widget response() {
|
||||
var headers = <Widget>[];
|
||||
widget.response?.headers.forEach((name, value) {
|
||||
headers.add(Row(children: [
|
||||
Expanded(flex: 2, child: SelectableText('$name:')),
|
||||
Expanded(flex: 4, child: SelectableText(value)),
|
||||
const SizedBox(height: 20),
|
||||
]));
|
||||
});
|
||||
|
||||
return ListView(children: [
|
||||
ExpansionTile(
|
||||
title: const Text("Response Headers", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
initiallyExpanded: true,
|
||||
shape: const Border(),
|
||||
childrenPadding: const EdgeInsets.all(20),
|
||||
children: headers),
|
||||
const Divider(),
|
||||
ExpansionTile(
|
||||
title: const Text("Response Body", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
initiallyExpanded: true,
|
||||
shape: const Border(),
|
||||
childrenPadding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
SelectableText.rich(
|
||||
TextSpan(text: widget.response?.bodyAsString, style: const TextStyle(color: Colors.black))
|
||||
)
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NetworkTabController extends DefaultTabController {
|
||||
static const myTabs = <Tab>[
|
||||
Tab(child: Text('General', style: TextStyle(fontSize: 18))),
|
||||
Tab(child: Text('Request', style: TextStyle(fontSize: 18))),
|
||||
Tab(child: Text('Response', style: TextStyle(fontSize: 18))),
|
||||
Tab(child: Text('Cookies', style: TextStyle(fontSize: 18))),
|
||||
];
|
||||
|
||||
NetworkTabController({super.key})
|
||||
: super(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const TabBar(tabs: myTabs),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
ListView(children: const [
|
||||
Text.rich(TextSpan(children: [
|
||||
TextSpan(text: "Home: "),
|
||||
TextSpan(text: "https://flutterchina.club", style: TextStyle(color: Colors.blue)),
|
||||
]))
|
||||
]),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: const Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('General', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
Row(children: [
|
||||
Expanded(flex: 1, child: Text("Request URL:")),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
"https://googleads.g.doubleclick.net/pagead/html/r20230601/r20190131/zrt_lookup.html"))
|
||||
]),
|
||||
SizedBox(height: 10), //保留间距
|
||||
Row(children: [
|
||||
Expanded(flex: 1, child: Text("Request Method:")),
|
||||
Expanded(flex: 4, child: Text("GET"))
|
||||
]),
|
||||
SizedBox(height: 10), //保留间距
|
||||
Row(children: [
|
||||
Expanded(flex: 1, child: Text("Status Code:")),
|
||||
Expanded(flex: 4, child: Text("200"))
|
||||
]),
|
||||
SizedBox(height: 10), //保留间距
|
||||
Row(children: [
|
||||
Expanded(flex: 1, child: Text("Remote Address:")),
|
||||
Expanded(flex: 4, child: Text("127.0.0.1:8080"))
|
||||
])
|
||||
])),
|
||||
const ExpansionTile(
|
||||
title: Text("General", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
initiallyExpanded: true,
|
||||
childrenPadding: EdgeInsets.all(20),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Expanded(flex: 1, child: Text("Request URL:")),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
"https://googleads.g.doubleclick.net/pagead/html/r20230601/r20190131/zrt_lookup.html"))
|
||||
]),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(children: [
|
||||
Expanded(flex: 1, child: Text("Status Code:")),
|
||||
Expanded(flex: 4, child: Text("200"))
|
||||
])
|
||||
]),
|
||||
ListView(children: const [Text("Cookies")])
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user