From 099b26798f6d10cf08c5787d071fe4b50f8bb528 Mon Sep 17 00:00:00 2001 From: Ankit Mahato Date: Sat, 5 Apr 2025 21:14:34 +0530 Subject: [PATCH] Refactor response widgets --- lib/widgets/response_body.dart | 67 +++ lib/widgets/response_body_success.dart | 153 +++++ lib/widgets/response_headers.dart | 85 +++ lib/widgets/response_pane_header.dart | 65 +++ lib/widgets/response_tab_view.dart | 68 +++ lib/widgets/response_widgets.dart | 524 ------------------ lib/widgets/widget_not_sent.dart | 28 + lib/widgets/widget_sending.dart | 84 +++ lib/widgets/widgets.dart | 8 +- .../his_response_pane_test.dart | 4 +- test/widgets/response_widgets_test.dart | 21 +- 11 files changed, 573 insertions(+), 534 deletions(-) create mode 100644 lib/widgets/response_body.dart create mode 100644 lib/widgets/response_body_success.dart create mode 100644 lib/widgets/response_headers.dart create mode 100644 lib/widgets/response_pane_header.dart create mode 100644 lib/widgets/response_tab_view.dart delete mode 100644 lib/widgets/response_widgets.dart create mode 100644 lib/widgets/widget_not_sent.dart create mode 100644 lib/widgets/widget_sending.dart diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart new file mode 100644 index 00000000..561d9271 --- /dev/null +++ b/lib/widgets/response_body.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import 'error_message.dart'; +import 'response_body_success.dart'; + +class ResponseBody extends StatelessWidget { + const ResponseBody({ + super.key, + this.selectedRequestModel, + }); + + final RequestModel? selectedRequestModel; + + @override + Widget build(BuildContext context) { + final responseModel = selectedRequestModel?.httpResponseModel; + if (responseModel == null) { + return const ErrorMessage( + message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); + } + + var body = responseModel.body; + var formattedBody = responseModel.formattedBody; + if (body == null) { + return const ErrorMessage( + message: '$kMsgNullBody $kUnexpectedRaiseIssue'); + } + if (body.isEmpty) { + return const ErrorMessage( + message: kMsgNoContent, + showIcon: false, + showIssueButton: false, + ); + } + + final mediaType = + responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); + // Fix #415: Treat null Content-type as plain text instead of Error message + // if (mediaType == null) { + // return ErrorMessage( + // message: + // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); + // } + + var responseBodyView = getResponseBodyViewOptions(mediaType); + var options = responseBodyView.$1; + var highlightLanguage = responseBodyView.$2; + + if (formattedBody == null) { + options = [...options]; + options.remove(ResponseBodyView.code); + } + + return ResponseBodySuccess( + key: Key("${selectedRequestModel!.id}-response"), + mediaType: mediaType, + options: options, + bytes: responseModel.bodyBytes!, + body: body, + formattedBody: formattedBody, + highlightLanguage: highlightLanguage, + ); + } +} diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart new file mode 100644 index 00000000..cb68b86d --- /dev/null +++ b/lib/widgets/response_body_success.dart @@ -0,0 +1,153 @@ +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(), + 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, + ), + ), + ), + ), + } + ], + ), + ); + }, + ); + } +} diff --git a/lib/widgets/response_headers.dart b/lib/widgets/response_headers.dart new file mode 100644 index 00000000..98c1fb53 --- /dev/null +++ b/lib/widgets/response_headers.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/consts.dart'; +import 'button_copy.dart'; +import 'table_map.dart'; + +class ResponseHeadersHeader extends StatelessWidget { + const ResponseHeadersHeader({ + super.key, + required this.map, + required this.name, + }); + + final Map map; + final String name; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: kHeaderHeight, + child: Row( + children: [ + Expanded( + child: Text( + "$name (${map.length} $kLabelItems)", + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + if (map.isNotEmpty) + CopyButton( + toCopy: kJsonEncoder.convert(map), + ), + ], + ), + ); + } +} + +class ResponseHeaders extends StatelessWidget { + const ResponseHeaders({ + super.key, + required this.responseHeaders, + required this.requestHeaders, + }); + + final Map responseHeaders; + final Map requestHeaders; + + @override + Widget build(BuildContext context) { + return Padding( + padding: kPh20v5, + child: ListView( + children: [ + ResponseHeadersHeader( + map: responseHeaders, + name: kLabelResponseHeaders, + ), + if (responseHeaders.isNotEmpty) kVSpacer5, + if (responseHeaders.isNotEmpty) + MapTable( + map: responseHeaders, + colNames: kHeaderRow, + firstColumnHeaderCase: true, + ), + kVSpacer10, + ResponseHeadersHeader( + map: requestHeaders, + name: kLabelRequestHeaders, + ), + if (requestHeaders.isNotEmpty) kVSpacer5, + if (requestHeaders.isNotEmpty) + MapTable( + map: requestHeaders, + colNames: kHeaderRow, + firstColumnHeaderCase: true, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/response_pane_header.dart b/lib/widgets/response_pane_header.dart new file mode 100644 index 00000000..406889d8 --- /dev/null +++ b/lib/widgets/response_pane_header.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import 'button_clear_response.dart'; + +class ResponsePaneHeader extends StatelessWidget { + const ResponsePaneHeader({ + super.key, + this.responseStatus, + this.message, + this.time, + this.onClearResponse, + }); + + final int? responseStatus; + final String? message; + final Duration? time; + final VoidCallback? onClearResponse; + + @override + Widget build(BuildContext context) { + final bool showClearButton = onClearResponse != null; + return Padding( + padding: kPv8, + child: SizedBox( + height: kHeaderHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + kHSpacer10, + Expanded( + child: Text( + "$responseStatus: ${message ?? '-'}", + softWrap: false, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontFamily: kCodeStyle.fontFamily, + color: getResponseStatusCodeColor( + responseStatus, + brightness: Theme.of(context).brightness, + ), + ), + ), + ), + kHSpacer10, + Text( + humanizeDuration(time), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontFamily: kCodeStyle.fontFamily, + color: Theme.of(context).colorScheme.secondary, + ), + ), + kHSpacer10, + showClearButton + ? ClearResponseButton( + onPressed: onClearResponse, + ) + : const SizedBox.shrink(), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/response_tab_view.dart b/lib/widgets/response_tab_view.dart new file mode 100644 index 00000000..b5e2be73 --- /dev/null +++ b/lib/widgets/response_tab_view.dart @@ -0,0 +1,68 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'tab_label.dart'; + +class ResponseTabView extends StatefulWidget { + const ResponseTabView({ + super.key, + this.selectedId, + required this.children, + }); + + final String? selectedId; + final List children; + @override + State createState() => _ResponseTabViewState(); +} + +class _ResponseTabViewState extends State + with TickerProviderStateMixin { + late final TabController _controller; + + @override + void initState() { + super.initState(); + _controller = TabController( + length: 2, + animationDuration: kTabAnimationDuration, + vsync: this, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TabBar( + key: Key(widget.selectedId!), + controller: _controller, + labelPadding: kPh2, + overlayColor: kColorTransparentState, + onTap: (index) {}, + tabs: const [ + TabLabel( + text: kLabelResponseBody, + ), + TabLabel( + text: kLabelHeaders, + ), + ], + ), + Expanded( + child: TabBarView( + controller: _controller, + physics: const NeverScrollableScrollPhysics(), + children: widget.children, + ), + ), + ], + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart deleted file mode 100644 index 9488e5de..00000000 --- a/lib/widgets/response_widgets.dart +++ /dev/null @@ -1,524 +0,0 @@ -import 'dart:async'; -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:lottie/lottie.dart'; -import 'package:apidash/utils/utils.dart'; -import 'package:apidash/widgets/widgets.dart'; -import 'package:apidash/models/models.dart'; -import 'package:apidash/consts.dart'; - -import 'button_share.dart'; - -class NotSentWidget extends StatelessWidget { - const NotSentWidget({super.key}); - - @override - Widget build(BuildContext context) { - final color = Theme.of(context).colorScheme.secondary; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.north_east_rounded, - size: 40, - color: color, - ), - Text( - kLabelNotSent, - style: - Theme.of(context).textTheme.titleMedium?.copyWith(color: color), - ), - ], - ), - ); - } -} - -class SendingWidget extends StatefulWidget { - final DateTime? startSendingTime; - const SendingWidget({ - super.key, - required this.startSendingTime, - }); - - @override - State createState() => _SendingWidgetState(); -} - -class _SendingWidgetState extends State { - int _millisecondsElapsed = 0; - Timer? _timer; - - @override - void initState() { - super.initState(); - if (widget.startSendingTime != null) { - _millisecondsElapsed = - (DateTime.now().difference(widget.startSendingTime!).inMilliseconds ~/ - 100) * - 100; - _timer = Timer.periodic(const Duration(milliseconds: 100), _updateTimer); - } - } - - void _updateTimer(Timer timer) { - setState(() { - _millisecondsElapsed += 100; - }); - } - - @override - void dispose() { - if (_timer != null && _timer!.isActive) _timer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Center( - child: Lottie.asset(kAssetSendingLottie), - ), - Padding( - padding: kPh20t40, - child: Visibility( - visible: _millisecondsElapsed >= 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.alarm, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox( - width: 10, - ), - Text( - 'Time elapsed: ${humanizeDuration(Duration(milliseconds: _millisecondsElapsed))}', - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - style: kTextStyleButton.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ], - ); - } -} - -class ResponsePaneHeader extends StatelessWidget { - const ResponsePaneHeader({ - super.key, - this.responseStatus, - this.message, - this.time, - this.onClearResponse, - }); - - final int? responseStatus; - final String? message; - final Duration? time; - final VoidCallback? onClearResponse; - - @override - Widget build(BuildContext context) { - final bool showClearButton = onClearResponse != null; - return Padding( - padding: kPv8, - child: SizedBox( - height: kHeaderHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - kHSpacer10, - Expanded( - child: Text( - "$responseStatus: ${message ?? '-'}", - softWrap: false, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontFamily: kCodeStyle.fontFamily, - color: getResponseStatusCodeColor( - responseStatus, - brightness: Theme.of(context).brightness, - ), - ), - ), - ), - kHSpacer10, - Text( - humanizeDuration(time), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontFamily: kCodeStyle.fontFamily, - color: Theme.of(context).colorScheme.secondary, - ), - ), - kHSpacer10, - showClearButton - ? ClearResponseButton( - onPressed: onClearResponse, - ) - : const SizedBox.shrink(), - ], - ), - ), - ); - } -} - -class ResponseTabView extends StatefulWidget { - const ResponseTabView({ - super.key, - this.selectedId, - required this.children, - }); - - final String? selectedId; - final List children; - @override - State createState() => _ResponseTabViewState(); -} - -class _ResponseTabViewState extends State - with TickerProviderStateMixin { - late final TabController _controller; - - @override - void initState() { - super.initState(); - _controller = TabController( - length: 2, - animationDuration: kTabAnimationDuration, - vsync: this, - ); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - TabBar( - key: Key(widget.selectedId!), - controller: _controller, - labelPadding: kPh2, - overlayColor: kColorTransparentState, - onTap: (index) {}, - tabs: const [ - TabLabel( - text: kLabelResponseBody, - ), - TabLabel( - text: kLabelHeaders, - ), - ], - ), - Expanded( - child: TabBarView( - controller: _controller, - physics: const NeverScrollableScrollPhysics(), - children: widget.children, - ), - ), - ], - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} - -class ResponseHeadersHeader extends StatelessWidget { - const ResponseHeadersHeader({ - super.key, - required this.map, - required this.name, - }); - - final Map map; - final String name; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: kHeaderHeight, - child: Row( - children: [ - Expanded( - child: Text( - "$name (${map.length} $kLabelItems)", - style: Theme.of(context).textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - if (map.isNotEmpty) - CopyButton( - toCopy: kJsonEncoder.convert(map), - ), - ], - ), - ); - } -} - -class ResponseHeaders extends StatelessWidget { - const ResponseHeaders({ - super.key, - required this.responseHeaders, - required this.requestHeaders, - }); - - final Map responseHeaders; - final Map requestHeaders; - - @override - Widget build(BuildContext context) { - return Padding( - padding: kPh20v5, - child: ListView( - children: [ - ResponseHeadersHeader( - map: responseHeaders, - name: kLabelResponseHeaders, - ), - if (responseHeaders.isNotEmpty) kVSpacer5, - if (responseHeaders.isNotEmpty) - MapTable( - map: responseHeaders, - colNames: kHeaderRow, - firstColumnHeaderCase: true, - ), - kVSpacer10, - ResponseHeadersHeader( - map: requestHeaders, - name: kLabelRequestHeaders, - ), - if (requestHeaders.isNotEmpty) kVSpacer5, - if (requestHeaders.isNotEmpty) - MapTable( - map: requestHeaders, - colNames: kHeaderRow, - firstColumnHeaderCase: true, - ), - ], - ), - ); - } -} - -class ResponseBody extends StatelessWidget { - const ResponseBody({ - super.key, - this.selectedRequestModel, - }); - - final RequestModel? selectedRequestModel; - - @override - Widget build(BuildContext context) { - final responseModel = selectedRequestModel?.httpResponseModel; - if (responseModel == null) { - return const ErrorMessage( - message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); - } - - var body = responseModel.body; - var formattedBody = responseModel.formattedBody; - if (body == null) { - return const ErrorMessage( - message: '$kMsgNullBody $kUnexpectedRaiseIssue'); - } - if (body.isEmpty) { - return const ErrorMessage( - message: kMsgNoContent, - showIcon: false, - showIssueButton: false, - ); - } - - final mediaType = - responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); - // Fix #415: Treat null Content-type as plain text instead of Error message - // if (mediaType == null) { - // return ErrorMessage( - // message: - // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); - // } - - var responseBodyView = getResponseBodyViewOptions(mediaType); - var options = responseBodyView.$1; - var highlightLanguage = responseBodyView.$2; - - if (formattedBody == null) { - options = [...options]; - options.remove(ResponseBodyView.code); - } - - return BodySuccess( - key: Key("${selectedRequestModel!.id}-response"), - mediaType: mediaType, - options: options, - bytes: responseModel.bodyBytes!, - body: body, - formattedBody: formattedBody, - highlightLanguage: highlightLanguage, - ); - } -} - -class BodySuccess extends StatefulWidget { - const BodySuccess( - {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() => _BodySuccessState(); -} - -class _BodySuccessState 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(), - 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, - ), - ), - ), - ), - } - ], - ), - ); - }, - ); - } -} diff --git a/lib/widgets/widget_not_sent.dart b/lib/widgets/widget_not_sent.dart new file mode 100644 index 00000000..87d311a9 --- /dev/null +++ b/lib/widgets/widget_not_sent.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class NotSentWidget extends StatelessWidget { + const NotSentWidget({super.key}); + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.secondary; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.north_east_rounded, + size: 40, + color: color, + ), + Text( + kLabelNotSent, + style: + Theme.of(context).textTheme.titleMedium?.copyWith(color: color), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/widget_sending.dart b/lib/widgets/widget_sending.dart new file mode 100644 index 00000000..8c949061 --- /dev/null +++ b/lib/widgets/widget_sending.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:lottie/lottie.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +class SendingWidget extends StatefulWidget { + final DateTime? startSendingTime; + const SendingWidget({ + super.key, + required this.startSendingTime, + }); + + @override + State createState() => _SendingWidgetState(); +} + +class _SendingWidgetState extends State { + int _millisecondsElapsed = 0; + Timer? _timer; + + @override + void initState() { + super.initState(); + if (widget.startSendingTime != null) { + _millisecondsElapsed = + (DateTime.now().difference(widget.startSendingTime!).inMilliseconds ~/ + 100) * + 100; + _timer = Timer.periodic(const Duration(milliseconds: 100), _updateTimer); + } + } + + void _updateTimer(Timer timer) { + setState(() { + _millisecondsElapsed += 100; + }); + } + + @override + void dispose() { + if (_timer != null && _timer!.isActive) _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Center( + child: Lottie.asset(kAssetSendingLottie), + ), + Padding( + padding: kPh20t40, + child: Visibility( + visible: _millisecondsElapsed >= 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.alarm, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox( + width: 10, + ), + Text( + 'Time elapsed: ${humanizeDuration(Duration(milliseconds: _millisecondsElapsed))}', + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: false, + style: kTextStyleButton.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 8c21c075..7827d21d 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -47,7 +47,11 @@ export 'popup_menu_history.dart'; export 'popup_menu_uri.dart'; export 'previewer.dart'; export 'request_pane.dart'; -export 'response_widgets.dart'; +export 'response_body_success.dart'; +export 'response_body.dart'; +export 'response_headers.dart'; +export 'response_pane_header.dart'; +export 'response_tab_view.dart'; export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; @@ -59,5 +63,7 @@ export 'table_request.dart'; export 'tab_label.dart'; export 'texts.dart'; export 'uint8_audio_player.dart'; +export 'widget_not_sent.dart'; +export 'widget_sending.dart'; export 'window_caption.dart'; export 'workspace_selector.dart'; diff --git a/test/screens/history/history_widgets/his_response_pane_test.dart b/test/screens/history/history_widgets/his_response_pane_test.dart index f45aadda..c29158e3 100644 --- a/test/screens/history/history_widgets/his_response_pane_test.dart +++ b/test/screens/history/history_widgets/his_response_pane_test.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:apidash/providers/providers.dart'; -import 'package:apidash/widgets/response_widgets.dart'; import 'package:apidash/screens/history/history_widgets/his_response_pane.dart'; - +import 'package:apidash/widgets/response_pane_header.dart'; +import 'package:apidash/widgets/response_tab_view.dart'; import '../../../models/history_models.dart'; void main() { diff --git a/test/widgets/response_widgets_test.dart b/test/widgets/response_widgets_test.dart index ecb22bed..90509d52 100644 --- a/test/widgets/response_widgets_test.dart +++ b/test/widgets/response_widgets_test.dart @@ -1,8 +1,15 @@ +import 'package:apidash/widgets/response_body.dart'; +import 'package:apidash/widgets/response_body_success.dart'; +import 'package:apidash/widgets/response_headers.dart'; +import 'package:apidash/widgets/response_pane_header.dart'; +import 'package:apidash/widgets/response_tab_view.dart'; +import 'package:apidash/widgets/widget_not_sent.dart'; +import 'package:apidash/widgets/widget_sending.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/response_widgets.dart'; + import 'package:lottie/lottie.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; @@ -291,7 +298,7 @@ void main() { title: 'Body Success', theme: kThemeDataDark, home: Scaffold( - body: BodySuccess( + body: ResponseBodySuccess( body: 'Hello from API Dash', mediaType: MediaType("application", "json"), options: const [ @@ -315,7 +322,7 @@ void main() { title: 'Body Success', theme: kThemeDataDark, home: Scaffold( - body: BodySuccess( + body: ResponseBodySuccess( body: 'Hello from API Dash', mediaType: MediaType("application", "json"), options: const [ @@ -353,7 +360,7 @@ void main() async { title: 'Body Success', theme: kThemeDataLight, home: Scaffold( - body: BodySuccess( + body: ResponseBodySuccess( body: 'Hello', formattedBody: code, mediaType: MediaType("application", "json"), @@ -381,7 +388,7 @@ void main() async { title: 'Body Success', theme: kThemeDataDark, home: Scaffold( - body: BodySuccess( + body: ResponseBodySuccess( body: 'Hello from API Dash', mediaType: MediaType("image", "jpeg"), options: const [ @@ -405,7 +412,7 @@ void main() async { title: 'Body Success', theme: kThemeDataLight, home: Scaffold( - body: BodySuccess( + body: ResponseBodySuccess( body: 'Raw Hello from API Dash', formattedBody: 'Formatted Hello from API Dash', mediaType: MediaType("application", "json"), @@ -437,7 +444,7 @@ void main() async { title: 'Body Success', theme: kThemeDataLight, home: Scaffold( - body: BodySuccess( + body: ResponseBodySuccess( body: 'Raw Hello from API Dash', formattedBody: null, mediaType: MediaType("text", "csv"),