Files
apidash/lib/widgets/response_body_success.dart
2025-06-29 17:07:36 +05:30

298 lines
10 KiB
Dart

import 'dart:convert';
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:apidash/utils/utils.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import 'button_share.dart';
class ResponseBodySuccess extends StatefulWidget {
const ResponseBodySuccess(
{super.key,
required this.mediaType,
required this.body,
required this.options,
required this.bytes,
this.formattedBody,
this.highlightLanguage});
final MediaType mediaType;
final List<ResponseBodyView> options;
final String body;
final Uint8List bytes;
final String? formattedBody;
final String? highlightLanguage;
@override
State<ResponseBodySuccess> createState() => _ResponseBodySuccessState();
}
class _ResponseBodySuccessState extends State<ResponseBodySuccess> {
int segmentIdx = 0;
@override
Widget build(BuildContext context) {
var currentSeg = widget.options[segmentIdx];
var codeTheme = Theme.of(context).brightness == Brightness.light
? kLightCodeTheme
: kDarkCodeTheme;
final textContainerdecoration = BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
borderRadius: kBorderRadius8,
);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
var showLabel = showButtonLabelsInBodySuccess(
widget.options.length,
constraints.maxWidth,
);
return Padding(
padding: kP10,
child: Column(
children: [
Row(
children: [
(widget.options == kRawBodyViewOptions)
? const SizedBox()
: SegmentedButton<ResponseBodyView>(
style: SegmentedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8),
),
selectedIcon: Icon(currentSeg.icon),
segments: widget.options
.map<ButtonSegment<ResponseBodyView>>(
(e) => ButtonSegment<ResponseBodyView>(
value: e,
label: Text(e.label),
icon: constraints.maxWidth >
kMinWindowSize.width
? Icon(e.icon)
: null,
),
)
.toList(),
selected: {currentSeg},
onSelectionChanged: (newSelection) {
setState(() {
segmentIdx =
widget.options.indexOf(newSelection.first);
});
},
),
const Spacer(),
((widget.options == kPreviewRawBodyViewOptions) ||
kCodeRawBodyViewOptions.contains(currentSeg))
? CopyButton(
toCopy: widget.formattedBody ?? widget.body,
showLabel: showLabel,
)
: const SizedBox(),
kIsMobile
? ShareButton(
toShare: widget.formattedBody ?? widget.body,
showLabel: showLabel,
)
: SaveInDownloadsButton(
content: widget.bytes,
mimeType: widget.mediaType.mimeType,
showLabel: showLabel,
),
],
),
kVSpacer10,
switch (currentSeg) {
ResponseBodyView.preview || ResponseBodyView.none => Expanded(
child: Container(
width: double.maxFinite,
padding: kP8,
decoration: textContainerdecoration,
child: Previewer(
bytes: widget.bytes,
body: widget.body,
type: widget.mediaType.type,
subtype: widget.mediaType.subtype,
hasRaw: widget.options.contains(ResponseBodyView.raw),
),
),
),
ResponseBodyView.code => Expanded(
child: Container(
width: double.maxFinite,
padding: kP8,
decoration: textContainerdecoration,
child: CodePreviewer(
code: widget.formattedBody ?? widget.body,
theme: codeTheme,
language: widget.highlightLanguage,
textStyle: kCodeStyle,
),
),
),
ResponseBodyView.raw => Expanded(
child: Container(
width: double.maxFinite,
padding: kP8,
decoration: textContainerdecoration,
child: SingleChildScrollView(
child: SelectableText(
widget.formattedBody ?? widget.body,
style: kCodeStyle,
),
),
),
),
ResponseBodyView.sse => Expanded(
child: Container(
width: double.maxFinite,
padding: kP8,
decoration: textContainerdecoration,
child: SSEDisplay(
sseOutput: widget.body,
),
),
),
}
],
),
);
},
);
}
}
//MOVE THIS SOMEWHERE ELSE
class SSEDisplay extends StatefulWidget {
final String sseOutput;
const SSEDisplay({super.key, required this.sseOutput});
@override
State<SSEDisplay> createState() => _SSEDisplayState();
}
class _SSEDisplayState extends State<SSEDisplay> {
final _scrollController = ScrollController();
bool autoScrollEnabled = true;
bool _isScrolling = false;
@override
void initState() {
super.initState();
_scrollController.addListener(() {
final position = _scrollController.position;
final atBottom = position.pixels >= position.maxScrollExtent - 50;
if (autoScrollEnabled && !atBottom) {
// User scrolled up manually
setState(() => autoScrollEnabled = false);
} else if (!autoScrollEnabled && atBottom) {
// User scrolled back to bottom
setState(() => autoScrollEnabled = true);
}
});
}
@override
void didUpdateWidget(covariant SSEDisplay oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.sseOutput != widget.sseOutput &&
autoScrollEnabled &&
!_isScrolling) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (_scrollController.hasClients) {
_isScrolling = true;
_scrollController.jumpTo(
_scrollController.position.maxScrollExtent,
);
_isScrolling = false;
}
});
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
List<dynamic> sse;
try {
sse = jsonDecode(widget.sseOutput);
} catch (e) {
return Text(
'Invalid SSE output',
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.red),
);
}
return SingleChildScrollView(
controller: _scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: sse.map<Widget>((chunk) {
Map<String, dynamic>? parsedJson;
try {
parsedJson = jsonDecode(chunk);
} catch (_) {}
return Card(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: parsedJson != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: parsedJson.entries.map((entry) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${entry.key}: ',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: kColorGQL,
),
),
const SizedBox(width: 4),
Expanded(
child: Text(
entry.value.toString(),
style: theme.textTheme.bodyMedium?.copyWith(
fontFamily: 'monospace',
),
),
),
],
),
);
}).toList(),
)
: Text(
chunk.toString(),
style: theme.textTheme.bodyMedium?.copyWith(
fontFamily: 'monospace',
),
),
),
);
}).toList(),
),
);
}
}