Better handling of response of various content-types

This commit is contained in:
Ankit Mahato
2023-03-16 21:03:57 +05:30
parent ccef9c0162
commit a14284e20c
4 changed files with 128 additions and 106 deletions

View File

@ -83,6 +83,7 @@ const kTypeText = 'text';
const kSubTypeJson = 'json'; const kSubTypeJson = 'json';
const kSubTypePdf = 'pdf'; const kSubTypePdf = 'pdf';
const kSubTypeXml = 'xml'; // also text const kSubTypeXml = 'xml'; // also text
const kSubTypeOctetStream = 'octet-stream';
// text // text
const kSubTypeCss = 'css'; const kSubTypeCss = 'css';
@ -97,7 +98,7 @@ const kSubTypeSvg = 'svg+xml';
const kSubTypeDefaultViewOptions = 'all'; const kSubTypeDefaultViewOptions = 'all';
enum ResponseBodyView { preview, code, raw } enum ResponseBodyView { preview, code, raw, none }
const Map<ResponseBodyView, IconData> kResponseBodyViewIcons = { const Map<ResponseBodyView, IconData> kResponseBodyViewIcons = {
ResponseBodyView.preview: Icons.visibility_rounded, ResponseBodyView.preview: Icons.visibility_rounded,
@ -105,11 +106,11 @@ const Map<ResponseBodyView, IconData> kResponseBodyViewIcons = {
ResponseBodyView.raw: Icons.text_snippet_rounded ResponseBodyView.raw: Icons.text_snippet_rounded
}; };
const kDefaultBodyViewOptions = [ResponseBodyView.raw]; const kNoBodyViewOptions = [ResponseBodyView.none];
const kRawBodyViewOptions = [ResponseBodyView.raw];
const kCodeRawBodyViewOptions = [ResponseBodyView.code, ResponseBodyView.raw]; const kCodeRawBodyViewOptions = [ResponseBodyView.code, ResponseBodyView.raw];
const kPreviewRawBodyViewOptions = [ const kPreviewBodyViewOptions = [
ResponseBodyView.preview, ResponseBodyView.preview,
ResponseBodyView.raw
]; ];
const kPreviewCodeRawBodyViewOptions = [ const kPreviewCodeRawBodyViewOptions = [
ResponseBodyView.preview, ResponseBodyView.preview,
@ -120,22 +121,22 @@ const kPreviewCodeRawBodyViewOptions = [
const Map<String, Map<String, List<ResponseBodyView>>> const Map<String, Map<String, List<ResponseBodyView>>>
kResponseBodyViewOptions = { kResponseBodyViewOptions = {
kTypeApplication: { kTypeApplication: {
kSubTypeDefaultViewOptions: kDefaultBodyViewOptions, kSubTypeDefaultViewOptions: kNoBodyViewOptions,
kSubTypeJson: kCodeRawBodyViewOptions, kSubTypeJson: kCodeRawBodyViewOptions,
kSubTypePdf: kPreviewRawBodyViewOptions, kSubTypePdf: kPreviewBodyViewOptions,
kSubTypeXml: kCodeRawBodyViewOptions, kSubTypeXml: kCodeRawBodyViewOptions,
}, },
kTypeImage: { kTypeImage: {
kSubTypeDefaultViewOptions: kPreviewRawBodyViewOptions, kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
}, },
kTypeAudio: { kTypeAudio: {
kSubTypeDefaultViewOptions: kPreviewRawBodyViewOptions, kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
}, },
kTypeVideo: { kTypeVideo: {
kSubTypeDefaultViewOptions: kPreviewRawBodyViewOptions, kSubTypeDefaultViewOptions: kPreviewBodyViewOptions,
}, },
kTypeText: { kTypeText: {
kSubTypeDefaultViewOptions: kDefaultBodyViewOptions, kSubTypeDefaultViewOptions: kRawBodyViewOptions,
kSubTypeCss: kCodeRawBodyViewOptions, kSubTypeCss: kCodeRawBodyViewOptions,
kSubTypeHtml: kCodeRawBodyViewOptions, kSubTypeHtml: kCodeRawBodyViewOptions,
kSubTypeJavascript: kCodeRawBodyViewOptions, kSubTypeJavascript: kCodeRawBodyViewOptions,

View File

@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart';
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:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:apidash/providers/providers.dart'; import 'package:apidash/providers/providers.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/utils/utils.dart'; import 'package:apidash/utils/utils.dart';
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
@ -27,6 +27,7 @@ class _ResponseBodyState extends ConsumerState<ResponseBody> {
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 mediaType = responseModel?.mediaType; var mediaType = responseModel?.mediaType;
var body = responseModel?.body;
if (responseModel == null) { if (responseModel == null) {
return const ErrorMessage( return const ErrorMessage(
message: 'Error: No Response Data Found. $kRaiseIssue'); message: 'Error: No Response Data Found. $kRaiseIssue');
@ -36,112 +37,129 @@ class _ResponseBodyState extends ConsumerState<ResponseBody> {
message: message:
'Unknown Response content type - ${responseModel.contentType}. $kRaiseIssue'); 'Unknown Response content type - ${responseModel.contentType}. $kRaiseIssue');
} }
if (body == null) {
return const ErrorMessage(
message: 'Response body is empty. $kRaiseIssue');
}
var responseBodyView = getResponseBodyViewOptions(mediaType);
print(responseBodyView);
var options = responseBodyView.$0;
var highlightLanguage = responseBodyView.$1;
if (options == kNoBodyViewOptions) {
return ErrorMessage(
message:
"Viewing response data of Content-Type\n'${mediaType.mimeType}' $kMimeTypeRaiseIssue");
}
return BodySuccess( return BodySuccess(
mediaType: mediaType, mediaType: mediaType,
responseModel: responseModel, options: options,
bytes: responseModel.bodyBytes!,
body: body,
highlightLanguage: highlightLanguage,
); );
} }
} }
class BodySuccess extends StatefulWidget { class BodySuccess extends StatefulWidget {
const BodySuccess( const BodySuccess(
{super.key, required this.mediaType, required this.responseModel}); {super.key,
required this.mediaType,
required this.body,
required this.options,
required this.bytes,
this.highlightLanguage});
final MediaType mediaType; final MediaType mediaType;
final ResponseModel responseModel; final List<ResponseBodyView> options;
final String body;
final Uint8List bytes;
final String? highlightLanguage;
@override @override
State<BodySuccess> createState() => _BodySuccessState(); State<BodySuccess> createState() => _BodySuccessState();
} }
class _BodySuccessState extends State<BodySuccess> { class _BodySuccessState extends State<BodySuccess> {
int segmentIdx = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String? body = widget.responseModel.body; var currentSeg = widget.options[segmentIdx];
if (body == null) {
return Padding( final textContainerdecoration = BoxDecoration(
padding: kP5, color: Theme.of(context).brightness == Brightness.dark
child: Text( ? Color.alphaBlend(
'(empty)', Theme.of(context).colorScheme.surface.withOpacity(0.8),
style: kCodeStyle, Colors.black)
), : Color.alphaBlend(
); Theme.of(context).colorScheme.surface.withOpacity(0.2),
} Colors.white),
var bytes = widget.responseModel.bodyBytes!; border: Border.all(color: Theme.of(context).colorScheme.surfaceVariant),
var responseBodyView = getResponseBodyViewOptions(widget.mediaType); borderRadius: kBorderRadius8,
print(responseBodyView); );
return Padding( return Padding(
padding: kPh20v5, padding: kP10,
child: SingleChildScrollView( child: Column(
child: Column( children: [
children: [ SizedBox(
SizedBox( height: kHeaderHeight,
height: kHeaderHeight, child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ (widget.options == kRawBodyViewOptions)
responseBodyView.$0 == kDefaultBodyViewOptions ? const SizedBox()
? const SizedBox() : SegmentedButton<ResponseBodyView>(
: ResponseBodyViewSelector(options: responseBodyView.$0), selectedIcon: Icon(kResponseBodyViewIcons[currentSeg]),
CopyButton(toCopy: body), segments: widget.options
], .map<ButtonSegment<ResponseBodyView>>(
), (e) => ButtonSegment<ResponseBodyView>(
value: e,
label: Text(capitalizeFirstLetter(e.name)),
icon: Icon(kResponseBodyViewIcons[e]),
),
)
.toList(),
selected: {currentSeg},
onSelectionChanged: (newSelection) {
setState(() {
segmentIdx =
widget.options.indexOf(newSelection.first);
});
},
),
kCodeRawBodyViewOptions.contains(currentSeg)
? CopyButton(toCopy: widget.body)
: const SizedBox(),
],
), ),
if (responseBodyView.$0.contains(ResponseBodyView.preview)) ),
Previewer( kVSpacer10,
bytes: bytes, Expanded(
type: widget.mediaType.type, child: currentSeg == ResponseBodyView.preview
subtype: widget.mediaType.subtype, ? Previewer(
), bytes: widget.bytes,
if (responseBodyView.$0.contains(ResponseBodyView.code)) type: widget.mediaType.type,
CodeHighlight( subtype: widget.mediaType.subtype,
input: body, )
language: responseBodyView.$1, : (currentSeg == ResponseBodyView.code
textStyle: kCodeStyle, ? //SizedBox()
), CodeHighlight(
if (responseBodyView.$0.contains(ResponseBodyView.raw)) input: widget.body,
SelectableText(body), language: widget.highlightLanguage,
], textStyle: kCodeStyle,
), )
: Container(
padding: kP8,
decoration: textContainerdecoration,
child: SingleChildScrollView(
child: SelectableText(
widget.body,
style: kCodeStyle,
),
),
)),
),
],
), ),
); );
} }
} }
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

