Fix cannot copy & paste base64 image as inline image in composer web

This commit is contained in:
dab246
2024-08-19 19:53:01 +07:00
committed by Dat H. Pham
parent a5a8d42763
commit b27eaed659
10 changed files with 138 additions and 29 deletions
@@ -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<FileUpload> 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<FileUpload>? listFileUpload,
String? base64,
required UploadError uploadError
}) {
logError('ComposerController::handleOnPasteImageFailureAction: $uploadError');
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).thisImageCannotBePastedIntoTheEditor);
}
}
@@ -190,6 +190,18 @@ class ComposerView extends GetWidget<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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
),
)),
),
),
@@ -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<FileInfo?> 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;
}
}
@@ -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<dynamic>? 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,
);
}
}
@@ -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<dynamic>? types);
typedef OnPasteImageSuccessAction = Function(List<FileUpload> listFileUpload);
typedef OnPasteImageFailureAction = Function(
List<FileUpload>? 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<WebEditorWidget> {
),
onDragEnter: widget.onDragEnter,
onDragLeave: (_) {},
onImageUpload: widget.onPasteImageSuccessAction,
onImageUploadError: widget.onPasteImageFailureAction,
),
);
}
@@ -1,4 +1,3 @@
import 'package:model/upload/file_info.dart';
extension ListFileInfoExtension on List<FileInfo> {
@@ -7,4 +6,6 @@ extension ListFileInfoExtension on List<FileInfo> {
num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size);
List<FileInfo> get listInlineFiles => where((fileInfo) => fileInfo.isInline == true).toList();
List<FileInfo> get listAttachmentFiles => where((fileInfo) => fileInfo.isInline != true).toList();
}
@@ -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<FileUpload> {
List<FileInfo> toListFileInfo() => map((fileUpload) => fileUpload.toFileInfo()).whereNotNull().toList();
}
+7 -1
View File
@@ -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": {}
}
}
@@ -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');
}
}
+3 -1
View File
@@ -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,
);
}