feat: Multi Part Request Feature Added

This commit is contained in:
vidya-hub
2023-12-20 21:51:27 +05:30
parent ffb9681958
commit c04507f93a
6 changed files with 316 additions and 195 deletions

View File

@ -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 }

View File

@ -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;

View File

@ -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<EditRequestBody> {
List<FormDataModel> 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<EditRequestBody> {
.watch(collectionStateNotifierProvider)![activeId]
?.requestBodyContentType) ??
ContentType.values.first;
DaviModel<FormDataModel> model = DaviModel<FormDataModel>(
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<EditRequestBody> {
),
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<FormDataModel>(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<EditRequestBody> {
),
);
}
void _onFieldChange(String activeId) {
ref.read(collectionStateNotifierProvider.notifier).update(
activeId,
formDataList: rows,
);
}
}
class DropdownButtonBodyContentType extends ConsumerStatefulWidget {

View File

@ -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<String, String> 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);
}
}

View File

@ -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<String, dynamic>? map) {
return bytes;
}
}
Future<http.Response> 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;
}

View File

@ -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<FormDataWidget> createState() => _FormDataBodyState();
}
class _FormDataBodyState extends ConsumerState<FormDataWidget> {
@override
Widget build(BuildContext context) {
final activeId = ref.watch(activeIdStateProvider);
final requestModel = ref
.read(collectionStateNotifierProvider.notifier)
.getRequestModel(activeId!);
List<FormDataModel> rows = requestModel?.formDataList ?? [];
DaviModel<FormDataModel> model = DaviModel<FormDataModel>(
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<FormDataModel>(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<FormDataModel> formDataList =
ref.read(collectionStateNotifierProvider)?[activeId]?.formDataList ??
[];
ref.read(collectionStateNotifierProvider.notifier).update(
activeId,
formDataList: formDataList,
);
}
}