diff --git a/lib/constants/api_endpoints.dart b/lib/constants/api_endpoints.dart index d806807c86037bc0810f631d2dc88971483f5413..9255dc199a1b7d27200aa9a2406056c1b6a6d6b9 100644 --- a/lib/constants/api_endpoints.dart +++ b/lib/constants/api_endpoints.dart @@ -13,4 +13,6 @@ class ApiUrl { static const submitConcent = '$baseUrl/api/forms/consentApplication'; static const submitBulkConcent = '$baseUrl/api/forms/consentBulkApplication'; static const getAllForms = '$baseUrl/api/forms/getAllForms?isDetail=true'; + static const fileUpload = '$baseUrl/api/forms/fileUpload'; + static const deleteFile = '$baseUrl/api/forms/deleteCloudFile'; } diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index 6da8d2cdc4670e0bd1b28ad54d148584658c475d..743a3a8b97d5ded21247d8aab3074b707154d6d0 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -14,6 +14,8 @@ class FieldType { static const String radio = "radio"; static const String boolean = "boolean"; static const String file = "file"; + static const String image = "image"; + static const String pdf = "pdf"; static const String heading = 'heading'; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f8919c23a69681627877f03713853b7f746b4ac3..4904b68d3226df2faa3fbf5d45c23421d417bcc1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -61,5 +61,10 @@ "invalidResponse": "Invalid response recieved from the server", "takeAPicture": "Take a picture", "goToFiles": "Go to files", - "cropImage": "Crop image" + "cropImage": "Crop image", + "reasonForIncorrectSelection": "Reason for incorrect selection", + "fileViewer": "File viewer", + "preview": "Preview", + "remove": "Remove", + "attachment": "Attachment" } \ No newline at end of file diff --git a/lib/pages/application_details_page.dart b/lib/pages/application_details_page.dart index e973baecd4cb1ea44fae465639dc965b27ea92a2..e240dc6b7a2839cb5f67029c64485cc4007206d9 100644 --- a/lib/pages/application_details_page.dart +++ b/lib/pages/application_details_page.dart @@ -191,6 +191,11 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> ? existingData[childKey] [existingData[childKey].keys.elementAt(0)] ['inspectionValue'] + : '', + 'attachment': _isleadInspector + ? existingData[childKey] + [existingData[childKey].keys.elementAt(0)] + ['attachment'] : '' } } diff --git a/lib/pages/file_viewer.dart b/lib/pages/file_viewer.dart new file mode 100644 index 0000000000000000000000000000000000000000..6d45183e0d019eacfe346537bbceac7eef1c6df2 --- /dev/null +++ b/lib/pages/file_viewer.dart @@ -0,0 +1,84 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:smf_mobile/constants/app_constants.dart'; +import 'package:smf_mobile/constants/color_constants.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'dart:async'; + +class FileViewer extends StatefulWidget { + final String fileType; + final String fileUrl; + const FileViewer({Key? key, required this.fileType, required this.fileUrl}) + : super(key: key); + @override + _FileViewerState createState() => _FileViewerState(); +} + +class _FileViewerState extends State<FileViewer> { + final Completer<WebViewController> _controller = + Completer<WebViewController>(); + late String _fileUrl; + bool _isLoading = true; + @override + void initState() { + super.initState(); + if (Platform.isAndroid) WebView.platform = AndroidWebView(); + if (widget.fileType == FieldType.image) { + _fileUrl = widget.fileUrl; + } else { + _fileUrl = + 'https://docs.google.com/gview?embedded=true&url=' + widget.fileUrl; + } + // print(widget.fileUrl); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 10, + elevation: 0, + backgroundColor: Colors.white, + leading: const BackButton(color: AppColors.black60), + title: Text( + AppLocalizations.of(context)!.fileViewer, + style: GoogleFonts.lato( + color: AppColors.black87, + fontSize: 16.0, + letterSpacing: 0.12, + fontWeight: FontWeight.w600, + ), + ), + // centerTitle: true, + ), + // Tab controller + body: Container( + color: AppColors.scaffoldBackground, + height: MediaQuery.of(context).size.height, + child: Stack(children: <Widget>[ + Builder(builder: (BuildContext context) { + return WebView( + debuggingEnabled: true, + initialUrl: _fileUrl, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + onPageFinished: (finish) { + setState(() { + _isLoading = false; + }); + }, + gestureNavigationEnabled: true, + ); + }), + _isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : Stack(), + ]))); + } +} diff --git a/lib/services/application_service.dart b/lib/services/application_service.dart index 8deb5807bca0b5a5f017da54e0c73179c626dc9d..ae76d9267472b1a02be8cf069ccea0e537ed8b31 100644 --- a/lib/services/application_service.dart +++ b/lib/services/application_service.dart @@ -1,5 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:dio/adapter.dart'; import 'package:http/http.dart' as http; import 'package:smf_mobile/constants/api_endpoints.dart'; import 'package:smf_mobile/constants/app_constants.dart'; @@ -26,9 +28,9 @@ class ApplicationService extends BaseService { final response = await http.post(Uri.parse(ApiUrl.submitInspection), headers: headers, body: body); - developer.log(ApiUrl.submitInspection); - developer.log(body); - developer.log(response.body); + // developer.log(ApiUrl.submitInspection); + // developer.log(body); + // developer.log(response.body); return response; } @@ -67,4 +69,28 @@ class ApplicationService extends BaseService { // developer.log(response.body); return response; } + + static Future<dynamic> uploadImage(filepath) async { + var dio = Dio(); + dio.options.headers = await BaseService.getHeaders(); + var formData = FormData.fromMap({ + 'files': await MultipartFile.fromFile(filepath, + filename: filepath.split("/").last) + }); + final response = await dio.post( + ApiUrl.fileUpload, + data: formData, + ); + return response.data['responseData'][0]; + } + + static Future<dynamic> deleteImage(List data) async { + var body = json.encode(data); + Map<String, String> headers = await BaseService.getHeaders(); + + final response = await http.delete(Uri.parse(ApiUrl.deleteFile), + headers: headers, body: body); + Map _data = json.decode(response.body); + return _data['responseData']; + } } diff --git a/lib/widgets/assistant_inspector_application_field.dart b/lib/widgets/assistant_inspector_application_field.dart index ad7a4433e1df137e7f0895bf979d3046b20207be..473781f5e5e6485e09f7a170a881240cff9035aa 100644 --- a/lib/widgets/assistant_inspector_application_field.dart +++ b/lib/widgets/assistant_inspector_application_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:smf_mobile/constants/app_constants.dart'; import 'package:smf_mobile/constants/color_constants.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:smf_mobile/util/helper.dart'; @@ -26,6 +27,7 @@ class _AssistantInspectorApplicationFieldState String _radioValue = ''; String _inspectionValue = ''; String _inspectionComment = ''; + String _attachment = ''; String _noteText = ''; @override @@ -35,6 +37,7 @@ class _AssistantInspectorApplicationFieldState _inspectionComment = widget.leadInspectorData['comments']; _radioValue = widget.leadInspectorData['value']; _inspectionValue = widget.leadInspectorData['inspectionValue']; + _attachment = widget.leadInspectorData['attachment']; } catch (_) { return; } @@ -180,7 +183,8 @@ class _AssistantInspectorApplicationFieldState ), ), ), - _radioValue == 'incorrect' + _radioValue.toLowerCase() == + FieldValue.inCorrect.toLowerCase() ? Wrap(children: [ Container( width: @@ -188,7 +192,8 @@ class _AssistantInspectorApplicationFieldState padding: const EdgeInsets.only(top: 20), child: Text( - 'Reason for the incorrect selection', + AppLocalizations.of(context)! + .reasonForIncorrectSelection, style: GoogleFonts.lato( color: AppColors.black60, fontWeight: FontWeight.w700, @@ -271,50 +276,33 @@ class _AssistantInspectorApplicationFieldState ), ), ), - // Container( - // width: - // MediaQuery.of(context).size.width, - // padding: - // const EdgeInsets.only(top: 20), - // child: Text( - // "Instiute's comment", - // style: GoogleFonts.lato( - // color: AppColors.black60, - // fontWeight: FontWeight.w700, - // fontSize: 14.0, - // letterSpacing: 0.25, - // ), - // )), - // Container( - // width: - // MediaQuery.of(context).size.width, - // margin: - // const EdgeInsets.only(bottom: 0), - // child: Container( - // padding: const EdgeInsets.fromLTRB( - // 15, 10, 15, 10), - // margin: - // const EdgeInsets.only(top: 10), - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: - // const BorderRadius.all( - // Radius.circular(4.0)), - // border: Border.all( - // color: AppColors.black08, - // ), - // ), - // child: Text( - // 'Omne animal, simul atque integre iudicante itaque turbent.', - // style: GoogleFonts.lato( - // color: AppColors.black87, - // fontWeight: FontWeight.w400, - // fontSize: 14.0, - // letterSpacing: 0.25, - // ), - // ), - // ), - // ), + _attachment != '' + ? Container( + width: MediaQuery.of(context) + .size + .width, + margin: const EdgeInsets.only( + bottom: 0), + child: Container( + padding: + const EdgeInsets.fromLTRB( + 15, 10, 15, 10), + margin: const EdgeInsets.only( + top: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + const BorderRadius.all( + Radius.circular(4.0)), + border: Border.all( + color: AppColors.black08, + ), + ), + child: + Image.network(_attachment), + ), + ) + : const Center(), ]) : const Center() ], diff --git a/lib/widgets/lead_inspector_application_field.dart b/lib/widgets/lead_inspector_application_field.dart index 59011fc8b80eb151c8ad5fc0ca1a97c31d4b36cd..ba6b791e88c1d3e955ee15823ffe7f7e18577fdd 100644 --- a/lib/widgets/lead_inspector_application_field.dart +++ b/lib/widgets/lead_inspector_application_field.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:smf_mobile/constants/app_constants.dart'; import 'package:smf_mobile/constants/color_constants.dart'; +import 'package:smf_mobile/pages/file_viewer.dart'; +import 'package:smf_mobile/services/application_service.dart'; +import 'package:smf_mobile/util/helper.dart'; import 'package:smf_mobile/widgets/lead_inspector_dialog.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:image_picker/image_picker.dart'; @@ -34,11 +37,19 @@ class _LeadInspectorApplicationFieldState late Map _data; late String _radioValue; String _inspectionValue = ''; + String _attachment = ''; String _summaryText = ''; final List<String> _options = [FieldValue.correct, FieldValue.inCorrect]; - late File _selectedFile; final _picker = ImagePicker(); - bool _inProcess = false; + final List<String> _imageExtensions = [ + 'apng', + 'png', + 'jpg', + 'jpeg', + 'avif', + 'gif', + 'svg' + ]; @override void initState() { @@ -50,6 +61,7 @@ class _LeadInspectorApplicationFieldState _summaryText = _data[_data.keys.elementAt(1)]; try { _inspectionValue = _data[_data.keys.elementAt(2)]; + _attachment = _data[_data.keys.elementAt(3)]; } catch (_) { return; } @@ -69,7 +81,8 @@ class _LeadInspectorApplicationFieldState widget.fieldData.keys.elementAt(0): { 'value': FieldValue.correct, 'comments': '', - 'inspectionValue': '' + 'inspectionValue': '', + 'attachment': _attachment } } }; @@ -79,7 +92,8 @@ class _LeadInspectorApplicationFieldState widget.fieldData.keys.elementAt(0): { 'value': _radioValue, 'comments': dialogData['summaryText'], - 'inspectionValue': dialogData['inspectionValue'] + 'inspectionValue': dialogData['inspectionValue'], + 'attachment': _attachment } } }; @@ -90,7 +104,8 @@ class _LeadInspectorApplicationFieldState widget.fieldData.keys.elementAt(0): { 'value': _radioValue, 'comments': dialogData['summaryText'], - 'inspectionValue': dialogData['inspectionValue'] + 'inspectionValue': dialogData['inspectionValue'], + 'attachment': _attachment } } }; @@ -103,6 +118,24 @@ class _LeadInspectorApplicationFieldState widget.parentAction(data); } + _triggerAttachmentUpdate(String attachment) { + Map data = { + widget.fieldName: { + widget.fieldData.keys.elementAt(0): { + 'value': _radioValue, + 'comments': _summaryText, + 'inspectionValue': _inspectionValue, + 'attachment': attachment + } + } + }; + // print(data); + setState(() { + _attachment = attachment; + }); + widget.parentAction(data); + } + Future _displayCommentDialog() { return showDialog( barrierDismissible: false, @@ -199,9 +232,7 @@ class _LeadInspectorApplicationFieldState } Future<dynamic> _getImage(ImageSource source) async { - _inProcess = true; XFile? image = await _picker.pickImage(source: source); - if (image != null) { try { File? cropped = await ImageCropper().cropImage( @@ -218,21 +249,51 @@ class _LeadInspectorApplicationFieldState statusBarColor: Colors.grey.shade900, backgroundColor: Colors.white, )); - print(cropped!.path); - setState(() { - _selectedFile = cropped; - _inProcess = false; - }); + String fileUrl = await ApplicationService.uploadImage(cropped!.path); + _triggerAttachmentUpdate(fileUrl); } catch (e) { - print(e); + // print(e); + return; } - } else { + } + } + + Future<void> _deleteAttachment(String attachment) async { + List data = [attachment]; + final bool fileDeleted = await ApplicationService.deleteImage(data); + if (fileDeleted) { + Helper.toastMessage('Attachment removed'); setState(() { - _inProcess = false; + _attachment = ''; }); } } + void _viewFile(String fileUrl) { + String extension = fileUrl.split('.').last.toLowerCase(); + if (_imageExtensions.contains(extension) || extension == FieldType.pdf) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FileViewer( + fileType: extension == FieldType.pdf + ? FieldType.pdf + : FieldType.image, + fileUrl: fileUrl, + )), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FileViewer( + fileType: '', + fileUrl: fileUrl, + )), + ); + } + } + @override Widget build(BuildContext context) { return SingleChildScrollView( @@ -275,22 +336,46 @@ class _LeadInspectorApplicationFieldState ), ), Container( - margin: const EdgeInsets.only(top: 10), - padding: const EdgeInsets.fromLTRB(15, 10, 15, 10), - width: double.infinity, - decoration: BoxDecoration( - border: Border.all(color: AppColors.black16), - ), - child: Text( - widget.fieldData.keys.elementAt(0), - style: GoogleFonts.lato( - color: AppColors.black87, - fontSize: 14.0, - letterSpacing: 0.25, - fontWeight: FontWeight.w400, + margin: const EdgeInsets.only(top: 10), + padding: widget.fieldType == FieldType.file + ? const EdgeInsets.fromLTRB(10, 10, 10, 10) + : const EdgeInsets.fromLTRB(15, 10, 15, 10), + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: AppColors.black16), ), - ), - ) + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.fieldData.keys.elementAt(0), + style: GoogleFonts.lato( + color: AppColors.black87, + fontSize: 14.0, + letterSpacing: 0.25, + fontWeight: FontWeight.w400, + ), + ), + widget.fieldType == FieldType.file + ? InkWell( + onTap: () => _viewFile( + widget.fieldData.keys.elementAt(0)), + child: Padding( + padding: + const EdgeInsets.only(top: 10), + child: Text( + AppLocalizations.of(context)! + .preview, + style: GoogleFonts.lato( + color: AppColors.primaryBlue, + fontSize: 14.0, + letterSpacing: 0.25, + fontWeight: FontWeight.w700, + ), + ))) + : const Center(), + ], + )) ], ), ), @@ -457,13 +542,13 @@ class _LeadInspectorApplicationFieldState padding: const EdgeInsets.only(left: 0), child: IconButton( onPressed: () { - // if (widget.applicationStatus != - // InspectionStatus - // .inspectionCompleted && - // _radioValue != - // FieldValue.correct) { - _photoOptions(context); - // } + if (widget.applicationStatus != + InspectionStatus + .inspectionCompleted && + _radioValue != + FieldValue.correct) { + _photoOptions(context); + } }, icon: _radioValue != FieldValue.correct @@ -479,6 +564,22 @@ class _LeadInspectorApplicationFieldState ) ], )), + _radioValue != FieldValue.correct && + _summaryText != '' + ? Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.only(top: 20), + child: Text( + AppLocalizations.of(context)! + .reasonForIncorrectSelection, + style: GoogleFonts.lato( + color: AppColors.black60, + fontWeight: FontWeight.w700, + fontSize: 14.0, + letterSpacing: 0.25, + ), + )) + : const Center(), _radioValue != FieldValue.correct && _summaryText != '' ? Container( @@ -509,7 +610,8 @@ class _LeadInspectorApplicationFieldState width: MediaQuery.of(context).size.width, padding: const EdgeInsets.only(top: 20), child: Text( - widget.fieldName, + AppLocalizations.of(context)! + .actualValue, style: GoogleFonts.lato( color: AppColors.black60, fontWeight: FontWeight.w700, @@ -542,6 +644,110 @@ class _LeadInspectorApplicationFieldState ), ) : const Center(), + _radioValue != FieldValue.correct && + _attachment != '' + ? Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.only(top: 20), + child: Text( + AppLocalizations.of(context)! + .attachment, + style: GoogleFonts.lato( + color: AppColors.black60, + fontWeight: FontWeight.w700, + fontSize: 14.0, + letterSpacing: 0.25, + ), + )) + : const Center(), + _radioValue != FieldValue.correct && + _attachment != '' + ? Container( + margin: const EdgeInsets.only( + top: 10, + ), + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 10), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: AppColors.black16), + borderRadius: BorderRadius.circular(4), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + _attachment, + style: GoogleFonts.lato( + color: AppColors.black87, + fontSize: 14.0, + letterSpacing: 0.25, + fontWeight: FontWeight.w400, + ), + ), + Row( + children: [ + InkWell( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => + FileViewer( + fileType: + FieldType + .image, + fileUrl: + _attachment, + ))), + child: Padding( + padding: + const EdgeInsets + .only(top: 10), + child: Text( + AppLocalizations.of( + context)! + .preview, + style: + GoogleFonts.lato( + color: AppColors + .primaryBlue, + fontSize: 14.0, + letterSpacing: 0.25, + fontWeight: + FontWeight.w700, + ), + ))), + const Spacer(), + InkWell( + onTap: () => + _deleteAttachment( + _attachment), + child: Padding( + padding: + const EdgeInsets + .only(top: 10), + child: Text( + AppLocalizations.of( + context)! + .remove, + style: + GoogleFonts.lato( + color: AppColors + .sentForIns, + fontSize: 14.0, + letterSpacing: 0.25, + fontWeight: + FontWeight.w700, + ), + ))), + ], + ), + ])) + : const Center(), ], ))), ]))); diff --git a/lib/widgets/lead_inspector_dialog.dart b/lib/widgets/lead_inspector_dialog.dart index 0f7ad6971eb3a72713892d279a74d9f1a4f341eb..1f5faaf45f1e8a7906d21e296e6652f8ac6779c8 100644 --- a/lib/widgets/lead_inspector_dialog.dart +++ b/lib/widgets/lead_inspector_dialog.dart @@ -67,7 +67,7 @@ class _LeadInspectorDialogState extends State<LeadInspectorDialog> { Helper.toastMessage('Please enter reason'); return; } - if (_inspectionValue == '' && widget.fieldType != FieldType.file) { + if (_inspectionValue == '') { Helper.toastMessage('Please enter actual value'); return; } @@ -157,24 +157,23 @@ class _LeadInspectorDialogState extends State<LeadInspectorDialog> { ), ), ), - widget.fieldType != FieldType.file - ? Padding( - padding: const EdgeInsets.only(top: 20), - child: Text( - 'Actual value(s)', - style: GoogleFonts.lato( - color: AppColors.black87, - fontWeight: FontWeight.w700, - fontSize: 14, - letterSpacing: 0.25, - ), - )) - : const Center(), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text( + 'Actual value(s)', + style: GoogleFonts.lato( + color: AppColors.black87, + fontWeight: FontWeight.w700, + fontSize: 14, + letterSpacing: 0.25, + ), + )), widget.fieldType == FieldType.text || widget.fieldType == FieldType.numeric || widget.fieldType == FieldType.email || widget.fieldType == FieldType.date || - widget.fieldType == FieldType.textarea + widget.fieldType == FieldType.textarea || + widget.fieldType == FieldType.file ? TextQuestion( fieldType: widget.fieldType, answerGiven: widget.inspectionValue, diff --git a/pubspec.lock b/pubspec.lock index 7fbb4152118d06ed5541edcfb8376ce44cec2845..fb2e5f708a008e0f14875ad1077dd332fcc9d659 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,6 +141,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.1" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.4" email_validator: dependency: "direct main" description: @@ -461,7 +468,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" path_provider_android: dependency: transitive description: @@ -635,6 +642,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.3" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 49851ba51170db0cc7612c6eb51445bf590158b0..00a18f9e21cc1199ef9b1d2e27e33adb51ac3c72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,8 @@ dependencies: email_validator: ^2.0.1 image_picker: ^0.8.4+11 image_cropper: ^1.5.0 + dio: ^4.0.4 + webview_flutter: ^3.0.1 dev_dependencies: flutter_test: