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
@@ -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,
];
}