mirror of
https://github.com/foss42/apidash.git
synced 2025-06-03 08:16:25 +08:00
Better handling of response of various content-types
This commit is contained in:
@ -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,
|
||||||
|
@ -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;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user