Adapt request body viewer for different MimeTypes

This commit is contained in:
Ankit Mahato
2023-03-16 16:57:34 +05:30
parent b54275dbc3
commit ccef9c0162
5 changed files with 251 additions and 69 deletions

View File

@ -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<ResponseBodyView, IconData> 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<String, Map<String, List<ResponseBodyView>>>
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<String, String> 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.";

View File

@ -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<String, String>? headers;
final Map<String, String>? 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,
);
}

View File

@ -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<ResponsePane> {
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),
),
],
),
);
}
}

View File

@ -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<ResponseBody> {
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<BodySuccess> createState() => _BodySuccessState();
}
class _BodySuccessState extends State<BodySuccess> {
@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<ResponseBodyView> options;
@override
State<ResponseBodyViewSelector> createState() =>
_ResponseBodyViewSelectorState();
}
class _ResponseBodyViewSelectorState extends State<ResponseBodyViewSelector> {
late ResponseBodyView value;
@override
void initState() {
super.initState();
value = widget.options[0];
}
@override
Widget build(BuildContext context) {
return SegmentedButton<ResponseBodyView>(
segments: widget.options
.map<ButtonSegment<ResponseBodyView>>(
(e) => ButtonSegment<ResponseBodyView>(
value: e,
label: Text(capitalizeFirstLetter(e.name)),
icon: Icon(kResponseBodyViewIcons[e]),
),
)
.toList(),
selected: {value},
onSelectionChanged: (newSelection) {
setState(() {
value = newSelection.first;
});
},
);
}
}

View File

@ -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<ResponseBodyView>, 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);
}
}