@ -122,6 +122,6 @@ String formatHeaderCase(String text) {
return (kResponseBodyViewOptions[type]![kSubTypeDefaultViewOptions]!, subtype); return (kResponseBodyViewOptions[type]![kSubTypeDefaultViewOptions]!, subtype);
} }
else { else {
return (kDefaultBodyViewOptions, null); return (kNoBodyViewOptions, null);
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'error_message.dart';
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
class Previewer extends StatefulWidget { class Previewer extends StatefulWidget {
@ -20,24 +21,26 @@ class _PreviewerState extends State<Previewer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.type == kTypeApplication && widget.subtype == kSubTypePdf) { if (widget.type == kTypeApplication && widget.subtype == kSubTypePdf) {
return const SelectableText("PDF viewing $kMimeTypeRaiseIssue"); return const ErrorMessage(message: "PDF viewing $kMimeTypeRaiseIssue");
} }
if (widget.type == kTypeImage) { if (widget.type == kTypeImage) {
return Image.memory( return Image.memory(
widget.bytes, widget.bytes,
errorBuilder: (context, _, stackTrace) { errorBuilder: (context, _, stackTrace) {
return SelectableText( return ErrorMessage(
"${widget.type}/${widget.subtype} mimetype preview $kMimeTypeRaiseIssue"); message:
"${widget.type}/${widget.subtype} mimetype preview $kMimeTypeRaiseIssue");
}, },
); );
} }
if (widget.type == kTypeAudio) { if (widget.type == kTypeAudio) {
return const SelectableText("Audio playing $kMimeTypeRaiseIssue"); return const ErrorMessage(message: "Audio playing $kMimeTypeRaiseIssue");
} }
if (widget.type == kTypeVideo) { if (widget.type == kTypeVideo) {
return const SelectableText("Video playing $kMimeTypeRaiseIssue"); return const ErrorMessage(message: "Video playing $kMimeTypeRaiseIssue");
} }
return SelectableText( return ErrorMessage(
"${widget.type}/${widget.subtype} mimetype preview $kMimeTypeRaiseIssue"); message:
"${widget.type}/${widget.subtype} mimetype preview $kMimeTypeRaiseIssue");
} }
} }