diff --git a/core/lib/core.dart b/core/lib/core.dart index 596285f9c..b25e073b8 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -45,6 +45,7 @@ export 'utils/file_utils.dart'; export 'utils/option_param_mixin.dart'; export 'utils/print_utils.dart'; export 'utils/broadcast_channel/broadcast_channel.dart'; +export 'utils/list_utils.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/utils/list_utils.dart b/core/lib/utils/list_utils.dart new file mode 100644 index 000000000..9449a9e23 --- /dev/null +++ b/core/lib/utils/list_utils.dart @@ -0,0 +1,16 @@ +import 'package:dartz/dartz.dart'; + +Tuple2, List> partition(List list, bool Function(T) predicate) { + List trueList = []; + List falseList = []; + + for (var element in list) { + if (predicate(element)) { + trueList.add(element); + } else { + falseList.add(element); + } + } + + return Tuple2(trueList, falseList); +} \ No newline at end of file diff --git a/lib/features/composer/domain/state/download_image_as_base64_state.dart b/lib/features/composer/domain/state/download_image_as_base64_state.dart index af801f1a2..9370e13a6 100644 --- a/lib/features/composer/domain/state/download_image_as_base64_state.dart +++ b/lib/features/composer/domain/state/download_image_as_base64_state.dart @@ -10,15 +10,11 @@ class DownloadImageAsBase64Success extends UIState { final String base64Uri; final String cid; final FileInfo fileInfo; - final bool fromFileShared; DownloadImageAsBase64Success( this.base64Uri, this.cid, - this.fileInfo, - { - this.fromFileShared = false - } + this.fileInfo ); @override @@ -26,7 +22,6 @@ class DownloadImageAsBase64Success extends UIState { base64Uri, cid, fileInfo, - fromFileShared, ]; } diff --git a/lib/features/composer/domain/state/upload_attachment_state.dart b/lib/features/composer/domain/state/upload_attachment_state.dart index c58d04d11..08f4e9f9a 100644 --- a/lib/features/composer/domain/state/upload_attachment_state.dart +++ b/lib/features/composer/domain/state/upload_attachment_state.dart @@ -1,46 +1,25 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; class UploadAttachmentSuccess extends UIState { final UploadAttachment uploadAttachment; - final bool isInline; - final bool fromFileShared; - UploadAttachmentSuccess( - this.uploadAttachment, - { - this.isInline = false, - this.fromFileShared = false, - } - ); + UploadAttachmentSuccess(this.uploadAttachment); @override - List get props => [ - uploadAttachment, - isInline, - fromFileShared, - ]; + List get props => [uploadAttachment]; } class UploadAttachmentFailure extends FeatureFailure { - final bool isInline; - final bool fromFileShared; - UploadAttachmentFailure( - dynamic exception, - { - this.isInline = false, - this.fromFileShared = false, - } - ) : super(exception: exception); + final FileInfo fileInfo; + + UploadAttachmentFailure(dynamic exception, this.fileInfo) : super(exception: exception); @override - List get props => [ - isInline, - fromFileShared, - ...super.props - ]; + List get props => [...super.props, fileInfo]; } \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart index b3fb5d0ac..b3417daa1 100644 --- a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart +++ b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart @@ -16,7 +16,6 @@ class DownloadImageAsBase64Interactor { { double? maxWidth, bool? compress, - bool fromFileShared = false, } ) async* { try { @@ -33,7 +32,6 @@ class DownloadImageAsBase64Interactor { result!, cid, fileInfo, - fromFileShared: fromFileShared )); } else { yield Left(DownloadImageAsBase64Failure(null)); diff --git a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart index 6abdcf801..ad1b69da5 100644 --- a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart +++ b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart @@ -15,8 +15,6 @@ class UploadAttachmentInteractor { FileInfo fileInfo, Uri uploadUri, { CancelToken? cancelToken, - bool isInline = false, - bool fromFileShared = false, }) async* { try { final uploadAttachment = await _composerRepository.uploadAttachment( @@ -24,17 +22,9 @@ class UploadAttachmentInteractor { uploadUri, cancelToken: cancelToken ); - yield Right(UploadAttachmentSuccess( - uploadAttachment, - isInline: isInline, - fromFileShared: fromFileShared - )); + yield Right(UploadAttachmentSuccess(uploadAttachment)); } catch (e) { - yield Left(UploadAttachmentFailure( - e, - isInline: isInline, - fromFileShared: fromFileShared - )); + yield Left(UploadAttachmentFailure(e, fileInfo)); } } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index dda6062d2..6f9bc469a 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_ import 'package:tmail_ui_user/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart'; import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -125,7 +126,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find())); Get.lazyPut(() => RemoteServerSettingsDataSourceImpl( - Get.find(), + Get.find(), Get.find())); } @@ -182,6 +183,7 @@ class ComposerBindings extends BaseBindings { @override void bindingsInteractor() { Get.lazyPut(() => LocalFilePickerInteractor()); + Get.lazyPut(() => LocalImagePickerInteractor()); Get.lazyPut(() => UploadAttachmentInteractor(Get.find())); Get.lazyPut(() => SaveEmailAsDraftsInteractor( Get.find(), @@ -207,6 +209,7 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => ComposerController( Get.find(), Get.find(), + Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 8b8a295d4..2e878affb 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; @@ -51,8 +51,9 @@ import 'package:tmail_ui_user/features/composer/presentation/controller/rich_tex import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft_arguments.dart'; @@ -69,6 +70,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; @@ -79,18 +81,21 @@ import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email. import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; -import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; +import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:universal_html/html.dart' as html; -class ComposerController extends BaseController { +class ComposerController extends BaseController with DragDropFileMixin { final mailboxDashBoardController = Get.find(); final richTextMobileTabletController = Get.find(); @@ -116,6 +121,7 @@ class ComposerController extends BaseController { final listFromIdentities = RxList(); final LocalFilePickerInteractor _localFilePickerInteractor; + final LocalImagePickerInteractor _localImagePickerInteractor; final DeviceInfoPlugin _deviceInfoPlugin; final GetEmailContentInteractor _getEmailContentInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; @@ -155,6 +161,12 @@ class ComposerController extends BaseController { FocusNode? bccAddressFocusNode; FocusNode? searchIdentitiesFocusNode; + StreamSubscription? _subscriptionOnBeforeUnload; + StreamSubscription? _subscriptionOnDragEnter; + StreamSubscription? _subscriptionOnDragOver; + StreamSubscription? _subscriptionOnDragLeave; + StreamSubscription? _subscriptionOnDrop; + final RichTextController keyboardRichTextController = RichTextController(); final ScrollController scrollController = ScrollController(); @@ -178,6 +190,7 @@ class ComposerController extends BaseController { ComposerController( this._deviceInfoPlugin, this._localFilePickerInteractor, + this._localImagePickerInteractor, this._getEmailContentInteractor, this._getAllIdentitiesInteractor, this.uploadController, @@ -201,7 +214,7 @@ class ComposerController extends BaseController { }); } else { WidgetsBinding.instance.addPostFrameCallback((_) { - _listenBrowserTabRefresh(); + _listenBrowserEventAction(); }); } _getAlwaysReadReceiptSetting(); @@ -225,6 +238,11 @@ class ComposerController extends BaseController { emailContentsViewState.value = Right(UIClosedState()); identitySelected.value = null; listFromIdentities.clear(); + _subscriptionOnBeforeUnload?.cancel(); + _subscriptionOnDragEnter?.cancel(); + _subscriptionOnDragOver?.cancel(); + _subscriptionOnDragLeave?.cancel(); + _subscriptionOnDrop?.cancel(); if (PlatformInfo.isMobile) { FkUserAgent.release(); } @@ -267,6 +285,8 @@ class ComposerController extends BaseController { emailContentsViewState.value = Right(success); } else if (success is LocalFilePickerSuccess) { _handlePickFileSuccess(success); + } else if (success is LocalImagePickerSuccess) { + _handlePickImageSuccess(success); } else if (success is GetEmailContentSuccess) { _getEmailContentSuccess(success); } else if (success is GetEmailContentFromCacheSuccess) { @@ -274,23 +294,11 @@ class ComposerController extends BaseController { } else if (success is GetAllIdentitiesSuccess) { _handleGetAllIdentitiesSuccess(success); } else if (success is DownloadImageAsBase64Success) { + final inlineImage = InlineImage(fileInfo: success.fileInfo, base64Uri: success.base64Uri); if (PlatformInfo.isWeb) { - richTextWebController.insertImage( - InlineImage( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri)); + richTextWebController.insertImage(inlineImage); } else { - richTextMobileTabletController.insertImage( - InlineImage( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri - ), - fromFileShare: success.fromFileShared - ); + richTextMobileTabletController.insertImage(inlineImage); } maxWithEditor = null; } else if (success is GetAlwaysReadReceiptSettingSuccess) { @@ -301,8 +309,10 @@ class ComposerController extends BaseController { @override void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); - if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) { - _pickFileFailure(failure); + if (failure is LocalFilePickerFailure) { + _handlePickFileFailure(failure); + } else if (failure is LocalImagePickerFailure) { + _handlePickImageFailure(failure); } else if (failure is GetEmailContentFailure || failure is TransformHtmlEmailContentFailure) { emailContentsViewState.value = Left(failure); @@ -358,8 +368,9 @@ class ComposerController extends BaseController { }); } - void _listenBrowserTabRefresh() { - html.window.onBeforeUnload.listen((event) async { + void _listenBrowserEventAction() { + log('ComposerController::_listenBrowserEventAction:'); + _subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async { final userProfile = mailboxDashBoardController.userProfile.value; _removeComposerCacheOnWebInteractor.execute(); if (userProfile != null) { @@ -367,6 +378,22 @@ class ComposerController extends BaseController { _saveComposerCacheOnWebInteractor.execute(draftEmail); } }); + + _subscriptionOnDragEnter = html.window.onDragEnter.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + }); + + _subscriptionOnDragOver = html.window.onDragOver.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + }); + + _subscriptionOnDragLeave = html.window.onDragLeave.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; + }); + + _subscriptionOnDrop = html.window.onDrop.listen((event) { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; + }); } void _scrollControllerEmailAddressListener() { @@ -1064,29 +1091,45 @@ class ComposerController extends BaseController { consumeState(_localFilePickerInteractor.execute(fileType: fileType)); } - void _pickFileFailure(Failure failure) { - if (failure is LocalFilePickerFailure) { - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments); - } + void _handlePickFileFailure(LocalFilePickerFailure failure) { + if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).thisFileCannotBePicked); + } + } + + void _handlePickImageFailure(LocalImagePickerFailure failure) { + if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) { + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).cannotSelectThisImage); } } void _handlePickFileSuccess(LocalFilePickerSuccess success) { uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: success.pickedFiles.totalSize, - callbackAction: () => _uploadAttachmentsAction(success.pickedFiles) + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: success.pickedFiles) ); } - void _uploadAttachmentsAction(List pickedFiles) { + void _handlePickImageSuccess(LocalImagePickerSuccess success) { + uploadController.validateTotalSizeInlineAttachmentsBeforeUpload( + totalSizePreparedFiles: success.fileInfo.fileSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()]) + ); + } + + void _uploadAttachmentsAction({required List pickedFiles}) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); - uploadController.justUploadAttachmentsAction(pickedFiles, uploadUri); + uploadController.justUploadAttachmentsAction( + uploadFiles: pickedFiles, + uploadUri: uploadUri, + ); } else { log('ComposerController::_uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); } @@ -1237,71 +1280,17 @@ class ComposerController extends BaseController { ); } - File _covertSharedMediaFileToFile(SharedMediaFile sharedMediaFile) { - return File( - Platform.isIOS - ? sharedMediaFile.type == SharedMediaType.FILE - ? sharedMediaFile.path.toString().replaceAll('file:/', '').replaceAll('%20', ' ') - : sharedMediaFile.path.toString().replaceAll('%20', ' ') - : sharedMediaFile.path, - ); - } - - FileInfo _covertFileToFileInfo(File file) { - return FileInfo( - file.path.split('/').last, - file.path, - file.existsSync() ? file.lengthSync() : 0, - ); - } - - List covertListSharedMediaFileToInlineImage(List value) { - List newFiles = List.empty(growable: true); - if (value.isNotEmpty) { - for (var element in value) { - newFiles.add(_covertSharedMediaFileToFile(element)); - } - } - - final List listInlineImage = newFiles.map( - (e) => InlineImage( - ImageSource.local, - fileInfo: _covertFileToFileInfo(e), - ) - ).toList(); - return listInlineImage; - } - - List covertListSharedMediaFileToFileInfo(List value) { - List newFiles = List.empty(growable: true); - if (value.isNotEmpty) { - for (var element in value) { - newFiles.add(_covertSharedMediaFileToFile(element)); - } - } - - final List listFileInfo = newFiles.map( - (e) => _covertFileToFileInfo(e), - ).toList(); - return listFileInfo; - } - void _addAttachmentFromFileShare(List listSharedMediaFile) { - final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); - final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE); - if (listImageSharedMediaFile.isNotEmpty) { - final listInlineImage = covertListSharedMediaFileToInlineImage(listSharedMediaFile); - for (var e in listInlineImage) { - _uploadInlineAttachmentsAction(e.fileInfo!, fromFileShared: true); - } - } - if (listFileAttachmentSharedMediaFile.isNotEmpty) { - final listFileInfo = covertListSharedMediaFileToFileInfo(listSharedMediaFile); - uploadController.validateTotalSizeAttachmentsBeforeUpload( - totalSizePreparedFiles: listFileInfo.totalSize, - callbackAction: () => _uploadAttachmentsAction(listFileInfo) - ); - } + final listFileInfo = listSharedMediaFile.toListFileInfo(isShared: true); + + final tupleListFileInfo = partition(listFileInfo, (fileInfo) => fileInfo.isInline == true); + final listAttachments = tupleListFileInfo.value2; + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); } void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) { @@ -1712,80 +1701,8 @@ class ComposerController extends BaseController { } else { maxWithEditor = maxWith - 120; } - final inlineImage = await _selectFromFile(); - if (inlineImage != null) { - if (PlatformInfo.isWeb) { - _insertImageOnWeb(inlineImage); - } else { - _insertImageOnMobileAndTablet(inlineImage); - } - } else { - if (context.mounted) { - appToast.showToastErrorMessage(context, AppLocalizations.of(context).cannotSelectThisImage); - } - } - } - Future _selectFromFile() async { - final filePickerResult = await FilePicker.platform.pickFiles( - type: FileType.image, - withData: PlatformInfo.isWeb - ); - if (filePickerResult?.files.isNotEmpty == true) { - PlatformFile platformFile = filePickerResult!.files.first; - final fileSelected = FileInfo( - platformFile.name, - PlatformInfo.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - bytes: PlatformInfo.isWeb ? platformFile.bytes : null, - ); - return InlineImage(ImageSource.local, fileInfo: fileSelected); - } - - return null; - } - - void _insertImageOnWeb(InlineImage inlineImage) { - if (inlineImage.source == ImageSource.local) { - _uploadInlineAttachmentsAction(inlineImage.fileInfo!); - } else { - richTextWebController.insertImage(inlineImage); - } - } - - void _insertImageOnMobileAndTablet(InlineImage inlineImage) { - if (inlineImage.source == ImageSource.local) { - _uploadInlineAttachmentsAction(inlineImage.fileInfo!); - } else { - richTextMobileTabletController.insertImage(inlineImage); - } - } - - void _uploadInlineAttachmentsAction(FileInfo pickedFile, {bool fromFileShared = false}) async { - if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: pickedFile.fileSize)) { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - if (session != null && accountId != null) { - final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); - uploadController.uploadFileAction( - pickedFile, - uploadUri, - isInline: true, - fromFileShared: fromFileShared - ); - } - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - onConfirmAction: () => {isSendEmailLoading.value = false}, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false); - } - } + consumeState(_localImagePickerInteractor.execute()); } void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { @@ -1802,7 +1719,6 @@ class ComposerController extends BaseController { uploadState.attachment.cid!, uploadState.fileInfo, maxWidth: maxWithEditor, - fromFileShared: uploadState.fromFileShared, )); } } @@ -1896,6 +1812,7 @@ class ComposerController extends BaseController { log('ComposerController::handleInitHtmlEditorWeb:'); _isEmailBodyLoaded = true; richTextWebController.editorController.setFullScreen(); + richTextWebController.editorController.setOnDragDropEvent(); onChangeTextEditorWeb(initContent); richTextWebController.setEnableCodeView(); if (identitySelected.value == null) { @@ -1921,10 +1838,25 @@ class ComposerController extends BaseController { void handleImageUploadSuccess ( BuildContext context, - web_html_editor.FileUpload fileUpload + List listFileUpload ) async { - log('ComposerController::handleImageUploadSuccess:NAME: ${fileUpload.name} | TYPE: ${fileUpload.type} | SIZE: ${fileUpload.size}'); - if (fileUpload.base64 == null) { + log('ComposerController::handleImageUploadSuccess: COUNT_FILE_UPLOADED = ${listFileUpload.length}'); + List listFileInfo = []; + + await Future.forEach(listFileUpload, (fileUpload) async { + if (fileUpload.base64?.isNotEmpty == true) { + final fileInfo = await fileUpload.toFileInfo(); + if (fileInfo != null) { + if (fileInfo.mimeType.startsWith(MediaTypeExtension.imageType) == true) { + listFileInfo.add(fileInfo.withInline()); + } else { + listFileInfo.add(fileInfo); + } + } + } + }); + + if (listFileInfo.isEmpty && context.mounted) { appToast.showToastErrorMessage( context, AppLocalizations.of(context).can_not_upload_this_file_as_attachments @@ -1932,49 +1864,30 @@ class ComposerController extends BaseController { return; } - if (fileUpload.type?.startsWith(Constant.imageType) == true) { - final fileInfo = await fileUpload.toFileInfo(); - if (fileInfo != null) { - _uploadInlineAttachmentsAction(fileInfo); - } else if (context.mounted) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - } - } else { - final fileInfo = await fileUpload.toFileInfo(); - if (fileInfo != null) { - _addAttachmentFromDragAndDrop(fileInfo: fileInfo); - } else if (context.mounted) { - appToast.showToastErrorMessage( - context, - AppLocalizations.of(context).can_not_upload_this_file_as_attachments - ); - } - } + final listAttachments = listFileInfo + .where((fileInfo) => fileInfo.isInline != true) + .toList(); + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); } void handleImageUploadFailure({ required BuildContext context, required web_html_editor.UploadError uploadError, - web_html_editor.FileUpload? fileUpload, + List? listFileUpload, String? base64Str, }) { - logError('ComposerController::handleImageUploadFailure:fileUpload: $fileUpload | uploadError: $uploadError'); + logError('ComposerController::handleImageUploadFailure: COUNT_FILE_FAILED = ${listFileUpload?.length} | ERROR = $uploadError'); appToast.showToastErrorMessage( context, '${AppLocalizations.of(context).can_not_upload_this_file_as_attachments}. (${uploadError.name})' ); } - void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) { - uploadController.validateTotalSizeAttachmentsBeforeUpload( - totalSizePreparedFiles: fileInfo.fileSize, - callbackAction: () => _uploadAttachmentsAction([fileInfo]) - ); - } - FocusNode? getNextFocusOfToEmailAddress() { if (ccRecipientState.value == PrefixRecipientState.enabled) { return ccAddressFocusNode; @@ -2066,11 +1979,11 @@ class ComposerController extends BaseController { _updateStatusEmailSendButton(); } - void addAttachmentWhenDragFromOtherEmail(Attachment attachment) { - log('ComposerController::addAttachmentWhenDragFromOtherEmail: attachment = $attachment'); + void onAttachmentDropZoneListener(Attachment attachment) { + log('ComposerController::onAttachmentDropZoneListener: attachment = $attachment'); uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: attachment.size?.value ?? 0, - callbackAction: () => uploadController.initializeUploadAttachments([attachment]) + onValidationSuccess: () => uploadController.initializeUploadAttachments([attachment]) ); } @@ -2144,4 +2057,33 @@ class ComposerController extends BaseController { consumeState(_getAlwaysReadReceiptSettingInteractor.execute(accountId)); } } + + void handleOnDragEnterHtmlEditorWeb() { + mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; + } + + void onLocalFileDropZoneListener({ + required BuildContext context, + required DropDoneDetails details + }) async { + final listFileInfo = await onDragDone(context: context, details: details); + + if (listFileInfo.isEmpty && context.mounted) { + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).can_not_upload_this_file_as_attachments + ); + return; + } + + final listAttachments = listFileInfo + .where((fileInfo) => fileInfo.isInline != true) + .toList(); + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 3cf878816..ccae48433 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -17,10 +17,11 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/web/drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/from_composer_drop_down_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -157,72 +158,110 @@ class ComposerView extends GetWidget { margin: ComposerStyle.mobileSubjectMargin, ), Expanded( - child: Stack( - children: [ - Padding( - padding: ComposerStyle.mobileEditorPadding, - child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, - ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, - )), - ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.mobileEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + )), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + Obx(() { + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details + ), + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ); + } ), ), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow - ), - ); - } else { - return const SizedBox.shrink(); - } - }) ] ), ); @@ -336,123 +375,149 @@ class ComposerView extends GetWidget { margin: ComposerStyle.desktopSubjectMargin, ), Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ComposerStyle.borderColor, - width: 1 - ) - ), - color: ComposerStyle.backgroundEditorColor - ), - child: Stack( - children: [ - Column( - children: [ - Expanded( - child: Padding( - padding: ComposerStyle.desktopEditorPadding, - child: Obx(() { - return Stack( - children: [ - WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, - ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, - ), - if (controller.mailboxDashBoardController.isDraggableAppActive) - PointerInterceptor( - child: DropZoneWidget( - width: constraints.maxWidth, - height: constraints.maxHeight, - addAttachmentFromDropZone: controller.addAttachmentWhenDragFromOtherEmail, - ) - ) - ], - ); - }), - ), - ), - Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Container( decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) + ), + color: ComposerStyle.backgroundEditorColor ), - ); - } else { - return const SizedBox.shrink(); - } - }) - ], - ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], - ), + child: Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.desktopEditorPadding, + child: Obx(() { + return WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + ); + }), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + ), + ), + Obx(() => BottomBarComposerWidget( + isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + attachFileAction: () => controller.openFilePickerByType(context, FileType.any), + insertImageAction: () => controller.insertImage(context, constraints.maxWidth), + showCodeViewAction: controller.richTextWebController.toggleCodeView, + deleteComposerAction: () => controller.handleClickDeleteComposer(context), + saveToDraftAction: () => controller.saveToDraftAction(context), + sendMessageAction: () => controller.validateInformationBeforeSending(context), + requestReadReceiptAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createReadReceiptPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + isSending: controller.isSendEmailLoading.value, + )), + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + Obx(() { + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details + ), + ) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + ], + ); + } ), ), - Obx(() => BottomBarComposerWidget( - isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, - isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, - openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, - attachFileAction: () => controller.openFilePickerByType(context, FileType.any), - insertImageAction: () => controller.insertImage(context, constraints.maxWidth), - showCodeViewAction: controller.richTextWebController.toggleCodeView, - deleteComposerAction: () => controller.handleClickDeleteComposer(context), - saveToDraftAction: () => controller.saveToDraftAction(context), - sendMessageAction: () => controller.validateInformationBeforeSending(context), - requestReadReceiptAction: (position) { - controller.openPopupMenuAction( - context, - position, - _createReadReceiptPopupItems(context), - radius: ComposerStyle.popupMenuRadius - ); - }, - isSending: controller.isSendEmailLoading.value, - )), ]), ); }, @@ -567,87 +632,119 @@ class ComposerView extends GetWidget { margin: ComposerStyle.tabletSubjectMargin, ), Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ComposerStyle.borderColor, - width: 1 - ) - ), - color: ComposerStyle.backgroundEditorColor - ), - child: Stack( - children: [ - Column( + child: LayoutBuilder( + builder: (context, constraintsEditor) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) + ), + color: ComposerStyle.backgroundEditorColor + ), + child: Stack( children: [ - Expanded( - child: Padding( - padding: ComposerStyle.tabletEditorPadding, - child: Obx(() => WebEditorView( - editorController: controller.richTextWebController.editorController, - arguments: controller.composerArguments.value, - contentViewState: controller.emailContentsViewState.value, - currentWebContent: controller.textEditorWeb, - onInitial: controller.handleInitHtmlEditorWeb, - onChangeContent: controller.onChangeTextEditorWeb, - onFocus: controller.handleOnFocusHtmlEditorWeb, - onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, - onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, - onEditorSettings: controller.richTextWebController.onEditorSettingsChange, - onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), - onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { - return controller.handleImageUploadFailure( - context: context, - uploadError: uploadError, - fileUpload: fileUpload, - base64Str: base64Str, + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.tabletEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, + width: constraints.maxWidth, + height: constraints.maxHeight, + onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + )), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, ); - }, - onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, - width: constraints.maxWidth, - height: constraints.maxHeight, - )), - ), + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), ), Obx(() { - if (controller.uploadController.listUploadAttachments.isNotEmpty) { - return AttachmentComposerWidget( - listFileUploaded: controller.uploadController.listUploadAttachments, - isCollapsed: controller.isAttachmentCollapsed, - onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), - onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: AttachmentDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener, + ) + ), ); } else { return const SizedBox.shrink(); } }), Obx(() { - if (controller.richTextWebController.isFormattingOptionsEnabled) { - return ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: ComposerStyle.richToolbarPadding, - decoration: const BoxDecoration( - color: ComposerStyle.richToolbarColor, - boxShadow: ComposerStyle.richToolbarShadow + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) { + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraintsEditor.maxWidth, + height: constraintsEditor.maxHeight, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details + ), + ) ), ); } else { return const SizedBox.shrink(); } - }) + }), ], ), - Align( - alignment: AlignmentDirectional.topCenter, - child: Obx(() => InsertImageLoadingBarWidget( - uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, - viewState: controller.viewState.value, - padding: ComposerStyle.insertImageLoadingBarPadding, - )), - ), - ], - ), + ); + } ), ), Obx(() => BottomBarComposerWidget( diff --git a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart index 84b6d98a4..269c5503e 100644 --- a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart @@ -6,27 +6,17 @@ import 'package:file_picker/file_picker.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/base_rich_text_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; class RichTextMobileTabletController extends BaseRichTextController { HtmlEditorApi? htmlEditorApi; - void insertImage( - InlineImage image, - { - double? maxWithEditor, - bool fromFileShare = false + void insertImage(InlineImage inlineImage) async { + if (inlineImage.fileInfo.isShared == true) { + await htmlEditorApi?.moveCursorAtLastNode(); } - ) async { - log('RichTextMobileTabletController::insertImage(): $image | maxWithEditor: $maxWithEditor | $fromFileShare'); - if (image.source == ImageSource.network) { - htmlEditorApi?.insertImageLink(image.link!); - } else { - if (fromFileShare) { - await htmlEditorApi?.moveCursorAtLastNode(); - } - await htmlEditorApi?.insertHtml(image.base64Uri ?? ''); + if (inlineImage.base64Uri?.isNotEmpty == true) { + await htmlEditorApi?.insertHtml(inlineImage.base64Uri!); } } diff --git a/lib/features/composer/presentation/controller/rich_text_web_controller.dart b/lib/features/composer/presentation/controller/rich_text_web_controller.dart index 782aca1e4..bb8bd07fb 100644 --- a/lib/features/composer/presentation/controller/rich_text_web_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_web_controller.dart @@ -15,7 +15,6 @@ import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/order_list_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/paragraph_type.dart'; @@ -194,13 +193,8 @@ class RichTextWebController extends BaseRichTextController { bool isTextStyleTypeSelected(RichTextStyleType richTextStyleType) => listTextStyleApply.contains(richTextStyleType); - void insertImage(InlineImage image) async { - log('RichTextWebController::insertImage(): $image'); - if (image.source == ImageSource.network) { - editorController.insertNetworkImage(image.link!); - } else { - editorController.insertHtml("
${image.base64Uri ?? ''}

"); - } + void insertImage(InlineImage inlineImage) { + editorController.insertHtml("
${inlineImage.base64Uri ?? ''}

"); } void applyNewFontStyle(FontNameType? newFont) { diff --git a/lib/features/composer/presentation/extensions/file_extension.dart b/lib/features/composer/presentation/extensions/file_extension.dart new file mode 100644 index 000000000..a5d447051 --- /dev/null +++ b/lib/features/composer/presentation/extensions/file_extension.dart @@ -0,0 +1,19 @@ + +import 'dart:io'; + +import 'package:model/upload/file_info.dart'; + +extension FileExtension on File { + FileInfo toFileInfo({ + bool? isInline, + bool? isShared + }) { + return FileInfo( + fileName: path.split('/').last, + fileSize: existsSync() ? lengthSync() : 0, + filePath: path, + isInline: isInline, + isShared: isShared + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/file_upload_extension.dart b/lib/features/composer/presentation/extensions/file_upload_extension.dart index 3f6e26c25..ec8707c7c 100644 --- a/lib/features/composer/presentation/extensions/file_upload_extension.dart +++ b/lib/features/composer/presentation/extensions/file_upload_extension.dart @@ -29,7 +29,8 @@ extension FileUploadExtension on FileUpload { return FileInfo.fromBytes( bytes: bytes, name: name, - size: size + size: size, + type: type, ); } else { return null; diff --git a/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart b/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart new file mode 100644 index 000000000..482196e1d --- /dev/null +++ b/lib/features/composer/presentation/extensions/list_shared_media_file_extension.dart @@ -0,0 +1,8 @@ + +import 'package:model/upload/file_info.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/shared_media_file_extension.dart'; + +extension ListSharedMediaFileExtension on List { + List toListFileInfo({bool? isShared}) => map((sharedMediaFile) => sharedMediaFile.toFileInfo(isShared: isShared)).toList(); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/shared_media_file_extension.dart b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart new file mode 100644 index 000000000..93f6caa37 --- /dev/null +++ b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart @@ -0,0 +1,22 @@ + +import 'dart:io'; + +import 'package:core/utils/platform_info.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/file_extension.dart'; + +extension SharedMediaFileExtension on SharedMediaFile { + File toFile() { + if (PlatformInfo.isIOS) { + final pathFile = type == SharedMediaType.FILE + ? path.toString().replaceAll('file:/', '').replaceAll('%20', ' ') + : path.toString().replaceAll('%20', ' '); + return File(pathFile); + } else { + return File(path); + } + } + + FileInfo toFileInfo({bool? isShared}) => toFile().toFileInfo(isInline: type == SharedMediaType.IMAGE, isShared: isShared); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart new file mode 100644 index 000000000..44b763109 --- /dev/null +++ b/lib/features/composer/presentation/mixin/drag_drog_file_mixin.dart @@ -0,0 +1,52 @@ +import 'dart:async' as async; +import 'package:async/async.dart'; +import 'package:core/domain/extensions/media_type_extension.dart'; +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +mixin DragDropFileMixin { + async.Future> showFutureLoadingDialogFullScreen({ + required BuildContext context, + required async.Future Function() future, + }) async { + return await showFutureLoadingDialog( + context: context, + title: AppLocalizations.of(context).loadingPleaseWait, + backLabel: AppLocalizations.of(context).close, + future: future, + ); + } + + async.Future> onDragDone({ + required BuildContext context, + required DropDoneDetails details + }) async { + final bytesList = await showFutureLoadingDialogFullScreen( + context: context, + future: () => async.Future.wait( + details.files.map( + (xFile) => xFile.readAsBytes(), + ), + ), + ); + + if (bytesList.error != null) return []; + + final listFileInfo = []; + for (var i = 0; i < bytesList.result!.length; i++) { + listFileInfo.add( + FileInfo( + bytes: bytesList.result![i], + fileName: details.files[i].name, + type: details.files[i].mimeType, + fileSize: bytesList.result![i].length, + isInline: details.files[i].mimeType?.startsWith(MediaTypeExtension.imageType) == true + ), + ); + } + return listFileInfo; + } +} diff --git a/lib/features/composer/presentation/model/image_source.dart b/lib/features/composer/presentation/model/image_source.dart deleted file mode 100644 index 6c2cbf3d2..000000000 --- a/lib/features/composer/presentation/model/image_source.dart +++ /dev/null @@ -1,5 +0,0 @@ - -enum ImageSource { - local, - network -} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/inline_image.dart b/lib/features/composer/presentation/model/inline_image.dart index 2dc8e33f0..db16bd6ac 100644 --- a/lib/features/composer/presentation/model/inline_image.dart +++ b/lib/features/composer/presentation/model/inline_image.dart @@ -1,25 +1,19 @@ import 'package:equatable/equatable.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; +import 'package:model/upload/file_info.dart'; class InlineImage with EquatableMixin { - final ImageSource source; - final String? link; - final FileInfo? fileInfo; - final String? cid; + final FileInfo fileInfo; final String? base64Uri; - InlineImage( - this.source, - { - this.link, - this.fileInfo, - this.cid, - this.base64Uri, - } - ); + InlineImage({ + required this.fileInfo, + this.base64Uri, + }); @override - List get props => [source, link, fileInfo, cid, base64Uri]; + List get props => [ + fileInfo, + base64Uri + ]; } \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart index 3389db408..ef82023e2 100644 --- a/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart +++ b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart @@ -10,6 +10,7 @@ class BottomBarComposerWidgetStyle { static const double richTextIconSize = 24; static const double sendButtonRadius = 8; static const double sendButtonIconSpace = 5; + static const double height = 60; static const Color backgroundColor = Colors.white; static const Color iconColor = AppColor.colorRichButtonComposer; @@ -17,7 +18,7 @@ class BottomBarComposerWidgetStyle { static const Color selectedBackgroundColor = AppColor.colorSelected; static const Color selectedIconColor = AppColor.primaryColor; - static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32, vertical: 12); + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32); static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(5); static const EdgeInsetsGeometry sendButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 24); static const EdgeInsetsGeometry richTextIconPadding = EdgeInsetsDirectional.all(2); diff --git a/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart index 033c224f6..35fe78543 100644 --- a/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart +++ b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart'; class DropZoneWidgetStyle { static const double space = 20; @@ -8,11 +9,16 @@ class DropZoneWidgetStyle { static const List dashSize = [6, 3]; - static const Color backgroundColor = AppColor.colorDropZoneBackground; + static Color backgroundColor = AppColor.colorDropZoneBackground.withOpacity(0.7); static const Color borderColor = AppColor.colorDropZoneBorder; static const EdgeInsetsGeometry padding = EdgeInsets.all(20); - static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.symmetric(vertical: 8); + static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only( + bottom: BottomBarComposerWidgetStyle.height, + start: 8, + end: 8, + top: 8 + ); static const TextStyle labelTextStyle = TextStyle( color: Colors.black, diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index e9d2826cc..5da136ef9 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -26,11 +26,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { final VoidCallback? onUnFocus; final OnMouseDownEditorAction? onMouseDown; final OnEditorSettingsChange? onEditorSettings; - final OnImageUploadSuccessAction? onImageUploadSuccessAction; - final OnImageUploadFailureAction? onImageUploadFailureAction; final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? width; final double? height; + final VoidCallback? onDragEnter; const WebEditorView({ super.key, @@ -44,11 +43,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { this.onUnFocus, this.onMouseDown, this.onEditorSettings, - this.onImageUploadSuccessAction, - this.onImageUploadFailureAction, this.onEditorTextSizeChanged, this.width, this.height, + this.onDragEnter, }); @override @@ -71,11 +69,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); case EmailActionType.editDraft: case EmailActionType.editSendingEmail: @@ -97,11 +94,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ), (success) { if (success is GetEmailContentLoading) { @@ -123,11 +119,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } @@ -156,11 +151,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); }, (success) { @@ -185,11 +179,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } @@ -205,11 +198,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { onUnFocus: onUnFocus, onMouseDown: onMouseDown, onEditorSettings: onEditorSettings, - onImageUploadSuccessAction: onImageUploadSuccessAction, - onImageUploadFailureAction: onImageUploadFailureAction, onEditorTextSizeChanged: onEditorTextSizeChanged, width: width, height: height, + onDragEnter: onDragEnter, ); } } diff --git a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart index e8515786e..174621b47 100644 --- a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart @@ -79,7 +79,7 @@ class AttachmentHeaderComposerWidget extends StatelessWidget { margin: const EdgeInsetsDirectional.only(start: 4), padding: const EdgeInsets.all(3), iconColor: AppColor.colorBackgroundQuotasWarning, - tooltipMessage: AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, + tooltipMessage: AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer, backgroundColor: Colors.transparent, ) ], @@ -89,5 +89,5 @@ class AttachmentHeaderComposerWidget extends StatelessWidget { } bool get _isExceedMaximumSizeFileAttachedInComposer => - listFileUploaded.totalSize > AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; + listFileUploaded.totalSize > AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024; } diff --git a/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart b/lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart similarity index 59% rename from lib/features/composer/presentation/widgets/web/drop_zone_widget.dart rename to lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart index 6391feb45..2faae585a 100644 --- a/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart +++ b/lib/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart @@ -3,42 +3,35 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:model/email/attachment.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -typedef OnAddAttachmentFromDropZone = Function(Attachment attachment); +typedef OnAttachmentDropZoneListener = Function(Attachment attachment); -class DropZoneWidget extends StatefulWidget { +class AttachmentDropZoneWidget extends StatelessWidget { + final ImagePaths imagePaths; final double? width; final double? height; - final OnAddAttachmentFromDropZone? addAttachmentFromDropZone; + final OnAttachmentDropZoneListener? onAttachmentDropZoneListener; - const DropZoneWidget({ + const AttachmentDropZoneWidget({ super.key, + required this.imagePaths, this.width, this.height, - this.addAttachmentFromDropZone + this.onAttachmentDropZoneListener }); - @override - State createState() => _DropZoneWidgetState(); -} - -class _DropZoneWidgetState extends State { - - final _imagePaths = Get.find(); - - bool _isDragging = false; - @override Widget build(BuildContext context) { return DragTarget( - builder: (context, candidateData, rejectedData) { - if (_isDragging) { - return Padding( + builder: (context, _, __) { + return SizedBox( + width: width, + height: height, + child: Padding( padding: DropZoneWidgetStyle.margin, child: DottedBorder( borderType: BorderType.RRect, @@ -48,20 +41,18 @@ class _DropZoneWidgetState extends State { dashPattern: DropZoneWidgetStyle.dashSize, child: Container( clipBehavior: Clip.antiAlias, - decoration: const ShapeDecoration( + decoration: ShapeDecoration( color: DropZoneWidgetStyle.backgroundColor, - shape: RoundedRectangleBorder( + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), ), ), - width: widget.width, - height: widget.height, padding: DropZoneWidgetStyle.padding, alignment: AlignmentDirectional.center, child: Column( mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset(_imagePaths.icDropZoneIcon), + SvgPicture.asset(imagePaths.icDropZoneIcon), const SizedBox(height: DropZoneWidgetStyle.space), Text( AppLocalizations.of(context).dropFileHereToAttachThem, @@ -71,22 +62,10 @@ class _DropZoneWidgetState extends State { ), ), ), - ); - } else { - return SizedBox(width: widget.width, height: widget.height); - } - }, - onAccept: widget.addAttachmentFromDropZone, - onLeave: (attachment) { - if (_isDragging) { - setState(() => _isDragging = false); - } - }, - onMove: (details) { - if (!_isDragging) { - setState(() => _isDragging = true); - } + ), + ); }, + onAccept: onAttachmentDropZoneListener ); } } diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index 90655a40e..648ac8630 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -42,6 +42,7 @@ class BottomBarComposerWidget extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: BottomBarComposerWidgetStyle.padding, + height: BottomBarComposerWidgetStyle.height, color: BottomBarComposerWidgetStyle.backgroundColor, child: Row( children: [ diff --git a/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart b/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart new file mode 100644 index 000000000..058e4a7a9 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart @@ -0,0 +1,69 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:desktop_drop/desktop_drop.dart'; + +typedef OnLocalFileDropZoneListener = Function(DropDoneDetails details); + +class LocalFileDropZoneWidget extends StatelessWidget { + + final ImagePaths imagePaths; + final double? width; + final double? height; + final OnLocalFileDropZoneListener? onLocalFileDropZoneListener; + + const LocalFileDropZoneWidget({ + super.key, + required this.imagePaths, + this.width, + this.height, + this.onLocalFileDropZoneListener + }); + + @override + Widget build(BuildContext context) { + return DropTarget( + onDragDone: onLocalFileDropZoneListener, + child: SizedBox( + width: width, + height: height, + child: Padding( + padding: DropZoneWidgetStyle.margin, + child: DottedBorder( + borderType: BorderType.RRect, + radius: const Radius.circular(DropZoneWidgetStyle.radius), + color: DropZoneWidgetStyle.borderColor, + strokeWidth: DropZoneWidgetStyle.borderWidth, + dashPattern: DropZoneWidgetStyle.dashSize, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: DropZoneWidgetStyle.backgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), + ), + ), + padding: DropZoneWidgetStyle.padding, + alignment: AlignmentDirectional.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(imagePaths.icDropZoneIcon), + const SizedBox(height: DropZoneWidgetStyle.space), + Text( + AppLocalizations.of(context).dropFileHereToAttachThem, + style: DropZoneWidgetStyle.labelTextStyle, + ) + ] + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index c3078af3c..a2ac28b52 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -11,8 +11,6 @@ typedef OnChangeContentEditorAction = Function(String? text); typedef OnInitialContentEditorAction = Function(String text); typedef OnMouseDownEditorAction = Function(BuildContext context); typedef OnEditorSettingsChange = Function(EditorSettings settings); -typedef OnImageUploadSuccessAction = Function(FileUpload fileUpload); -typedef OnImageUploadFailureAction = Function(FileUpload? fileUpload, String? base64Str, UploadError error); typedef OnEditorTextSizeChanged = Function(int? size); class WebEditorWidget extends StatefulWidget { @@ -26,11 +24,10 @@ class WebEditorWidget extends StatefulWidget { final VoidCallback? onUnFocus; final OnMouseDownEditorAction? onMouseDown; final OnEditorSettingsChange? onEditorSettings; - final OnImageUploadSuccessAction? onImageUploadSuccessAction; - final OnImageUploadFailureAction? onImageUploadFailureAction; final OnEditorTextSizeChanged? onEditorTextSizeChanged; final double? width; final double? height; + final VoidCallback? onDragEnter; const WebEditorWidget({ super.key, @@ -43,11 +40,10 @@ class WebEditorWidget extends StatefulWidget { this.onUnFocus, this.onMouseDown, this.onEditorSettings, - this.onImageUploadSuccessAction, - this.onImageUploadFailureAction, this.onEditorTextSizeChanged, this.width, this.height, + this.onDragEnter, }); @override @@ -133,6 +129,7 @@ class _WebEditorState extends State { initialText: widget.content, customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction), spellCheck: true, + disableDragAndDrop: true, webInitialScripts: UnmodifiableListView([ WebScript( name: HtmlUtils.lineHeight100Percent.name, @@ -145,7 +142,7 @@ class _WebEditorState extends State { WebScript( name: HtmlUtils.unregisterDropListener.name, script: HtmlUtils.unregisterDropListener.script, - ), + ) ]) ), htmlToolbarOptions: const HtmlToolbarOptions( @@ -154,8 +151,8 @@ class _WebEditorState extends State { ), otherOptions: OtherOptions( height: height, - dropZoneWidth: dropZoneWidth, - dropZoneHeight: dropZoneHeight, + // dropZoneWidth: dropZoneWidth, + // dropZoneHeight: dropZoneHeight, ), callbacks: Callbacks( onBeforeCommand: widget.onChangeContent, @@ -173,12 +170,12 @@ class _WebEditorState extends State { onMouseDown: () => widget.onMouseDown?.call(context), onChangeSelection: widget.onEditorSettings, onChangeCodeview: widget.onChangeContent, - onImageUpload: widget.onImageUploadSuccessAction, - onImageUploadError: widget.onImageUploadFailureAction, onTextFontSizeChanged: widget.onEditorTextSizeChanged, onPaste: () => _editorController.evaluateJavascriptWeb( HtmlUtils.lineHeight100Percent.name ), + onDragEnter: widget.onDragEnter, + onDragLeave: () {}, ), ); } diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 384812313..94c9d11df 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -339,9 +339,9 @@ class EmailView extends GetWidget { responsiveUtils: controller.responsiveUtils, attachments: controller.attachments, imagePaths: controller.imagePaths, - onDragStarted: controller.mailboxDashBoardController.enableDraggableApp, + onDragStarted: controller.mailboxDashBoardController.enableAttachmentDraggableApp, onDragEnd: (details) { - controller.mailboxDashBoardController.disableDraggableApp(); + controller.mailboxDashBoardController.disableAttachmentDraggableApp(); }, downloadAttachmentAction: (attachment) => controller.handleDownloadAttachmentAction(context, attachment), viewAttachmentAction: (attachment) => controller.handleViewAttachmentAction(context, attachment), @@ -376,7 +376,7 @@ class EmailView extends GetWidget { Obx(() => CalendarEventDetailWidget( calendarEvent: calendarEvent, emailContent: controller.currentEmailLoaded?.htmlContent ?? '', - isDraggableAppActive: controller.mailboxDashBoardController.isDraggableAppActive, + isDraggableAppActive: controller.mailboxDashBoardController.isAttachmentDraggableAppActive, onOpenComposerAction: controller.openNewComposerAction, onOpenNewTabAction: controller.openNewTabAction, onMailtoDelegateAction: controller.openMailToLink, @@ -407,7 +407,14 @@ class EmailView extends GetWidget { mailtoDelegate: controller.openMailToLink, direction: AppUtils.getCurrentDirection(context), ), - if (controller.mailboxDashBoardController.isDraggableAppActive) + if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) + PointerInterceptor( + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + ) + ), + if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) PointerInterceptor( child: SizedBox( width: constraints.maxWidth, diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index c1d1db8ae..27087a60d 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -1,38 +1,7 @@ -import 'package:core/data/constants/constant.dart'; -import 'package:core/domain/extensions/media_type_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:http_parser/http_parser.dart'; import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; extension AttachmentExtension on Attachment { - - String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) { - final mediaType = type ?? fileMediaType; - - if (isDisplayedPDFIcon) { - return imagePaths.icFilePdf; - } - - if (mediaType == null) { - return imagePaths.icFileEPup; - } - if (mediaType.isDocFile()) { - return imagePaths.icFileDocx; - } else if (mediaType.isExcelFile()) { - return imagePaths.icFileXlsx; - } else if (mediaType.isPowerPointFile()) { - return imagePaths.icFilePptx; - } else if (mediaType.isPdfFile()) { - return imagePaths.icFilePdf; - } else if (mediaType.isZipFile()) { - return imagePaths.icFileZip; - } else if (mediaType.isImageFile()) { - return imagePaths.icFilePng; - } - return imagePaths.icFileEPup; - } - - bool get isDisplayedPDFIcon => type?.mimeType == Constant.pdfMimeType - || (type?.mimeType == Constant.octetStreamMimeType - && name?.endsWith(Constant.pdfExtension) == true); + String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup; } \ No newline at end of file diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 1e75a46e3..34d21b5db 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dio/dio.dart'; -import 'package:get/get_connect/http/src/request/request.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/account/password.dart'; @@ -122,8 +121,6 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey]; requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token); - requestOptions.headers[HttpHeaders.contentTypeHeader] = uploadExtra[FileUploader.typeExtraKey]; - requestOptions.headers[HttpHeaders.contentLengthHeader] = uploadExtra[FileUploader.sizeExtraKey]; final newOptions = Options( method: requestOptions.method, @@ -155,11 +152,16 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { } Stream>? _getDataUploadRequest(dynamic mapUploadExtra) { - final currentPlatform = mapUploadExtra[FileUploader.platformExtraKey]; - if (currentPlatform == 'web') { - return BodyBytesStream.fromBytes(mapUploadExtra[FileUploader.bytesExtraKey]); - } else { - return File(mapUploadExtra[FileUploader.filePathExtraKey]).openRead(); + try { + String? filePath = mapUploadExtra[FileUploader.filePathExtraKey]; + if (filePath?.isNotEmpty == true) { + return File(filePath!).openRead(); + } else { + return mapUploadExtra[FileUploader.streamDataExtraKey]; + } + } catch(e) { + log('AuthorizationInterceptors::_getDataUploadRequest: Exception = $e'); + return null; } } diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 7b259b794..4314ec2e2 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -216,8 +216,9 @@ class MailboxDashBoardController extends ReloadableController { final searchMailboxActivated = RxBool(false); final listSendingEmails = RxList(); final refreshingMailboxState = Rx>(Right(UIState.idle)); - final draggableAppState = Rxn(); + final attachmentDraggableAppState = Rxn(); final isRecoveringDeletedMessage = RxBool(false); + final localFileDraggableAppState = Rxn(); Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; @@ -2148,14 +2149,16 @@ class MailboxDashBoardController extends ReloadableController { openMailboxAction(inboxPresentation); } - bool get isDraggableAppActive => draggableAppState.value == DraggableAppState.active; + bool get isAttachmentDraggableAppActive => attachmentDraggableAppState.value == DraggableAppState.active; - void enableDraggableApp() { - draggableAppState.value = DraggableAppState.active; + bool get isLocalFileDraggableAppActive => localFileDraggableAppState.value == DraggableAppState.active; + + void enableAttachmentDraggableApp() { + attachmentDraggableAppState.value = DraggableAppState.active; } - void disableDraggableApp() { - draggableAppState.value = DraggableAppState.inActive; + void disableAttachmentDraggableApp() { + attachmentDraggableAppState.value = DraggableAppState.inActive; } void saveEmailToDraft({required SaveToDraftArguments arguments}) { diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart index 46fc1f857..c6715357f 100644 --- a/lib/features/upload/data/network/file_uploader.dart +++ b/lib/features/upload/data/network/file_uploader.dart @@ -27,10 +27,7 @@ import 'package:worker_manager/worker_manager.dart' as worker; class FileUploader { static const String uploadAttachmentExtraKey = 'upload-attachment'; - static const String platformExtraKey = 'platform'; - static const String bytesExtraKey = 'bytes'; - static const String typeExtraKey = 'type'; - static const String sizeExtraKey = 'size'; + static const String streamDataExtraKey = 'streamData'; static const String filePathExtraKey = 'path'; final DioClient _dioClient; @@ -94,10 +91,7 @@ class FileUploader { final mapExtra = { uploadAttachmentExtraKey: { - platformExtraKey: 'mobile', filePathExtraKey: argsUpload.mobileFileUpload.filePath, - typeExtraKey: argsUpload.mobileFileUpload.mimeType, - sizeExtraKey: argsUpload.mobileFileUpload.fileSize, } }; @@ -109,7 +103,7 @@ class FileUploader { ), data: File(argsUpload.mobileFileUpload.filePath).openRead(), onSendProgress: (count, total) { - log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $count'); + log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); sendPort.send( UploadingAttachmentUploadState( argsUpload.uploadId, @@ -139,10 +133,7 @@ class FileUploader { final mapExtra = { uploadAttachmentExtraKey: { - platformExtraKey: 'web', - bytesExtraKey: fileInfo.bytes, - typeExtraKey: fileInfo.mimeType, - sizeExtraKey: fileInfo.fileSize, + streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!), } }; @@ -155,7 +146,7 @@ class FileUploader { data: BodyBytesStream.fromBytes(fileInfo.bytes!), cancelToken: cancelToken, onSendProgress: (count, total) { - log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: [${uploadId.id}] = $count'); + log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); onSendController.add( Right(UploadingAttachmentUploadState( uploadId, diff --git a/lib/features/upload/domain/exceptions/pick_file_exception.dart b/lib/features/upload/domain/exceptions/pick_file_exception.dart new file mode 100644 index 000000000..0620cf3a1 --- /dev/null +++ b/lib/features/upload/domain/exceptions/pick_file_exception.dart @@ -0,0 +1 @@ +class PickFileCanceledException implements Exception {} \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/file_info_extension.dart b/lib/features/upload/domain/extensions/file_info_extension.dart index 58866921c..ce079bacd 100644 --- a/lib/features/upload/domain/extensions/file_info_extension.dart +++ b/lib/features/upload/domain/extensions/file_info_extension.dart @@ -3,5 +3,15 @@ import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart'; extension FileInfoExtension on FileInfo { - MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath, fileSize, mimeType); + MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath!, fileSize, mimeType); + + FileInfo withInline() => FileInfo( + fileName: fileName, + fileSize: fileSize, + filePath: filePath, + bytes: bytes, + readStream: readStream, + type: type, + isInline: true, + isShared: isShared); } \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/list_file_info_extension.dart b/lib/features/upload/domain/extensions/list_file_info_extension.dart index 438a30a6a..3934f631d 100644 --- a/lib/features/upload/domain/extensions/list_file_info_extension.dart +++ b/lib/features/upload/domain/extensions/list_file_info_extension.dart @@ -3,5 +3,5 @@ import 'package:model/upload/file_info.dart'; extension ListFileInfoExtension on List { List get listSize => map((file) => file.fileSize).toList(); - num get totalSize => listSize.reduce((sum, size) => sum + size); + num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size); } \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/media_type_extension.dart b/lib/features/upload/domain/extensions/media_type_extension.dart new file mode 100644 index 000000000..e3bfcc8d8 --- /dev/null +++ b/lib/features/upload/domain/extensions/media_type_extension.dart @@ -0,0 +1,28 @@ +import 'package:core/domain/extensions/media_type_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:http_parser/http_parser.dart'; + +extension MediaTypeExtension on MediaType { + String getIcon(ImagePaths imagePaths, {String? fileName}) { + if (validatePDFIcon(fileName: fileName) == true) { + return imagePaths.icFilePdf; + } else if (isDocFile()) { + return imagePaths.icFileDocx; + } else if (isExcelFile()) { + return imagePaths.icFileXlsx; + } else if (isPowerPointFile()) { + return imagePaths.icFilePptx; + } else if (isPdfFile()) { + return imagePaths.icFilePdf; + } else if (isZipFile()) { + return imagePaths.icFileZip; + } else if (isImageFile()) { + return imagePaths.icFilePng; + } else { + return imagePaths.icFileEPup; + } + } + + bool validatePDFIcon({required String? fileName}) => mimeType == 'application/pdf' || + (mimeType == 'application/octet-stream' && fileName?.endsWith('.pdf') == true); +} diff --git a/lib/features/upload/domain/extensions/platform_file_extension.dart b/lib/features/upload/domain/extensions/platform_file_extension.dart new file mode 100644 index 000000000..f24da3baa --- /dev/null +++ b/lib/features/upload/domain/extensions/platform_file_extension.dart @@ -0,0 +1,13 @@ +import 'package:core/utils/platform_info.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:model/upload/file_info.dart'; + +extension PlatformFileExtension on PlatformFile { + FileInfo toFileInfo() => FileInfo( + fileName: name, + fileSize: size, + filePath: PlatformInfo.isWeb ? '' : path ?? '', + bytes: bytes, + readStream: readStream + ); +} \ No newline at end of file diff --git a/lib/features/upload/domain/state/attachment_upload_state.dart b/lib/features/upload/domain/state/attachment_upload_state.dart index b5f0ee2f5..5b5b20cdf 100644 --- a/lib/features/upload/domain/state/attachment_upload_state.dart +++ b/lib/features/upload/domain/state/attachment_upload_state.dart @@ -31,15 +31,11 @@ class SuccessAttachmentUploadState extends Success { final UploadTaskId uploadId; final Attachment attachment; final FileInfo fileInfo; - final bool fromFileShared; SuccessAttachmentUploadState( this.uploadId, this.attachment, - this.fileInfo, - { - this.fromFileShared = false - } + this.fileInfo ); @override @@ -47,7 +43,6 @@ class SuccessAttachmentUploadState extends Success { uploadId, attachment, fileInfo, - fromFileShared, ]; } diff --git a/lib/features/upload/domain/state/local_file_picker_state.dart b/lib/features/upload/domain/state/local_file_picker_state.dart index a2d948fe5..37745e989 100644 --- a/lib/features/upload/domain/state/local_file_picker_state.dart +++ b/lib/features/upload/domain/state/local_file_picker_state.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/upload/file_info.dart'; +class LocalFilePickerLoading extends LoadingState {} + class LocalFilePickerSuccess extends UIState { final List pickedFiles; @@ -14,6 +16,4 @@ class LocalFilePickerSuccess extends UIState { class LocalFilePickerFailure extends FeatureFailure { LocalFilePickerFailure(dynamic exception) : super(exception: exception); -} - -class LocalFilePickerCancel extends FeatureFailure {} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/features/upload/domain/state/local_image_picker_state.dart b/lib/features/upload/domain/state/local_image_picker_state.dart new file mode 100644 index 000000000..d48ac0245 --- /dev/null +++ b/lib/features/upload/domain/state/local_image_picker_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/upload/file_info.dart'; + +class LocalImagePickerLoading extends LoadingState {} + +class LocalImagePickerSuccess extends UIState { + final FileInfo fileInfo; + + LocalImagePickerSuccess(this.fileInfo); + + @override + List get props => [fileInfo]; +} + +class LocalImagePickerFailure extends FeatureFailure { + + LocalImagePickerFailure(dynamic exception) : super(exception: exception); +} diff --git a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart index c61789267..dd7cf46fa 100644 --- a/lib/features/upload/domain/usecases/local_file_picker_interactor.dart +++ b/lib/features/upload/domain/usecases/local_file_picker_interactor.dart @@ -4,7 +4,8 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; class LocalFilePickerInteractor { @@ -13,23 +14,22 @@ class LocalFilePickerInteractor { Stream> execute({FileType fileType = FileType.any}) async* { try { + yield Right(LocalFilePickerLoading()); + final filesResult = await FilePicker.platform.pickFiles( type: fileType, allowMultiple: true, - withData: PlatformInfo.isWeb + withData: PlatformInfo.isWeb, + withReadStream: PlatformInfo.isMobile ); - if (filesResult != null && filesResult.files.isNotEmpty) { - final fileInfoResults = filesResult.files - .map((platformFile) => FileInfo( - platformFile.name, - PlatformInfo.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - bytes: PlatformInfo.isWeb ? platformFile.bytes : null - )) - .toList(); - yield Right(LocalFilePickerSuccess(fileInfoResults)); + + if (filesResult?.files.isNotEmpty == true) { + final listFileInfo = filesResult!.files + .map((platformFile) => platformFile.toFileInfo()) + .toList(); + yield Right(LocalFilePickerSuccess(listFileInfo)); } else { - yield Left(LocalFilePickerCancel()); + yield Left(LocalFilePickerFailure(PickFileCanceledException())); } } catch (exception) { yield Left(LocalFilePickerFailure(exception)); diff --git a/lib/features/upload/domain/usecases/local_image_picker_interactor.dart b/lib/features/upload/domain/usecases/local_image_picker_interactor.dart new file mode 100644 index 000000000..ca91a31be --- /dev/null +++ b/lib/features/upload/domain/usecases/local_image_picker_interactor.dart @@ -0,0 +1,35 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart'; + +class LocalImagePickerInteractor { + + LocalImagePickerInteractor(); + + Stream> execute() async* { + try { + yield Right(LocalImagePickerLoading()); + + final filePickerResult = await FilePicker.platform.pickFiles( + type: FileType.image, + allowMultiple: false, + withReadStream: PlatformInfo.isMobile, + withData: PlatformInfo.isWeb + ); + + if (filePickerResult?.files.isNotEmpty == true) { + final fileInfo = filePickerResult!.files.first.toFileInfo(); + yield Right(LocalImagePickerSuccess(fileInfo)); + } else { + yield Left(LocalImagePickerFailure(PickFileCanceledException())); + } + } catch (exception) { + yield Left(LocalImagePickerFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index 38342ba40..f59ef2188 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -14,13 +14,13 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/attachment.dart'; import 'package:model/extensions/attachment_extension.dart'; -import 'package:model/extensions/list_attachment_extension.dart'; import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/presentation/extensions/upload_attachment_extension.dart'; @@ -80,7 +80,7 @@ class UploadController extends BaseController { failure.uploadId, (currentState) => currentState?.copyWith(uploadStatus: UploadFileStatus.uploadFailed)); deleteFileUploaded(failure.uploadId); - _handleUploadAttachmentsFailure(failure); + _showToastMessageWhenUploadAttachmentsFailure(failure); } }, (success) { @@ -115,7 +115,7 @@ class UploadController extends BaseController { ); _refreshListUploadAttachmentState(); - _handleUploadAttachmentsSuccess(success); + _showToastMessageWhenUploadAttachmentsSuccess(success); } } ); @@ -162,7 +162,7 @@ class UploadController extends BaseController { ); final uploadFileState = _uploadingStateInlineFiles.getUploadFileStateById(success.uploadId); - log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.fromFileShared}'); + log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.file?.isShared}'); _uploadingStateInlineFiles.updateElementByUploadTaskId( success.uploadId, @@ -179,7 +179,6 @@ class UploadController extends BaseController { success.uploadId, inlineAttachment, success.fileInfo, - fromFileShared: uploadFileState?.fromFileShared ?? false ); _handleUploadInlineAttachmentsSuccess(newUploadSuccess); } @@ -203,27 +202,24 @@ class UploadController extends BaseController { _refreshListUploadAttachmentState(); } - Future justUploadAttachmentsAction(List uploadFiles, Uri uploadUri) { + Future justUploadAttachmentsAction({ + required List uploadFiles, + required Uri uploadUri, + }) { return Future.forEach(uploadFiles, (uploadFile) async { - await uploadFileAction(uploadFile, uploadUri); + await uploadFileAction(uploadFile: uploadFile, uploadUri: uploadUri); }); } - Future uploadFileAction( - FileInfo uploadFile, - Uri uploadUri, - { - bool isInline = false, - bool fromFileShared = false, - } - ) { - log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | isInline: $isInline | fromFileShared: $fromFileShared'); + Future uploadFileAction({ + required FileInfo uploadFile, + required Uri uploadUri, + }) { + log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | mimeType: ${uploadFile.mimeType} | isInline: ${uploadFile.isInline} | fromFileShared: ${uploadFile.isShared}'); consumeState(_uploadAttachmentInteractor.execute( uploadFile, uploadUri, cancelToken: CancelToken(), - isInline: isInline, - fromFileShared: fromFileShared )); return Future.value(); } @@ -244,6 +240,16 @@ class UploadController extends BaseController { .toList(); } + List get attachmentsPicked { + if (listUploadAttachments.isEmpty) { + return List.empty(); + } + return listUploadAttachments + .map((fileState) => fileState.file) + .whereNotNull() + .toList(); + } + Set? generateAttachments() { if (attachmentsUploaded.isEmpty) { return null; @@ -254,7 +260,7 @@ class UploadController extends BaseController { .toSet(); } - void _handleUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { + void _showToastMessageWhenUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { if (currentContext != null && currentOverlayContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -264,7 +270,7 @@ class UploadController extends BaseController { } } - void _handleUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { + void _showToastMessageWhenUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -275,7 +281,7 @@ class UploadController extends BaseController { } bool isExceededMaxSizeAttachmentsPerEmail({num totalSizePreparedFiles = 0}) { - final currentTotalSize = attachmentsUploaded.totalSize + inlineAttachmentsUploaded.totalSize + totalSizePreparedFiles; + final currentTotalSize = attachmentsPicked.totalSize + inlineAttachmentsPicked.totalSize + totalSizePreparedFiles; final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value; log('UploadController::isExceededMaxSizeAttachmentsPerEmail(): currentTotalSize = $currentTotalSize | maxSizeAttachmentsPerEmail = $maxSizeAttachmentsPerEmail'); if (maxSizeAttachmentsPerEmail != null) { @@ -285,16 +291,17 @@ class UploadController extends BaseController { } } - bool isExceededMaxSizeFilesAttachedInComposer({num totalSizePreparedFiles = 0}) { - final currentTotalSizeAttachments = attachmentsUploaded.totalSize + totalSizePreparedFiles; - const maximumBytesSizeFileAttachedInComposer = AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; + bool isExceededWarningAttachmentFileSizeInComposer({num totalSizePreparedFiles = 0}) { + final currentTotalSizeAttachments = attachmentsPicked.totalSize + totalSizePreparedFiles; + const maximumBytesSizeFileAttachedInComposer = AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024; log('UploadController::isExceededMaxSizeFilesAttachedInComposer(): currentTotalSizeAttachments = $currentTotalSizeAttachments | maximumBytesSizeFileAttachedInComposer = $maximumBytesSizeFileAttachedInComposer'); return currentTotalSizeAttachments > maximumBytesSizeFileAttachedInComposer; } void validateTotalSizeAttachmentsBeforeUpload({ required num totalSizePreparedFiles, - VoidCallback? callbackAction + num? totalSizePreparedFilesWithDispositionAttachment, + VoidCallback? onValidationSuccess }) { log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizePreparedFiles = $totalSizePreparedFiles'); if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { @@ -307,7 +314,7 @@ class UploadController extends BaseController { return; } - if (isExceededMaxSizeFilesAttachedInComposer(totalSizePreparedFiles: totalSizePreparedFiles)) { + if (isExceededWarningAttachmentFileSizeInComposer(totalSizePreparedFiles: totalSizePreparedFilesWithDispositionAttachment ?? totalSizePreparedFiles)) { if (currentContext == null) { log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); return; @@ -315,12 +322,34 @@ class UploadController extends BaseController { _showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer( context: currentContext!, - confirmAction: callbackAction + confirmAction: () async { + await Future.delayed( + const Duration(milliseconds: 100), + onValidationSuccess + ); + } ); return; } - callbackAction?.call(); + onValidationSuccess?.call(); + } + + void validateTotalSizeInlineAttachmentsBeforeUpload({ + required num totalSizePreparedFiles, + VoidCallback? onValidationSuccess + }) { + if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { + if (currentContext == null) { + log('UploadController::validateTotalSizeInlineAttachmentsBeforeUpload: CONTEXT IS NULL'); + return; + } + + _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail(context: currentContext!); + return; + } + + onValidationSuccess?.call(); } void _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail({required BuildContext context}) { @@ -340,7 +369,7 @@ class UploadController extends BaseController { showConfirmDialogAction( context, title: '', - AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, + AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer, AppLocalizations.of(context).continueAction, cancelTitle: AppLocalizations.of(context).cancel, alignCenter: true, @@ -394,6 +423,17 @@ class UploadController extends BaseController { .toList(); } + List get inlineAttachmentsPicked { + if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) { + return List.empty(); + } + return _uploadingStateInlineFiles.uploadingStateFiles + .whereNotNull() + .map((fileState) => fileState.file) + .whereNotNull() + .toList(); + } + Map get mapInlineAttachments { final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles .whereNotNull() @@ -415,42 +455,47 @@ class UploadController extends BaseController { return mapInlineAttachments; } + void _handleUploadAttachmentFailure(UploadAttachmentFailure failure) { + if (currentContext != null && currentOverlayContext != null) { + appToast.showToastErrorMessage( + currentOverlayContext!, + failure.fileInfo.isInline == true + ? AppLocalizations.of(currentContext!).thisImageCannotBeAdded + : AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: failure.fileInfo.isInline == true + ? imagePaths.icInsertImage + : imagePaths.icAttachment + ); + } + } + + void _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async { + if (success.uploadAttachment.fileInfo.isInline == true) { + _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState()); + await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); + } else { + _uploadingStateFiles.add(success.uploadAttachment.toUploadFileState()); + await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState); + _refreshListUploadAttachmentState(); + } + } + @override - void handleFailureViewState(Failure failure) async { - super.handleFailureViewState(failure); + void handleFailureViewState(Failure failure) { if (failure is UploadAttachmentFailure) { - if (failure.isInline) { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).thisImageCannotBeAdded, - leadingSVGIconColor: Colors.white, - leadingSVGIcon: imagePaths.icInsertImage); - } - } else { - if (currentContext != null && currentOverlayContext != null) { - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, - leadingSVGIconColor: Colors.white, - leadingSVGIcon: imagePaths.icAttachment); - } - } + _handleUploadAttachmentFailure(failure); + } else { + super.handleFailureViewState(failure); } } @override void handleSuccessViewState(Success success) async { - super.handleSuccessViewState(success); if (success is UploadAttachmentSuccess) { - if (success.isInline) { - _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState(fromFileShared: success.fromFileShared)); - await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); - } else { - _uploadingStateFiles.add(success.uploadAttachment.toUploadFileState()); - await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState); - _refreshListUploadAttachmentState(); - } + _handleUploadAttachmentSuccess(success); + } else { + super.handleSuccessViewState(success); } } } \ No newline at end of file diff --git a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart index 4b45c9427..64feed8fe 100644 --- a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart +++ b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart @@ -4,12 +4,11 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta extension UploadAttachmentExtension on UploadAttachment { - UploadFileState toUploadFileState({bool fromFileShared = false}) { + UploadFileState toUploadFileState() { return UploadFileState( uploadTaskId, file: fileInfo, cancelToken: cancelToken, - fromFileShared: fromFileShared, ); } } \ No newline at end of file diff --git a/lib/features/upload/presentation/model/upload_file_state.dart b/lib/features/upload/presentation/model/upload_file_state.dart index 20876fbe4..f0e58b78c 100644 --- a/lib/features/upload/presentation/model/upload_file_state.dart +++ b/lib/features/upload/presentation/model/upload_file_state.dart @@ -1,11 +1,12 @@ import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; import 'package:http_parser/http_parser.dart'; import 'package:model/email/attachment.dart'; import 'package:model/upload/file_info.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; @@ -17,7 +18,6 @@ class UploadFileState with EquatableMixin { final int uploadingProgress; final Attachment? attachment; final CancelToken? cancelToken; - final bool fromFileShared; UploadFileState( this.uploadTaskId, @@ -27,7 +27,6 @@ class UploadFileState with EquatableMixin { this.uploadingProgress = 0, this.attachment, this.cancelToken, - this.fromFileShared = false, } ); @@ -38,7 +37,6 @@ class UploadFileState with EquatableMixin { int? uploadingProgress, Attachment? attachment, CancelToken? cancelToken, - bool? fromFileShared, }) { return UploadFileState( uploadTaskId ?? this.uploadTaskId, @@ -47,7 +45,6 @@ class UploadFileState with EquatableMixin { uploadingProgress: uploadingProgress ?? this.uploadingProgress, attachment: attachment ?? this.attachment, cancelToken: cancelToken ?? this.cancelToken, - fromFileShared: fromFileShared ?? this.fromFileShared ); } @@ -72,11 +69,16 @@ class UploadFileState with EquatableMixin { double get percentUploading => uploadingProgress / 100; String getIcon(ImagePaths imagePaths) { - var mediaType = attachment?.type; - if (mediaType == null && file != null) { - mediaType = MediaType.parse(file!.mimeType); + try { + MediaType? mediaType = attachment?.type; + if (mediaType == null && file != null) { + mediaType = MediaType.parse(file!.mimeType); + } + return mediaType?.getIcon(imagePaths, fileName: fileName) ?? imagePaths.icFileEPup; + } catch (e) { + logError('UploadFileState::getIcon: Exception: $e'); + return imagePaths.icFileEPup; } - return attachment?.getIcon(imagePaths, fileMediaType: mediaType) ?? imagePaths.icFileEPup; } @override @@ -87,6 +89,5 @@ class UploadFileState with EquatableMixin { uploadingProgress, attachment, cancelToken, - fromFileShared, ]; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fe33404be..9b9fd9e7a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3239,8 +3239,8 @@ "placeholders_order": [], "placeholders": {} }, - "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", - "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 6d98e60e1..a5c8dad9b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -3936,8 +3936,8 @@ "placeholders_order": [], "placeholders": {} }, - "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.", - "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 0779e9173..8675ed7ae 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-02-29T16:08:18.479839", + "@@last_modified": "2024-03-15T21:34:52.177175", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3750,8 +3750,8 @@ "placeholders_order": [], "placeholders": {} }, - "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", - "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} @@ -3761,5 +3761,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "thisFileCannotBePicked": "This file cannot be picked.", + "@thisFileCannotBePicked": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "loadingPleaseWait": "Loading... Please wait!", + "@loadingPleaseWait": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index 07f4cf948..8a0cf5605 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -3944,6 +3944,8 @@ }, "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", "@messageWarningDialogWhenExceedMaximumFileSizeComposer": { + "warningMessageWhenExceedGenerallySizeInComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", + "@warningMessageWhenExceedGenerallySizeInComposer": { "type": "text", "placeholders_order": [], "placeholders": {} diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 5c446ce03..ff1b9d64f 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3917,10 +3917,10 @@ class AppLocalizations { ); } - String get messageWarningDialogWhenExceedMaximumFileSizeComposer { + String get warningMessageWhenExceedGenerallySizeInComposer { return Intl.message( 'Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.', - name: 'messageWarningDialogWhenExceedMaximumFileSizeComposer', + name: 'warningMessageWhenExceedGenerallySizeInComposer', ); } @@ -3930,4 +3930,18 @@ class AppLocalizations { name: 'continueAction', ); } + + String get thisFileCannotBePicked { + return Intl.message( + 'This file cannot be picked.', + name: 'thisFileCannotBePicked', + ); + } + + String get loadingPleaseWait { + return Intl.message( + 'Loading... Please wait!', + name: 'loadingPleaseWait', + ); + } } \ No newline at end of file diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index f4f65fa6b..bff358776 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -8,7 +8,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class AppConfig { static const int limitCharToStartSearch = 3; - static const int maximumMegabytesSizeFileAttachedInComposer = 10; + static const int warningAttachmentFileSizeInMegabytes = 10; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; static const String appFCMConfigurationPath = "configurations/env.fcm"; diff --git a/model/lib/upload/file_info.dart b/model/lib/upload/file_info.dart index 924f5e86e..fa79b85fc 100644 --- a/model/lib/upload/file_info.dart +++ b/model/lib/upload/file_info.dart @@ -1,32 +1,65 @@ - import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:mime/mime.dart'; class FileInfo with EquatableMixin { final String fileName; - final String filePath; final int fileSize; + final String? filePath; final Uint8List? bytes; + final Stream>? readStream; + final String? type; + final bool? isInline; + final bool? isShared; - FileInfo(this.fileName, this.filePath, this.fileSize, {this.bytes}); - - factory FileInfo.empty() { - return FileInfo('', '', 0); - } + FileInfo({ + required this.fileName, + required this.fileSize, + this.filePath, + this.bytes, + this.readStream, + this.type, + this.isInline, + this.isShared, + }); factory FileInfo.fromBytes({ required Uint8List bytes, String? name, - int? size + int? size, + String? type, }) { - return FileInfo(name ?? '', '', size ?? 0, bytes: bytes); + return FileInfo( + fileName: name ?? '', + fileSize: size ?? 0, + bytes: bytes, + type: type + ); } String get fileExtension => fileName.split('.').last; - String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/octet-stream'; + String get mimeType { + if (type?.isNotEmpty == true) { + return type!; + } else if (filePath?.isNotEmpty == true){ + final matchedType = lookupMimeType(filePath!, headerBytes: bytes) ?? 'application/octet-stream'; + return matchedType; + } else { + final matchedType = lookupMimeType(fileName, headerBytes: bytes) ?? 'application/octet-stream'; + return matchedType; + } + } @override - List get props => [fileName, filePath, fileSize, bytes]; + List get props => [ + fileName, + filePath, + fileSize, + bytes, + readStream, + type, + isInline, + isShared, + ]; } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 8a2ef32cd..0f8d80bcd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -360,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d + url: "https://pub.dev" + source: hosted + version: "0.4.4" device_info_plus: dependency: "direct main" description: @@ -990,6 +998,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + future_loading_dialog: + dependency: "direct main" + description: + name: future_loading_dialog + sha256: "2718b1a308db452da32ab9bca9ad496ff92b683e217add9e92cf50520f90537e" + url: "https://pub.dev" + source: hosted + version: "0.3.0" get: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7acf15700..45f423df5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -240,6 +240,10 @@ dependencies: mime: 1.0.4 + desktop_drop: 0.4.4 + + future_loading_dialog: 0.3.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/features/model/attachment_extension_test.dart b/test/features/model/attachment_extension_test.dart index 33b050a38..2b686ec27 100644 --- a/test/features/model/attachment_extension_test.dart +++ b/test/features/model/attachment_extension_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http_parser/http_parser.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart'; void main() { final attachmentA = Attachment( @@ -34,7 +34,7 @@ void main() { 'WHEN perform call `attachmentA.isDisplayedPDFIcon()`\n' 'SHOULD return true', () { - bool result = attachmentA.isDisplayedPDFIcon; + bool result = attachmentA.type!.validatePDFIcon(fileName: null); expect(result, isTrue); }); @@ -45,7 +45,7 @@ void main() { 'WHEN perform call `attachmentB.isDisplayedPDFIcon()`\n' 'SHOULD return true', () { - bool result = attachmentB.isDisplayedPDFIcon; + bool result = attachmentB.type!.validatePDFIcon(fileName: 'attachmentB.pdf'); expect(result, isTrue); }); @@ -56,7 +56,7 @@ void main() { 'WHEN perform call `attachmentC.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentC.isDisplayedPDFIcon; + bool result = attachmentC.type!.validatePDFIcon(fileName: 'attachmentC.docx'); expect(result, isFalse); }); @@ -67,7 +67,7 @@ void main() { 'WHEN perform call `attachmentD.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentD.isDisplayedPDFIcon; + bool result = attachmentD.type!.validatePDFIcon(fileName: 'attachmentD.png'); expect(result, isFalse); }); @@ -78,7 +78,7 @@ void main() { 'WHEN perform call `attachmentE.isDisplayedPDFIcon()`\n' 'SHOULD return false', () { - bool result = attachmentE.isDisplayedPDFIcon; + bool result = attachmentE.type!.validatePDFIcon(fileName: 'attachmentE.pdf'); expect(result, isFalse); });