From ccef9c0162a8bf2b2d6382d5bbda4ece195e7041 Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Thu, 16 Mar 2023 16:57:34 +0530 Subject: [PATCH] Adapt request body viewer for different MimeTypes --- lib/consts.dart | 83 +++++++++- lib/models/request_model.dart | 16 +- .../response_pane/response_pane.dart | 31 +--- .../response_tabs/response_body.dart | 150 +++++++++++++----- lib/utils/utils.dart | 40 +++++ 5 files changed, 251 insertions(+), 69 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 9fe36410..d5d37dfd 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -38,6 +38,7 @@ const kVSpacer10 = SizedBox(height: 10); const kTabAnimationDuration = Duration(milliseconds: 200); const kTabHeight = 45.0; const kHeaderHeight = 32.0; +const kSegmentHeight = 24.0; const kRandMax = 100000; @@ -71,7 +72,81 @@ const kDefaultHttpMethod = HTTPVerb.get; const kDefaultContentType = ContentType.json; const JsonEncoder encoder = JsonEncoder.withIndent(' '); -const kJsonMimeType = 'application/json'; + +const kTypeApplication = 'application'; +const kTypeImage = 'image'; +const kTypeAudio = 'audio'; +const kTypeVideo = 'video'; +const kTypeText = 'text'; + +// application +const kSubTypeJson = 'json'; +const kSubTypePdf = 'pdf'; +const kSubTypeXml = 'xml'; // also text + +// text +const kSubTypeCss = 'css'; +const kSubTypeCsv = 'csv'; +const kSubTypeHtml = 'html'; +const kSubTypeJavascript = 'javascript'; +const kSubTypeMarkdown = 'markdown'; +const kSubTypePlain = 'plain'; + +//image +const kSubTypeSvg = 'svg+xml'; + +const kSubTypeDefaultViewOptions = 'all'; + +enum ResponseBodyView { preview, code, raw } + +const Map kResponseBodyViewIcons = { + ResponseBodyView.preview: Icons.visibility_rounded, + ResponseBodyView.code: Icons.code_rounded, + ResponseBodyView.raw: Icons.text_snippet_rounded +}; + +const kDefaultBodyViewOptions = [ResponseBodyView.raw]; +const kCodeRawBodyViewOptions = [ResponseBodyView.code, ResponseBodyView.raw]; +const kPreviewRawBodyViewOptions = [ + ResponseBodyView.preview, + ResponseBodyView.raw +]; +const kPreviewCodeRawBodyViewOptions = [ + ResponseBodyView.preview, + ResponseBodyView.code, + ResponseBodyView.raw +]; + +const Map>> + kResponseBodyViewOptions = { + kTypeApplication: { + kSubTypeDefaultViewOptions: kDefaultBodyViewOptions, + kSubTypeJson: kCodeRawBodyViewOptions, + kSubTypePdf: kPreviewRawBodyViewOptions, + kSubTypeXml: kCodeRawBodyViewOptions, + }, + kTypeImage: { + kSubTypeDefaultViewOptions: kPreviewRawBodyViewOptions, + }, + kTypeAudio: { + kSubTypeDefaultViewOptions: kPreviewRawBodyViewOptions, + }, + kTypeVideo: { + kSubTypeDefaultViewOptions: kPreviewRawBodyViewOptions, + }, + kTypeText: { + kSubTypeDefaultViewOptions: kDefaultBodyViewOptions, + kSubTypeCss: kCodeRawBodyViewOptions, + kSubTypeHtml: kCodeRawBodyViewOptions, + kSubTypeJavascript: kCodeRawBodyViewOptions, + kSubTypeXml: kCodeRawBodyViewOptions, + kSubTypeMarkdown: kCodeRawBodyViewOptions, + }, +}; + +const Map kCodeHighlighterMap = { + kSubTypeHtml: "xml", +}; const sendingIndicator = AssetImage("assets/sending.gif"); @@ -143,3 +218,9 @@ const kResponseCodeReasons = { 510: 'Not Extended', 511: 'Network Authentication Required', }; + +const kMimeTypeRaiseIssue = + " is currently not supported.\nPlease raise an issue in API Dash GitHub repo - https://github.com/foss42/api-dash so that we can prioritize adding it to the tool."; + +const kRaiseIssue = + "\nIf the behaviour is unexpected, please raise an issue in API Dash GitHub repo - https://github.com/foss42/api-dash so that we can resolve it."; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 95623c0e..d56232ec 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:http_parser/http_parser.dart'; import 'kvrow_model.dart'; import '../consts.dart'; @@ -102,7 +104,9 @@ class ResponseModel { this.headers, this.requestHeaders, this.contentType, + this.mediaType, this.body, + this.bodyBytes, this.time, }); @@ -110,7 +114,9 @@ class ResponseModel { final Map? headers; final Map? requestHeaders; final String? contentType; + final MediaType? mediaType; final String? body; + final Uint8List? bodyBytes; final Duration? time; ResponseModel fromResponse({ @@ -118,6 +124,12 @@ class ResponseModel { Duration? time, }) { var contentType = response.headers[HttpHeaders.contentTypeHeader]; + MediaType? mediaType; + try { + mediaType = MediaType.parse(contentType!); + } catch (e) { + mediaType = null; + } final responseHeaders = mergeMaps( {HttpHeaders.contentLengthHeader: response.contentLength.toString()}, response.headers); @@ -126,9 +138,11 @@ class ResponseModel { headers: responseHeaders, requestHeaders: response.request?.headers, contentType: contentType, - body: contentType == kJsonMimeType + mediaType: mediaType, + body: (mediaType?.subtype == kSubTypeJson) ? utf8.decode(response.bodyBytes) : response.body, + bodyBytes: response.bodyBytes, time: time, ); } diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane/response_pane.dart index e4228ad2..cddd1187 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane/response_pane.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import 'response_details.dart'; @@ -32,7 +33,7 @@ class _ResponsePaneState extends ConsumerState { return const NotSentWidget(); } if (responseStatus == -1) { - return ErrorMessage(message: message); + return ErrorMessage(message: '$message. $kRaiseIssue'); } return const ResponseDetails(); } @@ -95,31 +96,3 @@ class SendingWidget extends StatelessWidget { ); } } - -class ErrorMessage extends StatelessWidget { - const ErrorMessage({super.key, required this.message}); - - final String? message; - - @override - Widget build(BuildContext context) { - final color = Theme.of(context).colorScheme.secondary; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.warning_rounded, - size: 40, - color: color, - ), - Text( - message ?? 'And error occurred.', - style: - Theme.of(context).textTheme.titleMedium?.copyWith(color: color), - ), - ], - ), - ); - } -} diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane/response_tabs/response_body.dart b/lib/screens/home_page/editor_pane/details_card/response_pane/response_tabs/response_body.dart index 65cd9699..59a1e6ad 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane/response_tabs/response_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane/response_tabs/response_body.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_json_view/flutter_json_view.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:apidash/providers/providers.dart'; -import 'package:apidash/widgets/jsonview.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; class ResponseBody extends ConsumerStatefulWidget { @@ -25,49 +26,122 @@ class _ResponseBodyState extends ConsumerState { final collection = ref.watch(collectionStateNotifierProvider); final idIdx = collection.indexWhere((m) => m.id == activeId); final responseModel = collection[idIdx].responseModel; - var body = responseModel?.body ?? ''; - var contentType = responseModel?.contentType ?? ""; + var mediaType = responseModel?.mediaType; + if (responseModel == null) { + return const ErrorMessage( + message: 'Error: No Response Data Found. $kRaiseIssue'); + } + if (mediaType == null) { + return ErrorMessage( + message: + 'Unknown Response content type - ${responseModel.contentType}. $kRaiseIssue'); + } + return BodySuccess( + mediaType: mediaType, + responseModel: responseModel, + ); + } +} + +class BodySuccess extends StatefulWidget { + const BodySuccess( + {super.key, required this.mediaType, required this.responseModel}); + final MediaType mediaType; + final ResponseModel responseModel; + @override + State createState() => _BodySuccessState(); +} + +class _BodySuccessState extends State { + @override + Widget build(BuildContext context) { + String? body = widget.responseModel.body; + if (body == null) { + return Padding( + padding: kP5, + child: Text( + '(empty)', + style: kCodeStyle, + ), + ); + } + var bytes = widget.responseModel.bodyBytes!; + var responseBodyView = getResponseBodyViewOptions(widget.mediaType); + print(responseBodyView); return Padding( - padding: kP10, + padding: kPh20v5, child: SingleChildScrollView( child: Column( children: [ - kVSpacer5, - Row( - children: [ - Expanded( - child: Text( - "Body ${body.isEmpty ? '(empty)' : ''}", - style: kCodeStyle, - ), - ), - if (body.isNotEmpty) - TextButton( - onPressed: () async { - await Clipboard.setData(ClipboardData(text: body)); - }, - child: Row( - children: const [ - Icon( - Icons.content_copy, - size: 20, - ), - Text("Copy") - ], - ), - ), - ], - ), - kVSpacer5, - if (body.isNotEmpty && contentType.startsWith(kJsonMimeType)) - JsonView.string( - body, - theme: jsonViewTheme, + SizedBox( + height: kHeaderHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + responseBodyView.$0 == kDefaultBodyViewOptions + ? const SizedBox() + : ResponseBodyViewSelector(options: responseBodyView.$0), + CopyButton(toCopy: body), + ], ), - if (body.isNotEmpty && contentType.startsWith("text/")) Text(body), + ), + if (responseBodyView.$0.contains(ResponseBodyView.preview)) + Previewer( + bytes: bytes, + type: widget.mediaType.type, + subtype: widget.mediaType.subtype, + ), + if (responseBodyView.$0.contains(ResponseBodyView.code)) + CodeHighlight( + input: body, + language: responseBodyView.$1, + textStyle: kCodeStyle, + ), + if (responseBodyView.$0.contains(ResponseBodyView.raw)) + SelectableText(body), ], ), ), ); } } + +class ResponseBodyViewSelector extends StatefulWidget { + const ResponseBodyViewSelector({super.key, required this.options}); + + final List options; + @override + State createState() => + _ResponseBodyViewSelectorState(); +} + +class _ResponseBodyViewSelectorState extends State { + late ResponseBodyView value; + + @override + void initState() { + super.initState(); + value = widget.options[0]; + } + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: widget.options + .map>( + (e) => ButtonSegment( + value: e, + label: Text(capitalizeFirstLetter(e.name)), + icon: Icon(kResponseBodyViewIcons[e]), + ), + ) + .toList(), + selected: {value}, + onSelectionChanged: (newSelection) { + setState(() { + value = newSelection.first; + }); + }, + ); + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 9586611c..5f37e9fd 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:http_parser/http_parser.dart'; import '../consts.dart'; Color getResponseStatusCodeColor(int? statusCode, @@ -85,3 +86,42 @@ String humanizeDuration(Duration? duration) { return "$mili ms"; } } + +String capitalizeFirstLetter(String? text) { + if (text == null || text == "") { + return ""; + } else if (text.length == 1) { + return text.toUpperCase(); + } else { + var first = text[0]; + var rest = text.substring(1); + return first.toUpperCase() + rest; + } +} + +String formatHeaderCase(String text) { + var sp = text.split("-"); + sp = sp.map((e) => capitalizeFirstLetter(e)).toList(); + return sp.join("-"); +} + +(List, String?) getResponseBodyViewOptions(MediaType mediaType){ + var type = mediaType.type; + var subtype = mediaType.subtype; + print(mediaType); + if(kResponseBodyViewOptions.containsKey(type)){ + if(subtype.contains(kSubTypeJson)){ + subtype = kSubTypeJson; + } + if(subtype.contains(kSubTypeXml)){ + subtype = kSubTypeXml; + } + if (kResponseBodyViewOptions[type]!.containsKey(subtype)){ + return (kResponseBodyViewOptions[type]![subtype]!, kCodeHighlighterMap[subtype] ?? subtype); + } + return (kResponseBodyViewOptions[type]![kSubTypeDefaultViewOptions]!, subtype); + } + else { + return (kDefaultBodyViewOptions, null); + } +} \ No newline at end of file