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
+1
View File
@@ -45,6 +45,7 @@ export 'utils/file_utils.dart';
export 'utils/option_param_mixin.dart';
export 'utils/print_utils.dart';
export 'utils/broadcast_channel/broadcast_channel.dart';
export 'utils/list_utils.dart';
// Views
export 'presentation/views/text/slogan_builder.dart';
+16
View File
@@ -0,0 +1,16 @@
import 'package:dartz/dartz.dart';
Tuple2<List<T>, List<T>> partition<T>(List<T> list, bool Function(T) predicate) {
List<T> trueList = [];
List<T> falseList = [];
for (var element in list) {
if (predicate(element)) {
trueList.add(element);
} else {
falseList.add(element);
}
}
return Tuple2(trueList, falseList);
}
@@ -10,15 +10,11 @@ class DownloadImageAsBase64Success extends UIState {
final String base64Uri;
final String cid;
final FileInfo fileInfo;
final bool fromFileShared;
DownloadImageAsBase64Success(
this.base64Uri,
this.cid,
this.fileInfo,
{
this.fromFileShared = false
}
this.fileInfo
);
@override
@@ -26,7 +22,6 @@ class DownloadImageAsBase64Success extends UIState {
base64Uri,
cid,
fileInfo,
fromFileShared,
];
}
@@ -1,46 +1,25 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart';
class UploadAttachmentSuccess extends UIState {
final UploadAttachment uploadAttachment;
final bool isInline;
final bool fromFileShared;
UploadAttachmentSuccess(
this.uploadAttachment,
{
this.isInline = false,
this.fromFileShared = false,
}
);
UploadAttachmentSuccess(this.uploadAttachment);
@override
List<Object?> get props => [
uploadAttachment,
isInline,
fromFileShared,
];
List<Object?> get props => [uploadAttachment];
}
class UploadAttachmentFailure extends FeatureFailure {
final bool isInline;
final bool fromFileShared;
UploadAttachmentFailure(
dynamic exception,
{
this.isInline = false,
this.fromFileShared = false,
}
) : super(exception: exception);
final FileInfo fileInfo;
UploadAttachmentFailure(dynamic exception, this.fileInfo) : super(exception: exception);
@override
List<Object?> get props => [
isInline,
fromFileShared,
...super.props
];
List<Object?> get props => [...super.props, fileInfo];
}
@@ -16,7 +16,6 @@ class DownloadImageAsBase64Interactor {
{
double? maxWidth,
bool? compress,
bool fromFileShared = false,
}
) async* {
try {
@@ -33,7 +32,6 @@ class DownloadImageAsBase64Interactor {
result!,
cid,
fileInfo,
fromFileShared: fromFileShared
));
} else {
yield Left<Failure, Success>(DownloadImageAsBase64Failure(null));
@@ -15,8 +15,6 @@ class UploadAttachmentInteractor {
FileInfo fileInfo,
Uri uploadUri, {
CancelToken? cancelToken,
bool isInline = false,
bool fromFileShared = false,
}) async* {
try {
final uploadAttachment = await _composerRepository.uploadAttachment(
@@ -24,17 +22,9 @@ class UploadAttachmentInteractor {
uploadUri,
cancelToken: cancelToken
);
yield Right<Failure, Success>(UploadAttachmentSuccess(
uploadAttachment,
isInline: isInline,
fromFileShared: fromFileShared
));
yield Right<Failure, Success>(UploadAttachmentSuccess(uploadAttachment));
} catch (e) {
yield Left<Failure, Success>(UploadAttachmentFailure(
e,
isInline: isInline,
fromFileShared: fromFileShared
));
yield Left<Failure, Success>(UploadAttachmentFailure(e, fileInfo));
}
}
}
@@ -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: () {},
),
);
}
@@ -339,9 +339,9 @@ class EmailView extends GetWidget<SingleEmailController> {
responsiveUtils: controller.responsiveUtils,
attachments: controller.attachments,
imagePaths: controller.imagePaths,
onDragStarted: controller.mailboxDashBoardController.enableDraggableApp,
onDragStarted: controller.mailboxDashBoardController.enableAttachmentDraggableApp,
onDragEnd: (details) {
controller.mailboxDashBoardController.disableDraggableApp();
controller.mailboxDashBoardController.disableAttachmentDraggableApp();
},
downloadAttachmentAction: (attachment) => controller.handleDownloadAttachmentAction(context, attachment),
viewAttachmentAction: (attachment) => controller.handleViewAttachmentAction(context, attachment),
@@ -376,7 +376,7 @@ class EmailView extends GetWidget<SingleEmailController> {
Obx(() => CalendarEventDetailWidget(
calendarEvent: calendarEvent,
emailContent: controller.currentEmailLoaded?.htmlContent ?? '',
isDraggableAppActive: controller.mailboxDashBoardController.isDraggableAppActive,
isDraggableAppActive: controller.mailboxDashBoardController.isAttachmentDraggableAppActive,
onOpenComposerAction: controller.openNewComposerAction,
onOpenNewTabAction: controller.openNewTabAction,
onMailtoDelegateAction: controller.openMailToLink,
@@ -407,7 +407,14 @@ class EmailView extends GetWidget<SingleEmailController> {
mailtoDelegate: controller.openMailToLink,
direction: AppUtils.getCurrentDirection(context),
),
if (controller.mailboxDashBoardController.isDraggableAppActive)
if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive)
PointerInterceptor(
child: SizedBox(
width: constraints.maxWidth,
height: constraints.maxHeight,
)
),
if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive)
PointerInterceptor(
child: SizedBox(
width: constraints.maxWidth,
@@ -1,38 +1,7 @@
import 'package:core/data/constants/constant.dart';
import 'package:core/domain/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:http_parser/http_parser.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
extension AttachmentExtension on Attachment {
String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) {
final mediaType = type ?? fileMediaType;
if (isDisplayedPDFIcon) {
return imagePaths.icFilePdf;
}
if (mediaType == null) {
return imagePaths.icFileEPup;
}
if (mediaType.isDocFile()) {
return imagePaths.icFileDocx;
} else if (mediaType.isExcelFile()) {
return imagePaths.icFileXlsx;
} else if (mediaType.isPowerPointFile()) {
return imagePaths.icFilePptx;
} else if (mediaType.isPdfFile()) {
return imagePaths.icFilePdf;
} else if (mediaType.isZipFile()) {
return imagePaths.icFileZip;
} else if (mediaType.isImageFile()) {
return imagePaths.icFilePng;
}
return imagePaths.icFileEPup;
}
bool get isDisplayedPDFIcon => type?.mimeType == Constant.pdfMimeType
|| (type?.mimeType == Constant.octetStreamMimeType
&& name?.endsWith(Constant.pdfExtension) == true);
String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup;
}
@@ -5,7 +5,6 @@ import 'dart:io';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dio/dio.dart';
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:model/account/authentication_type.dart';
import 'package:model/account/password.dart';
@@ -122,8 +121,6 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey];
requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token);
requestOptions.headers[HttpHeaders.contentTypeHeader] = uploadExtra[FileUploader.typeExtraKey];
requestOptions.headers[HttpHeaders.contentLengthHeader] = uploadExtra[FileUploader.sizeExtraKey];
final newOptions = Options(
method: requestOptions.method,
@@ -155,11 +152,16 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
}
Stream<List<int>>? _getDataUploadRequest(dynamic mapUploadExtra) {
final currentPlatform = mapUploadExtra[FileUploader.platformExtraKey];
if (currentPlatform == 'web') {
return BodyBytesStream.fromBytes(mapUploadExtra[FileUploader.bytesExtraKey]);
} else {
return File(mapUploadExtra[FileUploader.filePathExtraKey]).openRead();
try {
String? filePath = mapUploadExtra[FileUploader.filePathExtraKey];
if (filePath?.isNotEmpty == true) {
return File(filePath!).openRead();
} else {
return mapUploadExtra[FileUploader.streamDataExtraKey];
}
} catch(e) {
log('AuthorizationInterceptors::_getDataUploadRequest: Exception = $e');
return null;
}
}
@@ -216,8 +216,9 @@ class MailboxDashBoardController extends ReloadableController {
final searchMailboxActivated = RxBool(false);
final listSendingEmails = RxList<SendingEmail>();
final refreshingMailboxState = Rx<Either<Failure, Success>>(Right(UIState.idle));
final draggableAppState = Rxn<DraggableAppState>();
final attachmentDraggableAppState = Rxn<DraggableAppState>();
final isRecoveringDeletedMessage = RxBool(false);
final localFileDraggableAppState = Rxn<DraggableAppState>();
Session? sessionCurrent;
Map<Role, MailboxId> mapDefaultMailboxIdByRole = {};
@@ -2148,14 +2149,16 @@ class MailboxDashBoardController extends ReloadableController {
openMailboxAction(inboxPresentation);
}
bool get isDraggableAppActive => draggableAppState.value == DraggableAppState.active;
bool get isAttachmentDraggableAppActive => attachmentDraggableAppState.value == DraggableAppState.active;
void enableDraggableApp() {
draggableAppState.value = DraggableAppState.active;
bool get isLocalFileDraggableAppActive => localFileDraggableAppState.value == DraggableAppState.active;
void enableAttachmentDraggableApp() {
attachmentDraggableAppState.value = DraggableAppState.active;
}
void disableDraggableApp() {
draggableAppState.value = DraggableAppState.inActive;
void disableAttachmentDraggableApp() {
attachmentDraggableAppState.value = DraggableAppState.inActive;
}
void saveEmailToDraft({required SaveToDraftArguments arguments}) {
@@ -27,10 +27,7 @@ import 'package:worker_manager/worker_manager.dart' as worker;
class FileUploader {
static const String uploadAttachmentExtraKey = 'upload-attachment';
static const String platformExtraKey = 'platform';
static const String bytesExtraKey = 'bytes';
static const String typeExtraKey = 'type';
static const String sizeExtraKey = 'size';
static const String streamDataExtraKey = 'streamData';
static const String filePathExtraKey = 'path';
final DioClient _dioClient;
@@ -94,10 +91,7 @@ class FileUploader {
final mapExtra = <String, dynamic>{
uploadAttachmentExtraKey: {
platformExtraKey: 'mobile',
filePathExtraKey: argsUpload.mobileFileUpload.filePath,
typeExtraKey: argsUpload.mobileFileUpload.mimeType,
sizeExtraKey: argsUpload.mobileFileUpload.fileSize,
}
};
@@ -109,7 +103,7 @@ class FileUploader {
),
data: File(argsUpload.mobileFileUpload.filePath).openRead(),
onSendProgress: (count, total) {
log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $count');
log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
sendPort.send(
UploadingAttachmentUploadState(
argsUpload.uploadId,
@@ -139,10 +133,7 @@ class FileUploader {
final mapExtra = <String, dynamic>{
uploadAttachmentExtraKey: {
platformExtraKey: 'web',
bytesExtraKey: fileInfo.bytes,
typeExtraKey: fileInfo.mimeType,
sizeExtraKey: fileInfo.fileSize,
streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!),
}
};
@@ -155,7 +146,7 @@ class FileUploader {
data: BodyBytesStream.fromBytes(fileInfo.bytes!),
cancelToken: cancelToken,
onSendProgress: (count, total) {
log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: [${uploadId.id}] = $count');
log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
onSendController.add(
Right(UploadingAttachmentUploadState(
uploadId,
@@ -0,0 +1 @@
class PickFileCanceledException implements Exception {}
@@ -3,5 +3,15 @@ import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart';
extension FileInfoExtension on FileInfo {
MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath, fileSize, mimeType);
MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath!, fileSize, mimeType);
FileInfo withInline() => FileInfo(
fileName: fileName,
fileSize: fileSize,
filePath: filePath,
bytes: bytes,
readStream: readStream,
type: type,
isInline: true,
isShared: isShared);
}
@@ -3,5 +3,5 @@ import 'package:model/upload/file_info.dart';
extension ListFileInfoExtension on List<FileInfo> {
List<int> get listSize => map((file) => file.fileSize).toList();
num get totalSize => listSize.reduce((sum, size) => sum + size);
num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size);
}
@@ -0,0 +1,28 @@
import 'package:core/domain/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType {
String getIcon(ImagePaths imagePaths, {String? fileName}) {
if (validatePDFIcon(fileName: fileName) == true) {
return imagePaths.icFilePdf;
} else if (isDocFile()) {
return imagePaths.icFileDocx;
} else if (isExcelFile()) {
return imagePaths.icFileXlsx;
} else if (isPowerPointFile()) {
return imagePaths.icFilePptx;
} else if (isPdfFile()) {
return imagePaths.icFilePdf;
} else if (isZipFile()) {
return imagePaths.icFileZip;
} else if (isImageFile()) {
return imagePaths.icFilePng;
} else {
return imagePaths.icFileEPup;
}
}
bool validatePDFIcon({required String? fileName}) => mimeType == 'application/pdf' ||
(mimeType == 'application/octet-stream' && fileName?.endsWith('.pdf') == true);
}
@@ -0,0 +1,13 @@
import 'package:core/utils/platform_info.dart';
import 'package:file_picker/file_picker.dart';
import 'package:model/upload/file_info.dart';
extension PlatformFileExtension on PlatformFile {
FileInfo toFileInfo() => FileInfo(
fileName: name,
fileSize: size,
filePath: PlatformInfo.isWeb ? '' : path ?? '',
bytes: bytes,
readStream: readStream
);
}
@@ -31,15 +31,11 @@ class SuccessAttachmentUploadState extends Success {
final UploadTaskId uploadId;
final Attachment attachment;
final FileInfo fileInfo;
final bool fromFileShared;
SuccessAttachmentUploadState(
this.uploadId,
this.attachment,
this.fileInfo,
{
this.fromFileShared = false
}
this.fileInfo
);
@override
@@ -47,7 +43,6 @@ class SuccessAttachmentUploadState extends Success {
uploadId,
attachment,
fileInfo,
fromFileShared,
];
}
@@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:model/upload/file_info.dart';
class LocalFilePickerLoading extends LoadingState {}
class LocalFilePickerSuccess extends UIState {
final List<FileInfo> pickedFiles;
@@ -14,6 +16,4 @@ class LocalFilePickerSuccess extends UIState {
class LocalFilePickerFailure extends FeatureFailure {
LocalFilePickerFailure(dynamic exception) : super(exception: exception);
}
class LocalFilePickerCancel extends FeatureFailure {}
}
@@ -0,0 +1,19 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:model/upload/file_info.dart';
class LocalImagePickerLoading extends LoadingState {}
class LocalImagePickerSuccess extends UIState {
final FileInfo fileInfo;
LocalImagePickerSuccess(this.fileInfo);
@override
List<Object> get props => [fileInfo];
}
class LocalImagePickerFailure extends FeatureFailure {
LocalImagePickerFailure(dynamic exception) : super(exception: exception);
}
@@ -4,7 +4,8 @@ import 'package:core/presentation/state/success.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:file_picker/file_picker.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart';
class LocalFilePickerInteractor {
@@ -13,23 +14,22 @@ class LocalFilePickerInteractor {
Stream<Either<Failure, Success>> execute({FileType fileType = FileType.any}) async* {
try {
yield Right<Failure, Success>(LocalFilePickerLoading());
final filesResult = await FilePicker.platform.pickFiles(
type: fileType,
allowMultiple: true,
withData: PlatformInfo.isWeb
withData: PlatformInfo.isWeb,
withReadStream: PlatformInfo.isMobile
);
if (filesResult != null && filesResult.files.isNotEmpty) {
final fileInfoResults = filesResult.files
.map((platformFile) => FileInfo(
platformFile.name,
PlatformInfo.isWeb ? '' : platformFile.path ?? '',
platformFile.size,
bytes: PlatformInfo.isWeb ? platformFile.bytes : null
))
.toList();
yield Right<Failure, Success>(LocalFilePickerSuccess(fileInfoResults));
if (filesResult?.files.isNotEmpty == true) {
final listFileInfo = filesResult!.files
.map((platformFile) => platformFile.toFileInfo())
.toList();
yield Right<Failure, Success>(LocalFilePickerSuccess(listFileInfo));
} else {
yield Left<Failure, Success>(LocalFilePickerCancel());
yield Left<Failure, Success>(LocalFilePickerFailure(PickFileCanceledException()));
}
} catch (exception) {
yield Left<Failure, Success>(LocalFilePickerFailure(exception));
@@ -0,0 +1,35 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:file_picker/file_picker.dart';
import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart';
class LocalImagePickerInteractor {
LocalImagePickerInteractor();
Stream<Either<Failure, Success>> execute() async* {
try {
yield Right<Failure, Success>(LocalImagePickerLoading());
final filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: false,
withReadStream: PlatformInfo.isMobile,
withData: PlatformInfo.isWeb
);
if (filePickerResult?.files.isNotEmpty == true) {
final fileInfo = filePickerResult!.files.first.toFileInfo();
yield Right<Failure, Success>(LocalImagePickerSuccess(fileInfo));
} else {
yield Left<Failure, Success>(LocalImagePickerFailure(PickFileCanceledException()));
}
} catch (exception) {
yield Left<Failure, Success>(LocalImagePickerFailure(exception));
}
}
}
@@ -14,13 +14,13 @@ import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
import 'package:model/email/attachment.dart';
import 'package:model/extensions/attachment_extension.dart';
import 'package:model/extensions/list_attachment_extension.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/base/state/base_ui_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
import 'package:tmail_ui_user/features/upload/presentation/extensions/upload_attachment_extension.dart';
@@ -80,7 +80,7 @@ class UploadController extends BaseController {
failure.uploadId,
(currentState) => currentState?.copyWith(uploadStatus: UploadFileStatus.uploadFailed));
deleteFileUploaded(failure.uploadId);
_handleUploadAttachmentsFailure(failure);
_showToastMessageWhenUploadAttachmentsFailure(failure);
}
},
(success) {
@@ -115,7 +115,7 @@ class UploadController extends BaseController {
);
_refreshListUploadAttachmentState();
_handleUploadAttachmentsSuccess(success);
_showToastMessageWhenUploadAttachmentsSuccess(success);
}
}
);
@@ -162,7 +162,7 @@ class UploadController extends BaseController {
);
final uploadFileState = _uploadingStateInlineFiles.getUploadFileStateById(success.uploadId);
log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.fromFileShared}');
log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.file?.isShared}');
_uploadingStateInlineFiles.updateElementByUploadTaskId(
success.uploadId,
@@ -179,7 +179,6 @@ class UploadController extends BaseController {
success.uploadId,
inlineAttachment,
success.fileInfo,
fromFileShared: uploadFileState?.fromFileShared ?? false
);
_handleUploadInlineAttachmentsSuccess(newUploadSuccess);
}
@@ -203,27 +202,24 @@ class UploadController extends BaseController {
_refreshListUploadAttachmentState();
}
Future<void> justUploadAttachmentsAction(List<FileInfo> uploadFiles, Uri uploadUri) {
Future<void> justUploadAttachmentsAction({
required List<FileInfo> uploadFiles,
required Uri uploadUri,
}) {
return Future.forEach<FileInfo>(uploadFiles, (uploadFile) async {
await uploadFileAction(uploadFile, uploadUri);
await uploadFileAction(uploadFile: uploadFile, uploadUri: uploadUri);
});
}
Future<void> uploadFileAction(
FileInfo uploadFile,
Uri uploadUri,
{
bool isInline = false,
bool fromFileShared = false,
}
) {
log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | isInline: $isInline | fromFileShared: $fromFileShared');
Future<void> uploadFileAction({
required FileInfo uploadFile,
required Uri uploadUri,
}) {
log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | mimeType: ${uploadFile.mimeType} | isInline: ${uploadFile.isInline} | fromFileShared: ${uploadFile.isShared}');
consumeState(_uploadAttachmentInteractor.execute(
uploadFile,
uploadUri,
cancelToken: CancelToken(),
isInline: isInline,
fromFileShared: fromFileShared
));
return Future.value();
}
@@ -244,6 +240,16 @@ class UploadController extends BaseController {
.toList();
}
List<FileInfo> get attachmentsPicked {
if (listUploadAttachments.isEmpty) {
return List.empty();
}
return listUploadAttachments
.map((fileState) => fileState.file)
.whereNotNull()
.toList();
}
Set<EmailBodyPart>? generateAttachments() {
if (attachmentsUploaded.isEmpty) {
return null;
@@ -254,7 +260,7 @@ class UploadController extends BaseController {
.toSet();
}
void _handleUploadAttachmentsFailure(ErrorAttachmentUploadState failure) {
void _showToastMessageWhenUploadAttachmentsFailure(ErrorAttachmentUploadState failure) {
if (currentContext != null && currentOverlayContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
@@ -264,7 +270,7 @@ class UploadController extends BaseController {
}
}
void _handleUploadAttachmentsSuccess(SuccessAttachmentUploadState success) {
void _showToastMessageWhenUploadAttachmentsSuccess(SuccessAttachmentUploadState success) {
if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) {
appToast.showToastSuccessMessage(
currentOverlayContext!,
@@ -275,7 +281,7 @@ class UploadController extends BaseController {
}
bool isExceededMaxSizeAttachmentsPerEmail({num totalSizePreparedFiles = 0}) {
final currentTotalSize = attachmentsUploaded.totalSize + inlineAttachmentsUploaded.totalSize + totalSizePreparedFiles;
final currentTotalSize = attachmentsPicked.totalSize + inlineAttachmentsPicked.totalSize + totalSizePreparedFiles;
final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value;
log('UploadController::isExceededMaxSizeAttachmentsPerEmail(): currentTotalSize = $currentTotalSize | maxSizeAttachmentsPerEmail = $maxSizeAttachmentsPerEmail');
if (maxSizeAttachmentsPerEmail != null) {
@@ -285,16 +291,17 @@ class UploadController extends BaseController {
}
}
bool isExceededMaxSizeFilesAttachedInComposer({num totalSizePreparedFiles = 0}) {
final currentTotalSizeAttachments = attachmentsUploaded.totalSize + totalSizePreparedFiles;
const maximumBytesSizeFileAttachedInComposer = AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024;
bool isExceededWarningAttachmentFileSizeInComposer({num totalSizePreparedFiles = 0}) {
final currentTotalSizeAttachments = attachmentsPicked.totalSize + totalSizePreparedFiles;
const maximumBytesSizeFileAttachedInComposer = AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024;
log('UploadController::isExceededMaxSizeFilesAttachedInComposer(): currentTotalSizeAttachments = $currentTotalSizeAttachments | maximumBytesSizeFileAttachedInComposer = $maximumBytesSizeFileAttachedInComposer');
return currentTotalSizeAttachments > maximumBytesSizeFileAttachedInComposer;
}
void validateTotalSizeAttachmentsBeforeUpload({
required num totalSizePreparedFiles,
VoidCallback? callbackAction
num? totalSizePreparedFilesWithDispositionAttachment,
VoidCallback? onValidationSuccess
}) {
log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizePreparedFiles = $totalSizePreparedFiles');
if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) {
@@ -307,7 +314,7 @@ class UploadController extends BaseController {
return;
}
if (isExceededMaxSizeFilesAttachedInComposer(totalSizePreparedFiles: totalSizePreparedFiles)) {
if (isExceededWarningAttachmentFileSizeInComposer(totalSizePreparedFiles: totalSizePreparedFilesWithDispositionAttachment ?? totalSizePreparedFiles)) {
if (currentContext == null) {
log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL');
return;
@@ -315,12 +322,34 @@ class UploadController extends BaseController {
_showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer(
context: currentContext!,
confirmAction: callbackAction
confirmAction: () async {
await Future.delayed(
const Duration(milliseconds: 100),
onValidationSuccess
);
}
);
return;
}
callbackAction?.call();
onValidationSuccess?.call();
}
void validateTotalSizeInlineAttachmentsBeforeUpload({
required num totalSizePreparedFiles,
VoidCallback? onValidationSuccess
}) {
if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) {
if (currentContext == null) {
log('UploadController::validateTotalSizeInlineAttachmentsBeforeUpload: CONTEXT IS NULL');
return;
}
_showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail(context: currentContext!);
return;
}
onValidationSuccess?.call();
}
void _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail({required BuildContext context}) {
@@ -340,7 +369,7 @@ class UploadController extends BaseController {
showConfirmDialogAction(
context,
title: '',
AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer,
AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer,
AppLocalizations.of(context).continueAction,
cancelTitle: AppLocalizations.of(context).cancel,
alignCenter: true,
@@ -394,6 +423,17 @@ class UploadController extends BaseController {
.toList();
}
List<FileInfo> get inlineAttachmentsPicked {
if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) {
return List.empty();
}
return _uploadingStateInlineFiles.uploadingStateFiles
.whereNotNull()
.map((fileState) => fileState.file)
.whereNotNull()
.toList();
}
Map<String, Attachment> get mapInlineAttachments {
final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles
.whereNotNull()
@@ -415,42 +455,47 @@ class UploadController extends BaseController {
return mapInlineAttachments;
}
void _handleUploadAttachmentFailure(UploadAttachmentFailure failure) {
if (currentContext != null && currentOverlayContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
failure.fileInfo.isInline == true
? AppLocalizations.of(currentContext!).thisImageCannotBeAdded
: AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments,
leadingSVGIconColor: Colors.white,
leadingSVGIcon: failure.fileInfo.isInline == true
? imagePaths.icInsertImage
: imagePaths.icAttachment
);
}
}
void _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async {
if (success.uploadAttachment.fileInfo.isInline == true) {
_uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState());
await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState);
} else {
_uploadingStateFiles.add(success.uploadAttachment.toUploadFileState());
await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState);
_refreshListUploadAttachmentState();
}
}
@override
void handleFailureViewState(Failure failure) async {
super.handleFailureViewState(failure);
void handleFailureViewState(Failure failure) {
if (failure is UploadAttachmentFailure) {
if (failure.isInline) {
if (currentContext != null && currentOverlayContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).thisImageCannotBeAdded,
leadingSVGIconColor: Colors.white,
leadingSVGIcon: imagePaths.icInsertImage);
}
} else {
if (currentContext != null && currentOverlayContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments,
leadingSVGIconColor: Colors.white,
leadingSVGIcon: imagePaths.icAttachment);
}
}
_handleUploadAttachmentFailure(failure);
} else {
super.handleFailureViewState(failure);
}
}
@override
void handleSuccessViewState(Success success) async {
super.handleSuccessViewState(success);
if (success is UploadAttachmentSuccess) {
if (success.isInline) {
_uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState(fromFileShared: success.fromFileShared));
await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState);
} else {
_uploadingStateFiles.add(success.uploadAttachment.toUploadFileState());
await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState);
_refreshListUploadAttachmentState();
}
_handleUploadAttachmentSuccess(success);
} else {
super.handleSuccessViewState(success);
}
}
}
@@ -4,12 +4,11 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta
extension UploadAttachmentExtension on UploadAttachment {
UploadFileState toUploadFileState({bool fromFileShared = false}) {
UploadFileState toUploadFileState() {
return UploadFileState(
uploadTaskId,
file: fileInfo,
cancelToken: cancelToken,
fromFileShared: fromFileShared,
);
}
}
@@ -1,11 +1,12 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:http_parser/http_parser.dart';
import 'package:model/email/attachment.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart';
@@ -17,7 +18,6 @@ class UploadFileState with EquatableMixin {
final int uploadingProgress;
final Attachment? attachment;
final CancelToken? cancelToken;
final bool fromFileShared;
UploadFileState(
this.uploadTaskId,
@@ -27,7 +27,6 @@ class UploadFileState with EquatableMixin {
this.uploadingProgress = 0,
this.attachment,
this.cancelToken,
this.fromFileShared = false,
}
);
@@ -38,7 +37,6 @@ class UploadFileState with EquatableMixin {
int? uploadingProgress,
Attachment? attachment,
CancelToken? cancelToken,
bool? fromFileShared,
}) {
return UploadFileState(
uploadTaskId ?? this.uploadTaskId,
@@ -47,7 +45,6 @@ class UploadFileState with EquatableMixin {
uploadingProgress: uploadingProgress ?? this.uploadingProgress,
attachment: attachment ?? this.attachment,
cancelToken: cancelToken ?? this.cancelToken,
fromFileShared: fromFileShared ?? this.fromFileShared
);
}
@@ -72,11 +69,16 @@ class UploadFileState with EquatableMixin {
double get percentUploading => uploadingProgress / 100;
String getIcon(ImagePaths imagePaths) {
var mediaType = attachment?.type;
if (mediaType == null && file != null) {
mediaType = MediaType.parse(file!.mimeType);
try {
MediaType? mediaType = attachment?.type;
if (mediaType == null && file != null) {
mediaType = MediaType.parse(file!.mimeType);
}
return mediaType?.getIcon(imagePaths, fileName: fileName) ?? imagePaths.icFileEPup;
} catch (e) {
logError('UploadFileState::getIcon: Exception: $e');
return imagePaths.icFileEPup;
}
return attachment?.getIcon(imagePaths, fileMediaType: mediaType) ?? imagePaths.icFileEPup;
}
@override
@@ -87,6 +89,5 @@ class UploadFileState with EquatableMixin {
uploadingProgress,
attachment,
cancelToken,
fromFileShared,
];
}
+2 -2
View File
@@ -3239,8 +3239,8 @@
"placeholders_order": [],
"placeholders": {}
},
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": {
"warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.",
"@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
+2 -2
View File
@@ -3936,8 +3936,8 @@
"placeholders_order": [],
"placeholders": {}
},
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": {
"warningMessageWhenExceedGenerallySizeInComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.",
"@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
+15 -3
View File
@@ -1,5 +1,5 @@
{
"@@last_modified": "2024-02-29T16:08:18.479839",
"@@last_modified": "2024-03-15T21:34:52.177175",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
@@ -3750,8 +3750,8 @@
"placeholders_order": [],
"placeholders": {}
},
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": {
"warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.",
"@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
@@ -3761,5 +3761,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"thisFileCannotBePicked": "This file cannot be picked.",
"@thisFileCannotBePicked": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"loadingPleaseWait": "Loading... Please wait!",
"@loadingPleaseWait": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
+2
View File
@@ -3944,6 +3944,8 @@
},
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": {
"warningMessageWhenExceedGenerallySizeInComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.",
"@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
+16 -2
View File
@@ -3917,10 +3917,10 @@ class AppLocalizations {
);
}
String get messageWarningDialogWhenExceedMaximumFileSizeComposer {
String get warningMessageWhenExceedGenerallySizeInComposer {
return Intl.message(
'Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.',
name: 'messageWarningDialogWhenExceedMaximumFileSizeComposer',
name: 'warningMessageWhenExceedGenerallySizeInComposer',
);
}
@@ -3930,4 +3930,18 @@ class AppLocalizations {
name: 'continueAction',
);
}
String get thisFileCannotBePicked {
return Intl.message(
'This file cannot be picked.',
name: 'thisFileCannotBePicked',
);
}
String get loadingPleaseWait {
return Intl.message(
'Loading... Please wait!',
name: 'loadingPleaseWait',
);
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class AppConfig {
static const int limitCharToStartSearch = 3;
static const int maximumMegabytesSizeFileAttachedInComposer = 10;
static const int warningAttachmentFileSizeInMegabytes = 10;
static const String appDashboardConfigurationPath = "configurations/app_dashboard.json";
static const String appFCMConfigurationPath = "configurations/env.fcm";
+44 -11
View File
@@ -1,32 +1,65 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:mime/mime.dart';
class FileInfo with EquatableMixin {
final String fileName;
final String filePath;
final int fileSize;
final String? filePath;
final Uint8List? bytes;
final Stream<List<int>>? readStream;
final String? type;
final bool? isInline;
final bool? isShared;
FileInfo(this.fileName, this.filePath, this.fileSize, {this.bytes});
factory FileInfo.empty() {
return FileInfo('', '', 0);
}
FileInfo({
required this.fileName,
required this.fileSize,
this.filePath,
this.bytes,
this.readStream,
this.type,
this.isInline,
this.isShared,
});
factory FileInfo.fromBytes({
required Uint8List bytes,
String? name,
int? size
int? size,
String? type,
}) {
return FileInfo(name ?? '', '', size ?? 0, bytes: bytes);
return FileInfo(
fileName: name ?? '',
fileSize: size ?? 0,
bytes: bytes,
type: type
);
}
String get fileExtension => fileName.split('.').last;
String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/octet-stream';
String get mimeType {
if (type?.isNotEmpty == true) {
return type!;
} else if (filePath?.isNotEmpty == true){
final matchedType = lookupMimeType(filePath!, headerBytes: bytes) ?? 'application/octet-stream';
return matchedType;
} else {
final matchedType = lookupMimeType(fileName, headerBytes: bytes) ?? 'application/octet-stream';
return matchedType;
}
}
@override
List<Object?> get props => [fileName, filePath, fileSize, bytes];
List<Object?> get props => [
fileName,
filePath,
fileSize,
bytes,
readStream,
type,
isInline,
isShared,
];
}
+16
View File
@@ -360,6 +360,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
url: "https://pub.dev"
source: hosted
version: "0.4.4"
device_info_plus:
dependency: "direct main"
description:
@@ -990,6 +998,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.0"
future_loading_dialog:
dependency: "direct main"
description:
name: future_loading_dialog
sha256: "2718b1a308db452da32ab9bca9ad496ff92b683e217add9e92cf50520f90537e"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
get:
dependency: "direct main"
description:
+4
View File
@@ -240,6 +240,10 @@ dependencies:
mime: 1.0.4
desktop_drop: 0.4.4
future_loading_dialog: 0.3.0
dev_dependencies:
flutter_test:
sdk: flutter
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:http_parser/http_parser.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
void main() {
final attachmentA = Attachment(
@@ -34,7 +34,7 @@ void main() {
'WHEN perform call `attachmentA.isDisplayedPDFIcon()`\n'
'SHOULD return true',
() {
bool result = attachmentA.isDisplayedPDFIcon;
bool result = attachmentA.type!.validatePDFIcon(fileName: null);
expect(result, isTrue);
});
@@ -45,7 +45,7 @@ void main() {
'WHEN perform call `attachmentB.isDisplayedPDFIcon()`\n'
'SHOULD return true',
() {
bool result = attachmentB.isDisplayedPDFIcon;
bool result = attachmentB.type!.validatePDFIcon(fileName: 'attachmentB.pdf');
expect(result, isTrue);
});
@@ -56,7 +56,7 @@ void main() {
'WHEN perform call `attachmentC.isDisplayedPDFIcon()`\n'
'SHOULD return false',
() {
bool result = attachmentC.isDisplayedPDFIcon;
bool result = attachmentC.type!.validatePDFIcon(fileName: 'attachmentC.docx');
expect(result, isFalse);
});
@@ -67,7 +67,7 @@ void main() {
'WHEN perform call `attachmentD.isDisplayedPDFIcon()`\n'
'SHOULD return false',
() {
bool result = attachmentD.isDisplayedPDFIcon;
bool result = attachmentD.type!.validatePDFIcon(fileName: 'attachmentD.png');
expect(result, isFalse);
});
@@ -78,7 +78,7 @@ void main() {
'WHEN perform call `attachmentE.isDisplayedPDFIcon()`\n'
'SHOULD return false',
() {
bool result = attachmentE.isDisplayedPDFIcon;
bool result = attachmentE.type!.validatePDFIcon(fileName: 'attachmentE.pdf');
expect(result, isFalse);
});