mirror of
https://github.com/foss42/apidash.git
synced 2025-09-22 21:23:22 +08:00
Adapt request body viewer for different MimeTypes
This commit is contained in:
@ -38,6 +38,7 @@ const kVSpacer10 = SizedBox(height: 10);
|
|||||||
const kTabAnimationDuration = Duration(milliseconds: 200);
|
const kTabAnimationDuration = Duration(milliseconds: 200);
|
||||||
const kTabHeight = 45.0;
|
const kTabHeight = 45.0;
|
||||||
const kHeaderHeight = 32.0;
|
const kHeaderHeight = 32.0;
|
||||||
|
const kSegmentHeight = 24.0;
|
||||||
|
|
||||||
const kRandMax = 100000;
|
const kRandMax = 100000;
|
||||||
|
|
||||||
@ -71,7 +72,81 @@ const kDefaultHttpMethod = HTTPVerb.get;
|
|||||||
const kDefaultContentType = ContentType.json;
|
const kDefaultContentType = ContentType.json;
|
||||||
|
|
||||||
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
|
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");
|
const sendingIndicator = AssetImage("assets/sending.gif");
|
||||||
|
|
||||||
@ -143,3 +218,9 @@ const kResponseCodeReasons = {
|
|||||||
510: 'Not Extended',
|
510: 'Not Extended',
|
||||||
511: 'Network Authentication Required',
|
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.";
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'kvrow_model.dart';
|
import 'kvrow_model.dart';
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
|
|
||||||
@ -102,7 +104,9 @@ class ResponseModel {
|
|||||||
this.headers,
|
this.headers,
|
||||||
this.requestHeaders,
|
this.requestHeaders,
|
||||||
this.contentType,
|
this.contentType,
|
||||||
|
this.mediaType,
|
||||||
this.body,
|
this.body,
|
||||||
|
this.bodyBytes,
|
||||||
this.time,
|
this.time,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,7 +114,9 @@ class ResponseModel {
|
|||||||
final Map<String, String>? headers;
|
final Map<String, String>? headers;
|
||||||
final Map<String, String>? requestHeaders;
|
final Map<String, String>? requestHeaders;
|
||||||
final String? contentType;
|
final String? contentType;
|
||||||
|
final MediaType? mediaType;
|
||||||
final String? body;
|
final String? body;
|
||||||
|
final Uint8List? bodyBytes;
|
||||||
final Duration? time;
|
final Duration? time;
|
||||||
|
|
||||||
ResponseModel fromResponse({
|
ResponseModel fromResponse({
|
||||||
@ -118,6 +124,12 @@ class ResponseModel {
|
|||||||
Duration? time,
|
Duration? time,
|
||||||
}) {
|
}) {
|
||||||
var contentType = response.headers[HttpHeaders.contentTypeHeader];
|
var contentType = response.headers[HttpHeaders.contentTypeHeader];
|
||||||
|
MediaType? mediaType;
|
||||||
|
try {
|
||||||
|
mediaType = MediaType.parse(contentType!);
|
||||||
|
} catch (e) {
|
||||||
|
mediaType = null;
|
||||||
|
}
|
||||||
final responseHeaders = mergeMaps(
|
final responseHeaders = mergeMaps(
|
||||||
{HttpHeaders.contentLengthHeader: response.contentLength.toString()},
|
{HttpHeaders.contentLengthHeader: response.contentLength.toString()},
|
||||||
response.headers);
|
response.headers);
|
||||||
@ -126,9 +138,11 @@ class ResponseModel {
|
|||||||
headers: responseHeaders,
|
headers: responseHeaders,
|
||||||
requestHeaders: response.request?.headers,
|
requestHeaders: response.request?.headers,
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
body: contentType == kJsonMimeType
|
mediaType: mediaType,
|
||||||
|
body: (mediaType?.subtype == kSubTypeJson)
|
||||||
? utf8.decode(response.bodyBytes)
|
? utf8.decode(response.bodyBytes)
|
||||||
: response.body,
|
: response.body,
|
||||||
|
bodyBytes: response.bodyBytes,
|
||||||
time: time,
|
time: time,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:apidash/providers/providers.dart';
|
import 'package:apidash/providers/providers.dart';
|
||||||
|
import 'package:apidash/widgets/widgets.dart';
|
||||||
import 'package:apidash/consts.dart';
|
import 'package:apidash/consts.dart';
|
||||||
import 'response_details.dart';
|
import 'response_details.dart';
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ class _ResponsePaneState extends ConsumerState<ResponsePane> {
|
|||||||
return const NotSentWidget();
|
return const NotSentWidget();
|
||||||
}
|
}
|
||||||
if (responseStatus == -1) {
|
if (responseStatus == -1) {
|
||||||
return ErrorMessage(message: message);
|
return ErrorMessage(message: '$message. $kRaiseIssue');
|
||||||
}
|
}
|
||||||
return const ResponseDetails();
|
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/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';
|
import 'package:apidash/consts.dart';
|
||||||
|
|
||||||
class ResponseBody extends ConsumerStatefulWidget {
|
class ResponseBody extends ConsumerStatefulWidget {
|
||||||
@ -25,49 +26,122 @@ class _ResponseBodyState extends ConsumerState<ResponseBody> {
|
|||||||
final collection = ref.watch(collectionStateNotifierProvider);
|
final collection = ref.watch(collectionStateNotifierProvider);
|
||||||
final idIdx = collection.indexWhere((m) => m.id == activeId);
|
final idIdx = collection.indexWhere((m) => m.id == activeId);
|
||||||
final responseModel = collection[idIdx].responseModel;
|
final responseModel = collection[idIdx].responseModel;
|
||||||
var body = responseModel?.body ?? '';
|
var mediaType = responseModel?.mediaType;
|
||||||
var contentType = responseModel?.contentType ?? "";
|
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(
|
return Padding(
|
||||||
padding: kP10,
|
padding: kPh20v5,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
kVSpacer5,
|
SizedBox(
|
||||||
Row(
|
height: kHeaderHeight,
|
||||||
children: [
|
child: Row(
|
||||||
Expanded(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
child: Text(
|
children: [
|
||||||
"Body ${body.isEmpty ? '(empty)' : ''}",
|
responseBodyView.$0 == kDefaultBodyViewOptions
|
||||||
style: kCodeStyle,
|
? const SizedBox()
|
||||||
),
|
: ResponseBodyViewSelector(options: responseBodyView.$0),
|
||||||
),
|
CopyButton(toCopy: body),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
|
|
||||||
Color getResponseStatusCodeColor(int? statusCode,
|
Color getResponseStatusCodeColor(int? statusCode,
|
||||||
@ -85,3 +86,42 @@ String humanizeDuration(Duration? duration) {
|
|||||||
return "$mili ms";
|
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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user