TF-2644 Handle total size exceeded maximum size when drag & drop multiple file to composer

This commit is contained in:
dab246
2024-03-01 17:09:09 +07:00
committed by Dat H. Pham
parent d646a5b9e4
commit 145a49d728
54 changed files with 1140 additions and 800 deletions
@@ -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<FileUtils>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => RemoteServerSettingsDataSourceImpl(
Get.find<ServerSettingsAPI>(),
Get.find<ServerSettingsAPI>(),
Get.find<RemoteExceptionThrower>()));
}
@@ -182,6 +183,7 @@ class ComposerBindings extends BaseBindings {
@override
void bindingsInteractor() {
Get.lazyPut(() => LocalFilePickerInteractor());
Get.lazyPut(() => LocalImagePickerInteractor());
Get.lazyPut(() => UploadAttachmentInteractor(Get.find<ComposerRepository>()));
Get.lazyPut(() => SaveEmailAsDraftsInteractor(
Get.find<EmailRepository>(),
@@ -207,6 +209,7 @@ class ComposerBindings extends BaseBindings {
Get.lazyPut(() => ComposerController(
Get.find<DeviceInfoPlugin>(),
Get.find<LocalFilePickerInteractor>(),
Get.find<LocalImagePickerInteractor>(),
Get.find<GetEmailContentInteractor>(),
Get.find<GetAllIdentitiesInteractor>(),
Get.find<UploadController>(),
@@ -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<MailboxDashBoardController>();
final richTextMobileTabletController = Get.find<RichTextMobileTabletController>();
@@ -116,6 +121,7 @@ class ComposerController extends BaseController {
final listFromIdentities = RxList<Identity>();
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<html.Event>? _subscriptionOnBeforeUnload;
StreamSubscription<html.Event>? _subscriptionOnDragEnter;
StreamSubscription<html.Event>? _subscriptionOnDragOver;
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
StreamSubscription<html.Event>? _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<FileInfo> pickedFiles) {
void _handlePickImageSuccess(LocalImagePickerSuccess success) {
uploadController.validateTotalSizeInlineAttachmentsBeforeUpload(
totalSizePreparedFiles: success.fileInfo.fileSize,
onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()])
);
}
void _uploadAttachmentsAction({required List<FileInfo> 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<InlineImage> covertListSharedMediaFileToInlineImage(List<SharedMediaFile> value) {
List<File> newFiles = List.empty(growable: true);
if (value.isNotEmpty) {
for (var element in value) {
newFiles.add(_covertSharedMediaFileToFile(element));
}
}
final List<InlineImage> listInlineImage = newFiles.map(
(e) => InlineImage(
ImageSource.local,
fileInfo: _covertFileToFileInfo(e),
)
).toList();
return listInlineImage;
}
List<FileInfo> covertListSharedMediaFileToFileInfo(List<SharedMediaFile> value) {
List<File> newFiles = List.empty(growable: true);
if (value.isNotEmpty) {
for (var element in value) {
newFiles.add(_covertSharedMediaFileToFile(element));
}
}
final List<FileInfo> listFileInfo = newFiles.map(
(e) => _covertFileToFileInfo(e),
).toList();
return listFileInfo;
}
void _addAttachmentFromFileShare(List<SharedMediaFile> 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<InlineImage?> _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<web_html_editor.FileUpload> 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<FileInfo> 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<web_html_editor.FileUpload>? 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)
);
}
}
@@ -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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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(
@@ -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!);
}
}
@@ -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("<div>${image.base64Uri ?? ''}</div><br>");
}
void insertImage(InlineImage inlineImage) {
editorController.insertHtml("<div>${inlineImage.base64Uri ?? ''}</div><br>");
}
void applyNewFontStyle(FontNameType? newFont) {
@@ -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
);
}
}
@@ -29,7 +29,8 @@ extension FileUploadExtension on FileUpload {
return FileInfo.fromBytes(
bytes: bytes,
name: name,
size: size
size: size,
type: type,
);
} else {
return null;
@@ -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<SharedMediaFile> {
List<FileInfo> toListFileInfo({bool? isShared}) => map((sharedMediaFile) => sharedMediaFile.toFileInfo(isShared: isShared)).toList();
}
@@ -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);
}
@@ -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<Result<T>> showFutureLoadingDialogFullScreen<T>({
required BuildContext context,
required async.Future<T> Function() future,
}) async {
return await showFutureLoadingDialog(
context: context,
title: AppLocalizations.of(context).loadingPleaseWait,
backLabel: AppLocalizations.of(context).close,
future: future,
);
}
async.Future<List<FileInfo>> 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 = <FileInfo>[];
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;
}
}
@@ -1,5 +0,0 @@
enum ImageSource {
local,
network
}
@@ -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<Object?> get props => [source, link, fileInfo, cid, base64Uri];
List<Object?> get props => [
fileInfo,
base64Uri
];
}
@@ -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);
@@ -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<double> 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,
@@ -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,
);
}
}
@@ -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;
}
@@ -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<DropZoneWidget> createState() => _DropZoneWidgetState();
}
class _DropZoneWidgetState extends State<DropZoneWidget> {
final _imagePaths = Get.find<ImagePaths>();
bool _isDragging = false;
@override
Widget build(BuildContext context) {
return DragTarget<Attachment>(
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<DropZoneWidget> {
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<DropZoneWidget> {
),
),
),
);
} 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
);
}
}
@@ -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: [
@@ -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,
)
]
),
),
),
),
),
);
}
}
@@ -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<WebEditorWidget> {
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<WebEditorWidget> {
WebScript(
name: HtmlUtils.unregisterDropListener.name,
script: HtmlUtils.unregisterDropListener.script,
),
)
])
),
htmlToolbarOptions: const HtmlToolbarOptions(
@@ -154,8 +151,8 @@ class _WebEditorState extends State<WebEditorWidget> {
),
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<WebEditorWidget> {
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: () {},
),
);
}