mirror of
https://github.com/foss42/apidash.git
synced 2025-06-03 16:27:06 +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 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.";
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user