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 options; final String body; final Uint8List bytes; final String? formattedBody; final String? highlightLanguage; @override State createState() => _ResponseBodySuccessState(); } class _ResponseBodySuccessState extends State { 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( style: SegmentedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8), ), selectedIcon: Icon(currentSeg.icon), segments: widget.options .map>( (e) => ButtonSegment( 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 createState() => _SSEDisplayState(); } class _SSEDisplayState extends State { 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 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((chunk) { Map? 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(), ), ); } }