diff --git a/lib/main.dart b/lib/main.dart index cf65b3a..3af1781 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,87 +40,69 @@ class NetworkHomePage extends StatefulWidget { } class DomainWidget extends StatefulWidget { - final _DomainWidgetState _state = _DomainWidgetState(); final NetworkTabController panel; - DomainWidget({super.key, required this.panel}); + DomainWidget({required this.panel}) : super(key: GlobalKey<_DomainWidgetState>()); void add(Channel channel, HttpRequest request) { - _state.add(channel, request); + var state = key as GlobalKey<_DomainWidgetState>; + state.currentState?.add(channel, request); } void addResponse(Channel channel, HttpResponse response) { - _state.addResponse(channel, response); + var state = key as GlobalKey<_DomainWidgetState>; + state.currentState?.addResponse(channel, response); } void clean() { - _state.clean(); + var state = key as GlobalKey<_DomainWidgetState>; + state.currentState?.clean(); } @override State createState() { - return _state; + return _DomainWidgetState(); } } class _DomainWidgetState extends State { - LinkedHashMap> containerMap = - LinkedHashMap>(); + LinkedHashMap containerMap = LinkedHashMap(); @override Widget build(BuildContext context) { - var map = containerMap.values.map((e) => ValueListenableBuilder( - valueListenable: e, - builder: (context, value, child) { - return _show(value); - })); - - return ListView(children: map.toList()); + var list = containerMap.values; + return ListView.builder(itemBuilder: (BuildContext context, int index) => list.elementAt(index), itemCount: list.length ); } ///添加请求 void add(Channel channel, HttpRequest request) { - HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY); - ValueNotifier? valueNotifier = containerMap[hostAndPort]; + HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.host); + HeaderBody? headerBody = containerMap[hostAndPort]; var listURI = RowURI(request, widget.panel); - if (valueNotifier != null) { - valueNotifier.value.addBody(channel.id, listURI); - valueNotifier.value = valueNotifier.value.copy(); + if (headerBody != null) { + headerBody.addBody(channel.id, listURI); return; } - var headerBody = HeaderBody(hostAndPort.url); - valueNotifier = ValueNotifier(headerBody); + headerBody = HeaderBody(hostAndPort.url); headerBody.addBody(channel.id, listURI); - setState(() { - containerMap[hostAndPort] = valueNotifier!; + containerMap[hostAndPort] = headerBody!; }); } ///添加响应 void addResponse(Channel channel, HttpResponse response) { - HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY); - ValueNotifier? valueNotifier = containerMap[hostAndPort]; - if (valueNotifier != null && valueNotifier.value.getBody(channel.id) != null) { - var body = valueNotifier.value.getBody(channel.id); - body?.add(response); - return; - } + HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.host); + HeaderBody? headerBody = containerMap[hostAndPort]; + headerBody?.getBody(channel.id)?.add(response); } 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 implements EventListener { @@ -139,7 +121,6 @@ class _NetworkHomePagePageState extends State implements EventL @override void initState() { - print("initState"); super.initState(); domainWidget = DomainWidget(panel: panel); start(listener: this); @@ -156,7 +137,7 @@ class _NetworkHomePagePageState extends State implements EventL body: Row(children: [ SizedBox(width: 420, child: domainWidget), const Spacer(), - Expanded(flex: 100, child: Visibility(visible: true, child: domainWidget.panel)), + Expanded(flex: 100, child: domainWidget.panel), ])); } } diff --git a/lib/network/channel.dart b/lib/network/channel.dart index 336db11..162ebf7 100644 --- a/lib/network/channel.dart +++ b/lib/network/channel.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:basic_utils/basic_utils.dart'; import 'package:logger/logger.dart'; +import 'package:network/network/http/http.dart'; import 'package:network/network/util/AttributeKeys.dart'; import 'package:network/network/util/CertificateManager.dart'; import 'package:network/network/util/HostFilter.dart'; @@ -27,11 +28,11 @@ abstract class ChannelHandler { void channelRead(Channel channel, T msg) {} void channelInactive(Channel channel) { - log.i("close $channel"); + // log.i("close $channel"); } void exceptionCaught(Channel channel, Object cause, {StackTrace? trace}) { - HostAndPort? attribute = channel.getAttribute(AttributeKeys.HOST_KEY); + HostAndPort? attribute = channel.getAttribute(AttributeKeys.host); X509CertificateData? x509certificateFromPem; if (attribute != null && CertificateManager.get(attribute.host) != null) { String cer = CertificateManager.get(attribute.host)!; @@ -121,7 +122,12 @@ class ChannelPipeline extends ChannelHandler { void channelRead(Channel channel, Uint8List msg) { try { var data = _decoder.decode(msg); - if (data != null) _handler.channelRead(channel, data!); + if (data != null) { + if (data is HttpResponse) { + data.remoteAddress = '${channel.remoteAddress.host}:${channel.remotePort}'; + } + _handler.channelRead(channel, data!); + } } catch (error, trace) { exceptionCaught(channel, error, trace: trace); } @@ -248,7 +254,7 @@ class Network { } _onEvent(Uint8List data, Channel channel) async { - HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.HOST_KEY); + HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.host); //ssl握手 if (hostAndPort != null && hostAndPort.isSsl()) { try { diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 979a82d..079c631 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -1,11 +1,6 @@ -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; - import 'package:network/network/http/http.dart'; import 'package:network/network/http/http_headers.dart'; import 'package:network/network/util/AttributeKeys.dart'; -import 'package:network/network/util/HostFilter.dart'; import 'package:network/network/util/logger.dart'; import 'channel.dart'; @@ -56,16 +51,17 @@ class HttpChannelHandler extends ChannelHandler { /// 转发请求 Future forward(Channel channel, HttpRequest httpRequest) async { + channel.putAttribute(AttributeKeys.request, httpRequest); + var remoteChannel = await _getRemoteChannel(channel, httpRequest); //实现抓包代理转发 if (httpRequest.method != HttpMethod.connect) { - if (channel.getAttribute(AttributeKeys.HOST_KEY) == null) { - remoteChannel.putAttribute(AttributeKeys.URI_KEY, httpRequest.uri); + if (channel.getAttribute(AttributeKeys.host) == null) { + remoteChannel.putAttribute(AttributeKeys.uri, httpRequest.uri); } else { - remoteChannel.putAttribute( - AttributeKeys.URI_KEY, '${channel.getAttribute(AttributeKeys.HOST_KEY)}${httpRequest.uri}'); + remoteChannel.putAttribute(AttributeKeys.uri, '${channel.getAttribute(AttributeKeys.host)}${httpRequest.uri}'); } - log.i("[${channel.id}] ${remoteChannel.getAttribute(AttributeKeys.URI_KEY)}"); + // log.i("[${channel.id}] ${remoteChannel.getAttribute(AttributeKeys.uri)}"); listener?.onRequest(channel, httpRequest); //实现抓包代理转发 await remoteChannel.write(httpRequest); @@ -82,7 +78,7 @@ class HttpChannelHandler extends ChannelHandler { } var hostAndPort = getHostAndPort(httpRequest); - clientChannel.putAttribute(AttributeKeys.HOST_KEY, hostAndPort); + clientChannel.putAttribute(AttributeKeys.host, hostAndPort); var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener); var proxyChannel = await HttpClients.connect(hostAndPort, proxyHandler); @@ -101,19 +97,14 @@ class HttpChannelHandler extends ChannelHandler { class HttpResponseProxyHandler extends ChannelHandler { final Channel clientChannel; - /// 排除的后缀 不打印日志 - final Set excludeContent = HashSet.from(["javascript", "text/css", "application/font-woff", "image"]); - 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 ?? [])}"); - } + msg.request = clientChannel.getAttribute(AttributeKeys.request); + // log.i("[${clientChannel.id}] Response ${ String.fromCharCodes(msg.body ?? [])}"); listener?.onResponse(clientChannel, msg); //发送给客户端 clientChannel.write(msg); diff --git a/lib/network/http/codec.dart b/lib/network/http/codec.dart index 81a2482..027fe60 100644 --- a/lib/network/http/codec.dart +++ b/lib/network/http/codec.dart @@ -60,11 +60,6 @@ abstract class HttpCodec implements Codec { _state = message.headers.isChunked ? State.readVariableLengthContent : State.readFixedLengthContent; } - if (message is HttpRequest) { - if ([HttpMethod.get, HttpMethod.connect].contains((message as HttpRequest).method)) { - _state = State.done; - } - } //chunked编码 if (_state == State.readChunkedContent) { _bodyBuffer.add(data.sublist(0, min(_chunkReadableSize, data.length))); @@ -100,14 +95,16 @@ abstract class HttpCodec implements Codec { //请求行 initialLine(builder, message); + List? body = message.body; if (message.headers.isGzip) { - message.body = gzipEncode(message.body!); + body = gzipEncode(body!); } //请求头 message.headers.remove(HttpHeaders.TRANSFER_ENCODING); - int contentLength = _contentLength(message); - message.headers.contentLength = contentLength; + if (body != null && body.isNotEmpty) { + message.headers.contentLength = body.length; + } message.headers.forEach((key, value) { builder ..add(key.codeUnits) @@ -121,14 +118,10 @@ abstract class HttpCodec implements Codec { builder.addByte(HttpConstants.lf); //请求体 - builder.add(message.body ?? Uint8List(0)); + builder.add(body ?? Uint8List(0)); return builder.toBytes(); } - int _contentLength(T message) { - return message.body?.length ?? 0; - } - //读取起始行 List _readInitialLine(Uint8List data) { _httpParse.reset(); @@ -151,7 +144,7 @@ abstract class HttpCodec implements Codec { continue; } int length = hexToInt(String.fromCharCodes(parseLine)); - //chunked编码结束 + //chunked编码结束 最后以length = 0 结束 if (length == 0) { _state = State.done; return; diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index de56307..6dc4749 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -1,38 +1,54 @@ import 'dart:convert'; -import 'dart:io'; import 'http_headers.dart'; ///定义HTTP消息的接口,为HttpRequest和HttpResponse提供公共属性。 abstract class HttpMessage { + ///内容类型 + static final Map contentTypes = { + "javascript": ContentType.js, + "text/css": ContentType.css, + "font-woff": ContentType.font, + "text/html": ContentType.html, + "text/plain": ContentType.text, + "image": ContentType.image, + "application/json": ContentType.json + }; + final String protocolVersion; final HttpHeaders headers = HttpHeaders(); int contentLength = -1; List? body; + String? remoteAddress; HttpMessage(this.protocolVersion); + ContentType get contentType => contentTypes.entries + .firstWhere((element) => headers.contentType.contains(element.key), + orElse: () => const MapEntry("unknown", ContentType.http)) + .value; + String get bodyAsString { if (body == null || body?.isEmpty == true) { return ""; } try { - if (headers.isGzip) { - return utf8.decode(gzip.decode(body!)); - } return utf8.decode(body!); } catch (e) { return String.fromCharCodes(body!); } } + + String get cookie => headers.cookie; } ///HTTP请求。 class HttpRequest extends HttpMessage { final String uri; late HttpMethod method; + final DateTime requestTime = DateTime.now(); HttpRequest(this.method, this.uri, String protocolVersion) : super(protocolVersion); @@ -42,12 +58,23 @@ class HttpRequest extends HttpMessage { } } +enum ContentType { json, js, html, text, css, font, image, http } + ///HTTP响应。 class HttpResponse extends HttpMessage { final HttpStatus status; + final DateTime responseTime = DateTime.now(); + HttpRequest? request; HttpResponse(String protocolVersion, this.status) : super(protocolVersion); + String costTime() { + if (request == null) { + return ''; + } + return '${responseTime.difference(request!.requestTime).inMilliseconds}ms'; + } + @override String toString() { return 'HttpResponse{status: ${status.code}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart index 0f115b9..ab192bc 100644 --- a/lib/network/http/http_headers.dart +++ b/lib/network/http/http_headers.dart @@ -6,6 +6,7 @@ class HttpHeaders { static const CONTENT_TYPE = "Content-Type"; static const String HOST = "Host"; static const String TRANSFER_ENCODING = "Transfer-Encoding"; + static const String Cookie = "Cookie"; final LinkedHashMap _headers = LinkedHashMap(); @@ -51,6 +52,7 @@ class HttpHeaders { bool get isGzip => get(HttpHeaders.CONTENT_ENCODING) == "gzip"; bool get isChunked => get(HttpHeaders.TRANSFER_ENCODING) == "chunked"; + String get cookie => get(Cookie) ?? ""; void forEach(void Function(String name, String value) f) { _originalHeaderNames.forEach(f); diff --git a/lib/network/util/AttributeKeys.dart b/lib/network/util/AttributeKeys.dart index 8e15c80..68e0a60 100644 --- a/lib/network/util/AttributeKeys.dart +++ b/lib/network/util/AttributeKeys.dart @@ -1,7 +1,7 @@ /// @author wanghongen /// 2023/5/23 interface class AttributeKeys { - static String HOST_KEY = "HOST"; - static String URI_KEY = "URI"; - static String REQUEST_KEY = "REQUEST"; + static const String host = "HOST"; + static const String uri= "URI"; + static const String request= "REQUEST"; } diff --git a/lib/ui/left.dart b/lib/ui/left.dart index c8f7820..0b227e7 100644 --- a/lib/ui/left.dart +++ b/lib/ui/left.dart @@ -1,37 +1,34 @@ import 'dart:collection'; +import 'package:date_format/date_format.dart'; import 'package:flutter/material.dart'; import 'package:network/network/http/http.dart'; import 'package:network/ui/panel.dart'; +import '../utils/lang.dart'; + ///标题和内容布局 标题是域名 内容是域名下请求 class HeaderBody extends StatefulWidget { final Map map = HashMap(); final String header; - final Queue _body = Queue(); + final Queue _body = Queue(); final bool selected; - HeaderBody(this.header, {Key? key, this.selected = false}) : super(key: key); + HeaderBody(this.header, {this.selected = false}) : super(key: GlobalKey<_HeaderBodyState>()); ///添加请求 void addBody(String key, RowURI widget) { _body.addFirst(widget); map[key] = widget; + var state = super.key as GlobalKey<_HeaderBodyState>; + state.currentState?.changeState(); } 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 createState() { return _HeaderBodyState(); @@ -47,11 +44,15 @@ class _HeaderBodyState extends State { selected = widget.selected; } + changeState() { + setState(() {}); + } + @override Widget build(BuildContext context) { return Column(children: [ _hostWidget(widget.header), - Visibility(visible: selected, child: Column(children: widget._body.toList())) + Offstage(offstage: !selected, child: Column(children: widget._body.toList())) ]); } @@ -71,42 +72,85 @@ class _HeaderBodyState extends State { } } +Widget show(Widget child) { + return AnimatedOpacity(opacity: 1.0, duration: const Duration(seconds: 3), child: child); +} + class RowURI extends StatefulWidget { final Color? color; final HttpRequest request; - HttpResponse? response; + final ValueWrap response = ValueWrap(); + final NetworkTabController panel; - final _RowURIState _state = _RowURIState(); - - RowURI(this.request, this.panel, {Key? key, this.color = Colors.green}) : super(key: key); + RowURI(this.request, this.panel, {Key? key, this.color = Colors.green}) : super(key: GlobalKey<_RowURIState>()); @override - State createState() => _state; + State createState() => _RowURIState(); void add(HttpResponse response) { - this.response = response; + this.response.set(response); + var state = key as GlobalKey<_RowURIState>; + state.currentState?.changeState(); } } class _RowURIState extends State { + static _RowURIState? selectedState; + bool selected = false; @override Widget build(BuildContext context) { var request = widget.request; - var leading = widget.response == null ? Icons.http : Icons.http; + var response = widget.response.get(); var title = '${request.method.name} ${Uri.parse(request.uri).path}'; - + var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]); return ListTile( - leading: Icon(leading, size: 15, color: widget.color), + leading: Icon(getIcon(), size: 16, color: widget.color), title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1), - trailing: const Icon(Icons.chevron_right), + subtitle: Text( + '$time - [${response?.status.code ?? ''}] ${response?.contentType.name ?? ''} ${response?.costTime() ?? ''} '), + selected: selected, + // 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); - }); + onTap: onClick); + } + + void changeState() { + setState(() {}); + } + + void onClick() { + if (selected) { + return; + } + setState(() { + selected = true; + }); + if (selectedState?.mounted == true && selectedState != this) { + selectedState?.setState(() { + selectedState?.selected = false; + }); + } + selectedState = this; + widget.panel.change(widget.request, widget.response.get()); + } + + IconData getIcon() { + var map = { + ContentType.json: Icons.data_object, + ContentType.html: Icons.html, + ContentType.js: Icons.javascript, + ContentType.image: Icons.image, + ContentType.text: Icons.text_fields, + ContentType.css: Icons.css + }; + if (widget.response.isNull()) { + return Icons.http; + } + var contentType = widget.response.get()?.contentType; + return map[contentType] ?? Icons.http; } } diff --git a/lib/ui/panel.dart b/lib/ui/panel.dart index 8cc5cb5..7816027 100644 --- a/lib/ui/panel.dart +++ b/lib/ui/panel.dart @@ -1,5 +1,8 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:network/network/http/http.dart'; +import 'package:network/utils/lang.dart'; class NetworkTabController extends StatefulWidget { final tabs = [ @@ -9,29 +12,34 @@ class NetworkTabController extends StatefulWidget { const Tab(child: Text('Cookies', style: TextStyle(fontSize: 18))), ]; - final _NetworkTabState _state = _NetworkTabState(); - HttpRequest? request; - HttpResponse? response; + final ValueWrap request = ValueWrap(); + final ValueWrap response = ValueWrap(); - NetworkTabController({super.key}); + NetworkTabController() : super(key: GlobalKey<_NetworkTabState>()); void change(HttpRequest request, HttpResponse? response) { - _state.setState(() { - this.request = request; - this.response = response; - }); + this.request.set(request); + if (response != null) { + this.response.set(response); + } + var state = key as GlobalKey<_NetworkTabState>; + state.currentState?.changeState(); } @override State createState() { - return _state; + return _NetworkTabState(); } } class _NetworkTabState extends State { + void changeState() { + setState(() {}); + } + @override Widget build(BuildContext context) { - if (widget.request == null) { + if (widget.request.isNull()) { return const SizedBox(); } @@ -44,33 +52,90 @@ class _NetworkTabState extends State { general(), request(), response(), - ListView(children: const [Text("Cookies")]) + const Center(child: 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() ?? '')) + var request = widget.request.get(); + var response = widget.response.get(); + + return ListView(children: [ + 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: 2, child: SelectableText("Request URL:")), + Expanded(flex: 4, child: SelectableText(request?.uri ?? '')) + ]), + const SizedBox(height: 20), + Row(children: [ + const Expanded(flex: 2, child: SelectableText("Request Method:")), + Expanded(flex: 4, child: SelectableText(request?.method.name ?? '')) + ]), + const SizedBox(height: 20), + Row(children: [ + const Expanded(flex: 2, child: SelectableText("Status Code:")), + Expanded(flex: 4, child: SelectableText(response?.status.code.toString() ?? '')) + ]), + const SizedBox(height: 20), + Row(children: [ + const Expanded(flex: 2, child: SelectableText("Remote Address:")), + Expanded(flex: 4, child: SelectableText(response?.remoteAddress ?? '')) + ]), + const SizedBox(height: 20), + Row(children: [ + const Expanded(flex: 2, child: SelectableText("Request Time:")), + Expanded(flex: 4, child: SelectableText(request?.requestTime.toString() ?? '')) + ]), + const SizedBox(height: 20), + Row(children: [ + const Expanded(flex: 2, child: SelectableText("Cost Time:")), + Expanded(flex: 4, child: SelectableText(response?.costTime() ?? '')) + ]), + const SizedBox(height: 20), + Row(children: [ + const Expanded(flex: 2, child: SelectableText("Request Content-Type:")), + Expanded(flex: 4, child: SelectableText(request?.headers.contentType ?? '')) + ]), + const SizedBox(height: 20), + Row(children: [ + const Expanded(flex: 2, child: SelectableText("Response Content-Type:")), + Expanded(flex: 4, child: SelectableText(response?.headers.contentType ?? '')) + ]), + const SizedBox(height: 20) ]) - ]); + ]); } Widget request() { + return message(widget.request.get(), "Request"); + } + + Widget response() { + return message(widget.response.get(), "Response"); + } + + Widget cookie() { + var requestCookie = widget.request.get()?.cookie.split(";").map((e) => e.split("=")).map((e) => Row( + children: [Expanded(flex: 2, child: SelectableText(e.elementAt(0))), Expanded(flex: 4, child: SelectableText(e.elementAt(1)))])); + var responseCookie = widget.response.get()?.cookie.split(";").map((element) => element.split("=")).map((e) => Row( + children: [Expanded(flex: 2, child: SelectableText(e[0])), Expanded(flex: 4, child: SelectableText(e[1]))])); + return ListView(children: [ + expansionTile("RequestCookie", requestCookie?.toList() ?? []), + const Divider(), + const SizedBox(height: 20), + expansionTile("ResponseCookie", responseCookie?.toList() ?? []), + ]); + } + + Widget message(HttpMessage? message, String type) { var headers = []; - widget.request?.headers.forEach((name, value) { + message?.headers.forEach((name, value) { headers.add(Row(children: [ Expanded(flex: 2, child: SelectableText('$name:')), Expanded(flex: 4, child: SelectableText(value)), @@ -78,59 +143,40 @@ class _NetworkTabState extends State { ])); }); - 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))) - ]); - } + Widget? bodyWidgets = message == null ? null : getBody(type, message); return ListView(children: [ ExpansionTile( - title: const Text("Request Headers", style: TextStyle(fontWeight: FontWeight.bold)), + title: Text("$type Headers", style: const TextStyle(fontWeight: FontWeight.bold)), initiallyExpanded: true, shape: const Border(), - childrenPadding: const EdgeInsets.all(20), + childrenPadding: const EdgeInsets.only(left: 20, bottom: 20), children: headers), const Divider(), bodyWidgets ?? const SizedBox() ]); } - Widget response() { - var headers = []; - 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), - ])); - }); + Widget expansionTile(String title, List content) { + return ExpansionTile( + title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + expandedAlignment: Alignment.topLeft, + childrenPadding: const EdgeInsets.only(left: 20), + initiallyExpanded: true, + shape: const Border(), + children: content); + } - 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)) - ) - ]) - ]); + Widget? getBody(String type, HttpMessage message) { + if (message.body?.isNotEmpty == true) { + if (message.contentType == ContentType.image) { + return expansionTile("$type Body", + [Image.memory(Uint8List.fromList(message.body ?? []), fit: BoxFit.cover, width: 200, height: 200)]); + } else { + return expansionTile("$type Body", + [SelectableText.rich(TextSpan(text: message.bodyAsString, style: const TextStyle(color: Colors.black)))]); + } + } + return null; } } diff --git a/lib/utils/lang.dart b/lib/utils/lang.dart new file mode 100644 index 0000000..949c143 --- /dev/null +++ b/lib/utils/lang.dart @@ -0,0 +1,9 @@ +class ValueWrap { + V? _v; + + void set(V v) => this._v = v; + + V? get() => this._v; + + bool isNull() => this._v == null; +} diff --git a/pubspec.lock b/pubspec.lock index 972c6f6..f83c9d5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - asn1lib: - dependency: transitive - description: - name: asn1lib - sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 - url: "https://pub.dev" - source: hosted - version: "1.4.0" async: dependency: transitive description: @@ -21,10 +13,10 @@ packages: dependency: "direct main" description: name: basic_utils - sha256: "8815477fcf58499e42326bd858e391442425fa57db9a45e48e15224c62049262" + sha256: "5748b8a2e810bba86da623940ac5c39874760a8f7cf02e62b1787a26f42f33bf" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.6.0" boolean_selector: dependency: transitive description: @@ -65,14 +57,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" - crypto_keys: - dependency: transitive - description: - name: crypto_keys - sha256: acc19abf34623d990a0e8aec69463d74a824c31f137128f42e2810befc509ad0 - url: "https://pub.dev" - source: hosted - version: "0.3.0+1" cupertino_icons: dependency: "direct main" description: @@ -81,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + date_format: + dependency: "direct main" + description: + name: date_format + sha256: "8e5154ca363411847220c8cbc43afcf69c08e8debe40ba09d57710c25711760c" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -111,10 +103,10 @@ packages: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: "4c3f04bfb64d3efd508d06b41b825542f08122d30bda4933fb95c069d22a4fa3" url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.0.0" http_parser: dependency: transitive description: @@ -143,18 +135,18 @@ packages: dependency: transitive description: name: lints - sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" logger: dependency: "direct main" description: name: logger - sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7 + sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" logging: dependency: transitive description: @@ -203,14 +195,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.3" - quiver: - dependency: transitive - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.dev" - source: hosted - version: "3.2.1" sky_engine: dependency: transitive description: flutter @@ -280,13 +264,5 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - x509b: - dependency: "direct main" - description: - name: x509b - sha256: bc306b48b23825fcfa4946b34ffd2a9aae9f3f4b3a128c717c40994e35939622 - url: "https://pub.dev" - source: hosted - version: "0.2.5" sdks: dart: ">=3.0.2 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3d46317..d4c7e6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,9 +11,10 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 - basic_utils: ^5.5.4 - logger: ^1.3.0 - x509b: ^0.2.5 + basic_utils: ^5.6.0 + logger: ^1.4.0 + date_format: ^2.0.7 + dev_dependencies: flutter_test: sdk: flutter @@ -23,7 +24,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^2.0.1 # The following section is specific to Flutter packages. flutter: