diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index e016aae3e..032bafe1b 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -86,6 +87,7 @@ import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_alway 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/file_info_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_upload_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'; @@ -2136,11 +2138,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement required DropDoneDetails details, required double maxWidth }) async { - if (responsiveUtils.isMobile(context)) { - maxWithEditor = maxWidth - 40; - } else { - maxWithEditor = maxWidth - 70; - } + _setUpMaxWidthInlineImage(context: context, maxWidth: maxWidth); final listFileInfo = await onDragDone(context: context, details: details); @@ -2152,13 +2150,9 @@ class ComposerController extends BaseController with DragDropFileMixin implement return; } - final listAttachments = listFileInfo - .where((fileInfo) => fileInfo.isInline != true) - .toList(); - uploadController.validateTotalSizeAttachmentsBeforeUpload( totalSizePreparedFiles: listFileInfo.totalSize, - totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize, + totalSizePreparedFilesWithDispositionAttachment: listFileInfo.listAttachmentFiles.totalSize, onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) ); } @@ -2341,4 +2335,43 @@ class ComposerController extends BaseController with DragDropFileMixin implement await _saveComposerCacheOnWebAction(); } } + + void _setUpMaxWidthInlineImage({ + required BuildContext context, + required double maxWidth + }) { + if (responsiveUtils.isMobile(context)) { + maxWithEditor = maxWidth - 40; + } else { + maxWithEditor = maxWidth - 70; + } + } + + void handleOnPasteImageSuccessAction({ + required BuildContext context, + required double maxWidth, + required List listFileUpload + }) { + log('ComposerController::handleOnPasteImageSuccessAction: listFileUpload = ${listFileUpload.length}'); + _setUpMaxWidthInlineImage(context: context, maxWidth: maxWidth); + + final listFileInfo = listFileUpload.toListFileInfo(); + + uploadController.validateTotalSizeAttachmentsBeforeUpload( + totalSizePreparedFiles: listFileInfo.totalSize, + onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo) + ); + } + + void handleOnPasteImageFailureAction({ + required BuildContext context, + List? listFileUpload, + String? base64, + required UploadError uploadError + }) { + logError('ComposerController::handleOnPasteImageFailureAction: $uploadError'); + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).thisImageCannotBePastedIntoTheEditor); + } } \ 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 943356f8c..90c32d58d 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -190,6 +190,18 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction( + context: context, + maxWidth: constraintsEditor.maxWidth, + listFileUpload: listFileUpload + ), + onPasteImageFailureAction: (listFileUpload, base64, uploadError) => + controller.handleOnPasteImageFailureAction( + context: context, + listFileUpload: listFileUpload, + base64: base64, + uploadError: uploadError + ), )), ), ), @@ -430,6 +442,18 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction( + context: context, + maxWidth: constraintsEditor.maxWidth, + listFileUpload: listFileUpload + ), + onPasteImageFailureAction: (listFileUpload, base64, uploadError) => + controller.handleOnPasteImageFailureAction( + context: context, + listFileUpload: listFileUpload, + base64: base64, + uploadError: uploadError + ), ); }), ), @@ -688,6 +712,18 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction( + context: context, + maxWidth: constraintsBody.maxWidth, + listFileUpload: listFileUpload + ), + onPasteImageFailureAction: (listFileUpload, base64, uploadError) => + controller.handleOnPasteImageFailureAction( + context: context, + listFileUpload: listFileUpload, + base64: base64, + uploadError: uploadError + ), )), ), ), diff --git a/lib/features/composer/presentation/extensions/file_upload_extension.dart b/lib/features/composer/presentation/extensions/file_upload_extension.dart index ec8707c7c..b5ecbefad 100644 --- a/lib/features/composer/presentation/extensions/file_upload_extension.dart +++ b/lib/features/composer/presentation/extensions/file_upload_extension.dart @@ -1,7 +1,4 @@ import 'dart:convert' as convert; -import 'dart:typed_data' as type_data; - -import 'package:flutter/foundation.dart'; import 'package:html_editor_enhanced/utils/file_upload_model.dart'; import 'package:model/upload/file_info.dart'; @@ -23,22 +20,17 @@ extension FileUploadExtension on FileUpload { return base64; } - Future toFileInfo() async { + FileInfo? toFileInfo() { if (base64Data != null) { - final bytes = await compute(convertBase64ToBytes, base64Data!); return FileInfo.fromBytes( - bytes: bytes, + bytes: convert.base64Decode(base64Data!), name: name, size: size, type: type, + isInline: true ); } else { return null; } } - - static Uint8List convertBase64ToBytes(String base64) { - type_data.Uint8List decodeBytes = convert.base64Decode(base64); - return decodeBytes; - } } \ No newline at end of file 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 1e99380a3..2f87bc08a 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -15,8 +15,6 @@ import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_c import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; -typedef OnDragEnterListener = Function(List? types); - class WebEditorView extends StatelessWidget with EditorViewMixin { final HtmlEditorController editorController; @@ -33,6 +31,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { final double? width; final double? height; final OnDragEnterListener? onDragEnter; + final OnPasteImageSuccessAction? onPasteImageSuccessAction; + final OnPasteImageFailureAction? onPasteImageFailureAction; const WebEditorView({ super.key, @@ -50,6 +50,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { this.width, this.height, this.onDragEnter, + this.onPasteImageSuccessAction, + this.onPasteImageFailureAction, }); @override @@ -76,6 +78,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onPasteImageSuccessAction: onPasteImageSuccessAction, + onPasteImageFailureAction: onPasteImageFailureAction, ); case EmailActionType.editDraft: case EmailActionType.editSendingEmail: @@ -101,6 +105,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onPasteImageSuccessAction: onPasteImageSuccessAction, + onPasteImageFailureAction: onPasteImageFailureAction, ), (success) { if (success is GetEmailContentLoading || success is RestoringEmailInlineImages) { @@ -126,6 +132,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onPasteImageSuccessAction: onPasteImageSuccessAction, + onPasteImageFailureAction: onPasteImageFailureAction, ); } } @@ -158,6 +166,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onPasteImageSuccessAction: onPasteImageSuccessAction, + onPasteImageFailureAction: onPasteImageFailureAction, ); }, (success) { @@ -186,6 +196,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onPasteImageSuccessAction: onPasteImageSuccessAction, + onPasteImageFailureAction: onPasteImageFailureAction, ); } } @@ -205,6 +217,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onPasteImageSuccessAction: onPasteImageSuccessAction, + onPasteImageFailureAction: onPasteImageFailureAction, ); } } 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 37c59563b..77a3c57af 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -5,7 +5,6 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/html/html_utils.dart'; import 'package:flutter/material.dart'; import 'package:html_editor_enhanced/html_editor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/view/web/web_editor_view.dart'; import 'package:universal_html/html.dart' hide VoidCallback; typedef OnChangeContentEditorAction = Function(String? text); @@ -13,6 +12,12 @@ typedef OnInitialContentEditorAction = Function(String text); typedef OnMouseDownEditorAction = Function(BuildContext context); typedef OnEditorSettingsChange = Function(EditorSettings settings); typedef OnEditorTextSizeChanged = Function(int? size); +typedef OnDragEnterListener = Function(List? types); +typedef OnPasteImageSuccessAction = Function(List listFileUpload); +typedef OnPasteImageFailureAction = Function( + List? listFileUpload, + String? base64, + UploadError uploadError); class WebEditorWidget extends StatefulWidget { @@ -29,6 +34,8 @@ class WebEditorWidget extends StatefulWidget { final double? width; final double? height; final OnDragEnterListener? onDragEnter; + final OnPasteImageSuccessAction? onPasteImageSuccessAction; + final OnPasteImageFailureAction? onPasteImageFailureAction; const WebEditorWidget({ super.key, @@ -45,6 +52,8 @@ class WebEditorWidget extends StatefulWidget { this.width, this.height, this.onDragEnter, + this.onPasteImageSuccessAction, + this.onPasteImageFailureAction, }); @override @@ -179,6 +188,8 @@ class _WebEditorState extends State { ), onDragEnter: widget.onDragEnter, onDragLeave: (_) {}, + onImageUpload: widget.onPasteImageSuccessAction, + onImageUploadError: widget.onPasteImageFailureAction, ), ); } 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 f8c193f88..c972b2694 100644 --- a/lib/features/upload/domain/extensions/list_file_info_extension.dart +++ b/lib/features/upload/domain/extensions/list_file_info_extension.dart @@ -1,4 +1,3 @@ - import 'package:model/upload/file_info.dart'; extension ListFileInfoExtension on List { @@ -7,4 +6,6 @@ extension ListFileInfoExtension on List { num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size); List get listInlineFiles => where((fileInfo) => fileInfo.isInline == true).toList(); + + List get listAttachmentFiles => where((fileInfo) => fileInfo.isInline != true).toList(); } \ No newline at end of file diff --git a/lib/features/upload/domain/extensions/list_file_upload_extension.dart b/lib/features/upload/domain/extensions/list_file_upload_extension.dart new file mode 100644 index 000000000..3e4abdc82 --- /dev/null +++ b/lib/features/upload/domain/extensions/list_file_upload_extension.dart @@ -0,0 +1,8 @@ +import 'package:collection/collection.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart'; + +extension ListFileUploadExtension on List { + List toListFileInfo() => map((fileUpload) => fileUpload.toFileInfo()).whereNotNull().toList(); +} \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 5daaf1dd3..58cd43349 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-08-20T10:36:35.487743", + "@@last_modified": "2024-08-19T19:20:23.876948", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3987,5 +3987,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "thisImageCannotBePastedIntoTheEditor": "This image cannot be pasted into the editor.", + "@thisImageCannotBePastedIntoTheEditor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 3630205ee..b6e33b7d6 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4159,13 +4159,13 @@ class AppLocalizations { } String get emptySpamFolderFailed { - return Intl.message('' + return Intl.message( 'Empty spam folder failed', name: 'emptySpamFolderFailed'); } String get markAsSpamFailed { - return Intl.message('' + return Intl.message( 'Mark as spam failed', name: 'markAsSpamFailed'); } @@ -4176,4 +4176,10 @@ class AppLocalizations { name: 'canNotUploadFileToSignature' ); } + + String get thisImageCannotBePastedIntoTheEditor { + return Intl.message( + 'This image cannot be pasted into the editor.', + name: 'thisImageCannotBePastedIntoTheEditor'); + } } \ No newline at end of file diff --git a/model/lib/upload/file_info.dart b/model/lib/upload/file_info.dart index fa79b85fc..e2bcad149 100644 --- a/model/lib/upload/file_info.dart +++ b/model/lib/upload/file_info.dart @@ -28,12 +28,14 @@ class FileInfo with EquatableMixin { String? name, int? size, String? type, + bool? isInline, }) { return FileInfo( fileName: name ?? '', fileSize: size ?? 0, bytes: bytes, - type: type + type: type, + isInline: isInline, ); }