From c04507f93a76bfaed6603b247597f726e83ed306 Mon Sep 17 00:00:00 2001 From: vidya-hub Date: Wed, 20 Dec 2023 21:51:27 +0530 Subject: [PATCH] feat: Multi Part Request Feature Added --- lib/consts.dart | 8 +- lib/providers/collection_providers.dart | 19 +- .../request_pane/request_body.dart | 190 +--------------- lib/services/http_service.dart | 64 +++++- lib/utils/convert_utils.dart | 18 ++ lib/widgets/form_data_widget.dart | 212 ++++++++++++++++++ 6 files changed, 316 insertions(+), 195 deletions(-) create mode 100644 lib/widgets/form_data_widget.dart diff --git a/lib/consts.dart b/lib/consts.dart index 05d0e6e8..cba29ab6 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -301,12 +301,12 @@ const kContentTypeMap = { ContentType.formdata: "multipart/form-data", }; const kFormDataTypeMap = { - FormDataType.file: "File", - FormDataType.text: "Text", + FormDataType.file: "file", + FormDataType.text: "text", }; const kMapFormDataType = { - "File": FormDataType.file, - "Text": FormDataType.text, + "file": FormDataType.file, + "text": FormDataType.text, }; enum ResponseBodyView { preview, code, raw, none } diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index cd905e79..dc059913 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,5 +1,7 @@ import 'package:apidash/models/form_data_model.dart'; +import 'package:apidash/services/http_service.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart' as http; import '../consts.dart'; import '../models/models.dart'; @@ -158,10 +160,20 @@ class CollectionStateNotifier ref.read(codePaneVisibleStateProvider.notifier).state = false; final defaultUriScheme = ref.read(settingsProvider.select((value) => value.defaultUriScheme)); - + (http.Response?, Duration?, String?)? responseRec; RequestModel requestModel = state![id]!; - var responseRec = - await request(requestModel, defaultUriScheme: defaultUriScheme); + if (requestModel.formDataList != null && + requestModel.formDataList!.isNotEmpty) { + responseRec = await multiPartRequest( + requestModel, + defaultUriScheme: defaultUriScheme, + ); + } else { + responseRec = await request( + requestModel, + defaultUriScheme: defaultUriScheme, + ); + } late final RequestModel newRequestModel; if (responseRec.$1 == null) { newRequestModel = requestModel.copyWith( @@ -180,7 +192,6 @@ class CollectionStateNotifier responseModel: responseModel, ); } - //print(newRequestModel); ref.read(sentRequestIdStateProvider.notifier).state = null; var map = {...state!}; map[id] = newRequestModel; diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index da1d15d7..21dd63c6 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -1,13 +1,9 @@ import 'dart:math'; import 'package:apidash/consts.dart'; -import 'package:apidash/models/form_data_model.dart'; import 'package:apidash/providers/providers.dart'; -import 'package:apidash/utils/extensions/file_extension.dart'; -import 'package:apidash/widgets/form_data_field.dart'; +import 'package:apidash/widgets/form_data_widget.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:davi/davi.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,15 +15,12 @@ class EditRequestBody extends ConsumerStatefulWidget { } class _EditRequestBodyState extends ConsumerState { - List rows = []; final random = Random.secure(); late int seed; - late FilePicker filePicker; @override void initState() { super.initState(); seed = random.nextInt(kRandMax); - filePicker = FilePicker.platform; } @override @@ -40,138 +33,6 @@ class _EditRequestBodyState extends ConsumerState { .watch(collectionStateNotifierProvider)![activeId] ?.requestBodyContentType) ?? ContentType.values.first; - DaviModel model = DaviModel( - rows: rows, - columns: [ - DaviColumn( - name: 'Key', - grow: 1, - cellBuilder: (_, row) { - int idx = row.index; - return SizedBox( - child: FormDataField( - keyId: "$activeId-$idx-form-v-$seed", - initialValue: rows[idx].value, - hintText: " Key", - onChanged: (value) { - rows[idx] = rows[idx].copyWith( - name: value, - ); - _onFieldChange(activeId); - }, - colorScheme: Theme.of(context).colorScheme, - formDataType: rows[idx].type, - onFormDataTypeChanged: (value) { - rows[idx] = rows[idx].copyWith( - type: value ?? FormDataType.text, - ); - rows[idx] = rows[idx].copyWith(value: ""); - _onFieldChange(activeId); - }, - ), - ); - }, - sortable: false, - ), - DaviColumn( - width: 10, - cellBuilder: (_, row) { - return Text( - "=", - style: kCodeStyle, - ); - }, - ), - DaviColumn( - name: 'Value', - grow: 4, - cellBuilder: (_, row) { - int idx = row.index; - return rows[idx].type == FormDataType.file - ? Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: kPs2, - child: Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () async { - FilePickerResult? pickedResult = - await filePicker.pickFiles(); - if (pickedResult != null && - pickedResult.files.isNotEmpty) { - rows[idx] = rows[idx].copyWith( - value: pickedResult.files.first.path, - ); - _onFieldChange(activeId); - } - }, - child: Text( - "Select File", - style: kTextStyleButton.copyWith( - fontSize: 10, - ), - ), - ), - ), - Expanded( - child: Padding( - padding: kPs2, - child: Text( - rows[idx].type == FormDataType.file - ? (rows[idx].value != null - ? rows[idx].value.toString().fileName - : "") - : "", - style: kTextStyleButton, - overflow: TextOverflow.ellipsis, - ), - ), - ) - ], - ), - ), - ) - : CellField( - keyId: "$activeId-$idx-form-v-$seed", - initialValue: rows[idx].value, - hintText: " Value", - onChanged: (value) { - rows[idx] = rows[idx].copyWith(value: value); - _onFieldChange(activeId); - }, - colorScheme: Theme.of(context).colorScheme, - ); - }, - sortable: false, - ), - DaviColumn( - pinStatus: PinStatus.none, - width: 30, - cellBuilder: (_, row) { - return InkWell( - child: Theme.of(context).brightness == Brightness.dark - ? kIconRemoveDark - : kIconRemoveLight, - onTap: () { - seed = random.nextInt(kRandMax); - if (rows.length == 1) { - setState(() { - rows = [ - kFormDataEmptyModel, - ]; - }); - } else { - rows.removeAt(row.index); - } - _onFieldChange(activeId); - }, - ); - }, - ), - ], - ); return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, @@ -193,43 +54,11 @@ class _EditRequestBodyState extends ConsumerState { ), Expanded( child: requestBodyStateWatcher == ContentType.formdata - ? Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: kBorderRadius12, - ), - margin: kP10, - child: Column( - children: [ - Expanded( - child: DaviTheme( - data: kTableThemeData, - child: Davi(model), - ), - ), - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.only(bottom: 30), - child: ElevatedButton.icon( - onPressed: () { - rows.add(kFormDataEmptyModel); - _onFieldChange(activeId); - }, - icon: const Icon(Icons.add), - label: const Text( - "Add Form Data", - style: kTextStyleButton, - ), - ), - ), - ), - ], + ? FormDataWidget( + seed: seed, + onFormDataRemove: () { + seed = random.nextInt(kRandMax); + }, ) : TextFieldEditor( key: Key("$activeId-body"), @@ -246,13 +75,6 @@ class _EditRequestBodyState extends ConsumerState { ), ); } - - void _onFieldChange(String activeId) { - ref.read(collectionStateNotifierProvider.notifier).update( - activeId, - formDataList: rows, - ); - } } class DropdownButtonBodyContentType extends ConsumerStatefulWidget { diff --git a/lib/services/http_service.dart b/lib/services/http_service.dart index 8265a299..d49656a0 100644 --- a/lib/services/http_service.dart +++ b/lib/services/http_service.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:http/http.dart' as http; -import 'package:apidash/utils/utils.dart'; -import 'package:apidash/models/models.dart'; + import 'package:apidash/consts.dart'; +import 'package:apidash/models/form_data_model.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:http/http.dart' as http; Future<(http.Response?, Duration?, String?)> request( RequestModel requestModel, { @@ -63,3 +65,59 @@ Future<(http.Response?, Duration?, String?)> request( return (null, null, uriRec.$2); } } + +Future<(http.Response?, Duration?, String?)> multiPartRequest( + RequestModel requestModel, { + String defaultUriScheme = kDefaultUriScheme, +}) async { + (Uri?, String?) uriRec = getValidRequestUri( + requestModel.url, + requestModel.requestParams, + defaultUriScheme: defaultUriScheme, + ); + if (uriRec.$1 != null) { + Uri requestUrl = uriRec.$1!; + Map headers = requestModel.headersMap; + try { + var requestBody = requestModel.requestBody; + if (kMethodsWithBody.contains(requestModel.method) && + requestBody != null) { + var contentLength = utf8.encode(requestBody).length; + if (contentLength > 0) { + headers[HttpHeaders.contentLengthHeader] = contentLength.toString(); + headers[HttpHeaders.contentTypeHeader] = + kContentTypeMap[requestModel.requestBodyContentType] ?? ""; + } + } + Stopwatch stopwatch = Stopwatch()..start(); + + var request = http.MultipartRequest( + requestModel.method.name.toUpperCase(), + requestUrl, + ); + for (FormDataModel formData in (requestModel.formDataList ?? [])) { + if (formData.type == FormDataType.text) { + request.fields.addAll({formData.name: formData.value}); + } else { + request.files.add( + await http.MultipartFile.fromPath( + formData.name, + formData.value, + ), + ); + } + } + + http.StreamedResponse response = await request.send(); + + stopwatch.stop(); + http.Response convertedHttpResponse = + await convertStreamedResponse(response); + return (convertedHttpResponse, stopwatch.elapsed, null); + } catch (e) { + return (null, null, e.toString()); + } + } else { + return (null, null, uriRec.$2); + } +} diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index 81c5bff8..63b1f3eb 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:apidash/models/form_data_model.dart'; +import 'package:http/http.dart' as http; import '../consts.dart'; import '../models/models.dart'; @@ -142,3 +143,20 @@ Uint8List jsonMapToBytes(Map? map) { return bytes; } } + +Future convertStreamedResponse( + http.StreamedResponse streamedResponse, +) async { + Uint8List bodyBytes = await streamedResponse.stream.toBytes(); + + http.Response response = http.Response.bytes( + bodyBytes, + streamedResponse.statusCode, + headers: streamedResponse.headers, + persistentConnection: streamedResponse.persistentConnection, + reasonPhrase: streamedResponse.reasonPhrase, + request: streamedResponse.request, + ); + + return response; +} diff --git a/lib/widgets/form_data_widget.dart b/lib/widgets/form_data_widget.dart new file mode 100644 index 00000000..85318d70 --- /dev/null +++ b/lib/widgets/form_data_widget.dart @@ -0,0 +1,212 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/models/form_data_model.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/form_data_field.dart'; +import 'package:apidash/widgets/textfields.dart'; +import 'package:davi/davi.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class FormDataWidget extends ConsumerStatefulWidget { + const FormDataWidget({ + super.key, + required this.seed, + required this.onFormDataRemove, + }); + final int seed; + final Function onFormDataRemove; + @override + ConsumerState createState() => _FormDataBodyState(); +} + +class _FormDataBodyState extends ConsumerState { + @override + Widget build(BuildContext context) { + final activeId = ref.watch(activeIdStateProvider); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(activeId!); + List rows = requestModel?.formDataList ?? []; + DaviModel model = DaviModel( + rows: rows, + columns: [ + DaviColumn( + name: 'Key', + grow: 1, + cellBuilder: (_, row) { + int idx = row.index; + return SizedBox( + child: FormDataField( + keyId: "$activeId-$idx-form-v-${widget.seed}", + initialValue: rows[idx].name, + hintText: " Key", + onChanged: (value) { + rows[idx] = rows[idx].copyWith( + name: value, + ); + _onFieldChange(activeId); + }, + colorScheme: Theme.of(context).colorScheme, + formDataType: rows[idx].type, + onFormDataTypeChanged: (value) { + rows[idx] = rows[idx].copyWith( + type: value ?? FormDataType.text, + ); + rows[idx] = rows[idx].copyWith(value: ""); + _onFieldChange(activeId); + }, + ), + ); + }, + sortable: false, + ), + DaviColumn( + width: 10, + cellBuilder: (_, row) { + return Text( + "=", + style: kCodeStyle, + ); + }, + ), + DaviColumn( + name: 'Value', + grow: 4, + cellBuilder: (_, row) { + int idx = row.index; + return rows[idx].type == FormDataType.file + ? Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: kPs8, + child: Row( + children: [ + Expanded( + child: ElevatedButtonTheme( + data: const ElevatedButtonThemeData(), + child: ElevatedButton.icon( + onPressed: () async { + FilePickerResult? pickedResult = + await FilePicker.platform.pickFiles(); + if (pickedResult != null && + pickedResult.files.isNotEmpty) { + rows[idx] = rows[idx].copyWith( + value: pickedResult.files.first.path, + ); + _onFieldChange(activeId); + } + }, + icon: const Icon( + Icons.snippet_folder_rounded, + size: 18, + ), + label: Text( + rows[idx].type == FormDataType.file + ? (rows[idx].value != null + ? rows[idx].value.toString() + : "Select File") + : "Select File", + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: kTextStyleButton.copyWith( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ], + ), + ), + ) + : CellField( + keyId: "$activeId-$idx-form-v-${widget.seed}", + initialValue: rows[idx].value, + hintText: " Value", + onChanged: (value) { + rows[idx] = rows[idx].copyWith(value: value); + _onFieldChange(activeId); + }, + colorScheme: Theme.of(context).colorScheme, + ); + }, + sortable: false, + ), + DaviColumn( + pinStatus: PinStatus.none, + width: 30, + cellBuilder: (_, row) { + return InkWell( + child: Theme.of(context).brightness == Brightness.dark + ? kIconRemoveDark + : kIconRemoveLight, + onTap: () { + widget.onFormDataRemove(); + if (rows.length == 1) { + setState(() { + rows = [ + kFormDataEmptyModel, + ]; + }); + } else { + rows.removeAt(row.index); + } + _onFieldChange(activeId); + }, + ); + }, + ), + ], + ); + return Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: kBorderRadius12, + ), + margin: kP10, + child: Column( + children: [ + Expanded( + child: DaviTheme( + data: kTableThemeData, + child: Davi(model), + ), + ), + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: ElevatedButton.icon( + onPressed: () { + rows.add(kFormDataEmptyModel); + _onFieldChange(activeId); + }, + icon: const Icon(Icons.add), + label: const Text( + "Add Form Data", + style: kTextStyleButton, + ), + ), + ), + ), + ], + ); + } + + void _onFieldChange(String activeId) { + List formDataList = + ref.read(collectionStateNotifierProvider)?[activeId]?.formDataList ?? + []; + ref.read(collectionStateNotifierProvider.notifier).update( + activeId, + formDataList: formDataList, + ); + } +}