TF-3084 Handle drop image to identity editor for public asset

This commit is contained in:
DatDang
2024-08-15 09:20:45 +07:00
committed by Dat H. Pham
parent 791edb41c2
commit 0cb9ddf350
15 changed files with 422 additions and 183 deletions
+1
View File
@@ -6,6 +6,7 @@ export 'presentation/extensions/url_extension.dart';
export 'presentation/extensions/uri_extension.dart'; export 'presentation/extensions/uri_extension.dart';
export 'presentation/extensions/capitalize_extension.dart'; export 'presentation/extensions/capitalize_extension.dart';
export 'presentation/extensions/list_extensions.dart'; export 'presentation/extensions/list_extensions.dart';
export 'presentation/extensions/list_nullable_extensions.dart';
export 'domain/extensions/datetime_extension.dart'; export 'domain/extensions/datetime_extension.dart';
export 'presentation/extensions/html_extension.dart'; export 'presentation/extensions/html_extension.dart';
export 'presentation/extensions/compare_string_extension.dart'; export 'presentation/extensions/compare_string_extension.dart';
@@ -0,0 +1,4 @@
extension ListNullableExtensions<T> on List<T>? {
bool get validateFilesTransfer => this?.any((type) => type == 'Files') ?? false;
}
@@ -385,7 +385,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_subscriptionOnDragEnter = html.window.onDragEnter.listen((event) { _subscriptionOnDragEnter = html.window.onDragEnter.listen((event) {
event.preventDefault(); event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) { if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
} }
}); });
@@ -393,7 +393,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_subscriptionOnDragOver = html.window.onDragOver.listen((event) { _subscriptionOnDragOver = html.window.onDragOver.listen((event) {
event.preventDefault(); event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) { if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
} }
}); });
@@ -401,7 +401,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_subscriptionOnDragLeave = html.window.onDragLeave.listen((event) { _subscriptionOnDragLeave = html.window.onDragLeave.listen((event) {
event.preventDefault(); event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) { if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
} }
}); });
@@ -409,16 +409,12 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_subscriptionOnDrop = html.window.onDrop.listen((event) { _subscriptionOnDrop = html.window.onDrop.listen((event) {
event.preventDefault(); event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) { if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive; mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
} }
}); });
} }
bool _validateFilesTransfer(List<dynamic>? types) {
return types?.any((type) => type == 'Files') ?? false;
}
Future<void> _saveComposerCacheOnWebAction() async { Future<void> _saveComposerCacheOnWebAction() async {
_autoCreateEmailTag(); _autoCreateEmailTag();
@@ -2130,7 +2126,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
} }
void handleOnDragEnterHtmlEditorWeb(List<dynamic>? types) { void handleOnDragEnterHtmlEditorWeb(List<dynamic>? types) {
if (_validateFilesTransfer(types)) { if (types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active; mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
} }
} }
@@ -14,6 +14,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
final ImagePaths imagePaths; final ImagePaths imagePaths;
final double? width; final double? width;
final double? height; final double? height;
final EdgeInsetsGeometry margin;
final OnLocalFileDropZoneListener? onLocalFileDropZoneListener; final OnLocalFileDropZoneListener? onLocalFileDropZoneListener;
const LocalFileDropZoneWidget({ const LocalFileDropZoneWidget({
@@ -21,6 +22,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
required this.imagePaths, required this.imagePaths,
this.width, this.width,
this.height, this.height,
this.margin = DropZoneWidgetStyle.margin,
this.onLocalFileDropZoneListener this.onLocalFileDropZoneListener
}); });
@@ -32,7 +34,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
width: width, width: width,
height: height, height: height,
child: Padding( child: Padding(
padding: DropZoneWidgetStyle.margin, padding: margin,
child: DottedBorder( child: DottedBorder(
borderType: BorderType.RRect, borderType: BorderType.RRect,
radius: const Radius.circular(DropZoneWidgetStyle.radius), radius: const Radius.circular(DropZoneWidgetStyle.radius),
@@ -52,7 +54,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SvgPicture.asset(imagePaths.icDropZoneIcon), Flexible(child: SvgPicture.asset(imagePaths.icDropZoneIcon)),
const SizedBox(height: DropZoneWidgetStyle.space), const SizedBox(height: DropZoneWidgetStyle.space),
Text( Text(
AppLocalizations.of(context).dropFileHereToAttachThem, AppLocalizations.of(context).dropFileHereToAttachThem,
@@ -3,12 +3,15 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:core/presentation/extensions/list_nullable_extensions.dart';
import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/presentation/utils/keyboard_utils.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/file_utils.dart'; import 'package:core/utils/file_utils.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@@ -32,7 +35,7 @@ import 'package:rich_text_composer/views/commons/constants.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/extesions/size_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/utils/identity_creator_constants.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/utils/identity_creator_constants.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/email_address_validator.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/email_address_validator.dart';
@@ -40,6 +43,7 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification
import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart';
import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart';
import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.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/state/get_all_identities_state.dart';
@@ -51,12 +55,16 @@ import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_i
import 'package:tmail_ui_user/features/public_asset/presentation/model/public_asset_arguments.dart'; import 'package:tmail_ui_user/features/public_asset/presentation/model/public_asset_arguments.dart';
import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_bindings.dart'; import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_bindings.dart';
import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_controller.dart'; import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_controller.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/list_platform_file_extensions.dart';
import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html hide File;
class IdentityCreatorController extends BaseController { class IdentityCreatorController extends BaseController with DragDropFileMixin {
final VerifyNameInteractor _verifyNameInteractor; final VerifyNameInteractor _verifyNameInteractor;
final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor;
@@ -74,6 +82,7 @@ class IdentityCreatorController extends BaseController {
final isDefaultIdentity = RxBool(false); final isDefaultIdentity = RxBool(false);
final isDefaultIdentitySupported = RxBool(false); final isDefaultIdentitySupported = RxBool(false);
final isCompressingInlineImage = RxBool(false); final isCompressingInlineImage = RxBool(false);
final draggableAppState = DraggableAppState.inActive.obs;
final TextEditingController inputNameIdentityController = TextEditingController(); final TextEditingController inputNameIdentityController = TextEditingController();
final TextEditingController inputBccIdentityController = TextEditingController(); final TextEditingController inputBccIdentityController = TextEditingController();
@@ -83,6 +92,10 @@ class IdentityCreatorController extends BaseController {
RichTextMobileTabletController? richTextMobileTabletController; RichTextMobileTabletController? richTextMobileTabletController;
RichTextWebController? richTextWebController; RichTextWebController? richTextWebController;
StreamSubscription<html.Event>? _subscriptionOnDragEnter;
StreamSubscription<html.Event>? _subscriptionOnDragOver;
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
StreamSubscription<html.Event>? _subscriptionOnDrop;
String? _nameIdentity; String? _nameIdentity;
String? _contentHtmlEditor; String? _contentHtmlEditor;
@@ -103,14 +116,23 @@ class IdentityCreatorController extends BaseController {
void updateContentHtmlEditor(String? text) => _contentHtmlEditor = text; void updateContentHtmlEditor(String? text) => _contentHtmlEditor = text;
String? get contentHtmlEditor { String get contentHtmlEditor {
if (_contentHtmlEditor != null) { if (_contentHtmlEditor != null) {
return _contentHtmlEditor; return _contentHtmlEditor ?? '';
} else { } else {
return arguments?.identity?.signatureAsString; return arguments?.identity?.signatureAsString ?? '';
} }
} }
int get maxSizeUploadByBytes {
if (session == null || accountId == null) return 0;
return session!.getCapabilityProperties<CoreCapability>(
accountId!,
CapabilityIdentifier.jmapCore
)?.maxSizeUpload?.value.toInt() ?? 0;
}
IdentityCreatorController( IdentityCreatorController(
this._verifyNameInteractor, this._verifyNameInteractor,
this._getAllIdentitiesInteractor, this._getAllIdentitiesInteractor,
@@ -138,10 +160,14 @@ class IdentityCreatorController extends BaseController {
session = arguments!.session; session = arguments!.session;
identity = arguments!.identity; identity = arguments!.identity;
actionType.value = arguments!.actionType; actionType.value = arguments!.actionType;
if (actionType.value == IdentityActionType.create) {
isLoadSignatureCompleted = true;
}
_checkDefaultIdentityIsSupported(); _checkDefaultIdentityIsSupported();
_checkPublicAssetCapability(); _checkPublicAssetCapability();
_setUpValueFromIdentity(); _setUpValueFromIdentity();
_getAllIdentities(); _getAllIdentities();
_triggerBrowserEventListener();
} }
} }
@@ -152,6 +178,46 @@ class IdentityCreatorController extends BaseController {
} }
} }
void _triggerBrowserEventListener() {
_subscriptionOnDragEnter = html.window.onDragEnter.listen((event) {
event.preventDefault();
if (event.dataTransfer.types.validateFilesTransfer) {
draggableAppState.value = DraggableAppState.active;
}
});
_subscriptionOnDragOver = html.window.onDragOver.listen((event) {
event.preventDefault();
if (event.dataTransfer.types.validateFilesTransfer) {
draggableAppState.value = DraggableAppState.active;
}
});
_subscriptionOnDragLeave = html.window.onDragLeave.listen((event) {
event.preventDefault();
if (event.dataTransfer.types.validateFilesTransfer) {
draggableAppState.value = DraggableAppState.inActive;
}
});
_subscriptionOnDrop = html.window.onDrop.listen((event) {
event.preventDefault();
if (event.dataTransfer.types.validateFilesTransfer) {
draggableAppState.value = DraggableAppState.inActive;
}
});
}
void handleOnDragEnterSignatureEditorWeb(List<dynamic>? types) {
if (types.validateFilesTransfer) {
draggableAppState.value = DraggableAppState.active;
}
}
@override @override
void onClose() { void onClose() {
log('IdentityCreatorController::onClose():'); log('IdentityCreatorController::onClose():');
@@ -170,6 +236,10 @@ class IdentityCreatorController extends BaseController {
} }
Get.delete<PublicAssetController>(tag: BindingTag.publicAssetBindingsTag); Get.delete<PublicAssetController>(tag: BindingTag.publicAssetBindingsTag);
publicAssetController = null; publicAssetController = null;
_subscriptionOnDragEnter?.cancel();
_subscriptionOnDragOver?.cancel();
_subscriptionOnDragLeave?.cancel();
_subscriptionOnDrop?.cancel();
super.onClose(); super.onClose();
} }
@@ -177,7 +247,7 @@ class IdentityCreatorController extends BaseController {
void handleSuccessViewState(Success success) { void handleSuccessViewState(Success success) {
super.handleSuccessViewState(success); super.handleSuccessViewState(success);
if (success is GetAllIdentitiesSuccess) { if (success is GetAllIdentitiesSuccess) {
_getALlIdentitiesSuccess(success); _getAllIdentitiesSuccess(success);
} }
} }
@@ -185,7 +255,7 @@ class IdentityCreatorController extends BaseController {
void handleFailureViewState(Failure failure) { void handleFailureViewState(Failure failure) {
super.handleFailureViewState(failure); super.handleFailureViewState(failure);
if (failure is GetAllIdentitiesFailure) { if (failure is GetAllIdentitiesFailure) {
_getALlIdentitiesFailure(failure); _getAllIdentitiesFailure(failure);
} }
} }
@@ -218,7 +288,7 @@ class IdentityCreatorController extends BaseController {
} }
} }
void _getALlIdentitiesSuccess(GetAllIdentitiesSuccess success) { void _getAllIdentitiesSuccess(GetAllIdentitiesSuccess success) {
if (success.identities?.isNotEmpty == true) { if (success.identities?.isNotEmpty == true) {
listEmailAddressDefault.value = success.identities! listEmailAddressDefault.value = success.identities!
.map((identity) => identity.toEmailAddressNoName()) .map((identity) => identity.toEmailAddressNoName())
@@ -236,7 +306,7 @@ class IdentityCreatorController extends BaseController {
} }
} }
void _getALlIdentitiesFailure(GetAllIdentitiesFailure failure) { void _getAllIdentitiesFailure(GetAllIdentitiesFailure failure) {
_setDefaultEmailAddressList(); _setDefaultEmailAddressList();
} }
@@ -339,11 +409,11 @@ class IdentityCreatorController extends BaseController {
bccOfIdentity.value = newEmailAddress; bccOfIdentity.value = newEmailAddress;
} }
Future<String?> _getSignatureHtmlText() async { Future<String> _getSignatureHtmlText() async {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
return richTextWebController?.editorController.getText(); return (await richTextWebController?.editorController.getText()) ?? '';
} else { } else {
return richTextMobileTabletController?.richTextController.htmlEditorApi?.getText(); return (await richTextMobileTabletController?.richTextController.htmlEditorApi?.getText()) ?? '';
} }
} }
@@ -380,7 +450,7 @@ class IdentityCreatorController extends BaseController {
: null; : null;
final publicAssetsInIdentityArguments = PublicAssetsInIdentityArguments( final publicAssetsInIdentityArguments = PublicAssetsInIdentityArguments(
htmlSignature: signatureHtmlText ?? '', htmlSignature: signatureHtmlText,
preExistingPublicAssetIds: List.from(publicAssetController?.preExistingPublicAssetIds ?? []), preExistingPublicAssetIds: List.from(publicAssetController?.preExistingPublicAssetIds ?? []),
newlyPickedPublicAssetIds: List.from(publicAssetController?.newlyPickedPublicAssetIds ?? []), newlyPickedPublicAssetIds: List.from(publicAssetController?.newlyPickedPublicAssetIds ?? []),
); );
@@ -390,7 +460,7 @@ class IdentityCreatorController extends BaseController {
email: emailOfIdentity.value?.email, email: emailOfIdentity.value?.email,
replyTo: replyToAddress, replyTo: replyToAddress,
bcc: bccAddress, bcc: bccAddress,
htmlSignature: Signature(signatureHtmlText ?? ''), htmlSignature: Signature(signatureHtmlText),
sortOrder: sortOrder); sortOrder: sortOrder);
final generateCreateId = Id(uuid.v1()); final generateCreateId = Id(uuid.v1());
@@ -470,6 +540,8 @@ class IdentityCreatorController extends BaseController {
} }
void clearFocusEditor(BuildContext context) { void clearFocusEditor(BuildContext context) {
inputNameIdentityFocusNode.unfocus();
inputBccIdentityFocusNode.unfocus();
if (PlatformInfo.isMobile) { if (PlatformInfo.isMobile) {
richTextMobileTabletController?.richTextController.htmlEditorApi?.unfocus(); richTextMobileTabletController?.richTextController.htmlEditorApi?.unfocus();
} }
@@ -550,46 +622,30 @@ class IdentityCreatorController extends BaseController {
} }
} }
bool _isExceedMaxSizeInlineImage(int fileSize) =>
fileSize > IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes;
bool _isExceedMaxUploadSize(int fileSize) { bool _isExceedMaxUploadSize(int fileSize) {
final coreCapability = session?.getCapabilityProperties<CoreCapability>( return fileSize > maxSizeUploadByBytes;
accountId!,
CapabilityIdentifier.jmapCore
);
int maxUploadSize = coreCapability?.maxSizeUpload?.value.toInt() ?? 0;
return fileSize > maxUploadSize;
} }
void _insertInlineImage( Future<void> _insertInlineImage(
BuildContext context, BuildContext context,
PlatformFile platformFile, PlatformFile platformFile,
int maxWidth int maxWidth,
{PlatformFile? compressedFile}
) async { ) async {
final PlatformFile file; final PlatformFile? file;
try { if (compressedFile != null) {
isCompressingInlineImage.value = true; file = compressedFile;
file = await _compressImage(platformFile, maxWidth); } else {
isCompressingInlineImage.value = false; file = await _compressFileAction(context, originalFile: platformFile, maxWidth: maxWidth);
} catch (e) {
logError("IdentityCreatorController::_insertInlineImage: compress image error: $e");
isCompressingInlineImage.value = false;
if (context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).cannotCompressInlineImage);
}
return;
} }
if (_isExceedMaxSizeInlineImage(file.size) || _isExceedMaxUploadSize(file.size)) { if (file == null) return;
if (_isExceedMaxUploadSize(file.size)) {
if (context.mounted) { if (context.mounted) {
appToast.showToastErrorMessage( appToast.showToastErrorMessage(
context, context,
AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly( AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly(
IdentityCreatorConstants.maxKBSizeIdentityInlineImage maxSizeUploadByBytes
) )
); );
} else { } else {
@@ -613,8 +669,32 @@ class IdentityCreatorController extends BaseController {
} }
} }
Future<PlatformFile?> _compressFileAction(
BuildContext context,
{
required PlatformFile originalFile,
required int maxWidth
}
) async {
try {
isCompressingInlineImage.value = true;
final compressedFile = await _compressImage(originalFile, maxWidth);
isCompressingInlineImage.value = false;
return compressedFile;
} catch (e) {
logError("$runtimeType::_compressFileAction: compress image error: $e");
isCompressingInlineImage.value = false;
if (context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).cannotCompressInlineImage);
}
return null;
}
}
Future<PlatformFile> _compressImage(PlatformFile originalFile, int maxWidthCompressedImage) async { Future<PlatformFile> _compressImage(PlatformFile originalFile, int maxWidthCompressedImage) async {
if (originalFile.size <= IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes) { if (originalFile.size <= maxSizeUploadByBytes) {
return originalFile; return originalFile;
} }
@@ -701,4 +781,58 @@ class IdentityCreatorController extends BaseController {
} }
}); });
} }
void onLocalFileDropZoneListener({
required BuildContext context,
required DropDoneDetails details,
required double maxWidth
}) async {
try {
clearFocusEditor(context);
final listFileInfo = await onDragDone(context: context, details: details);
final listImages = listFileInfo.listInlineFiles;
if (listImages.isEmpty && listFileInfo.isNotEmpty && context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).canNotUploadFileToSignature
);
return;
}
final listCompressedImages = await Future.wait(
listImages.map((fileInfo) => _compressFileAction(
context,
originalFile: fileInfo.toPlatformFile(),
maxWidth: maxWidth.toInt()))
).then((listPlatformFiles) => listPlatformFiles.whereNotNull().toList());
if (_isExceedMaxUploadSize(listCompressedImages.totalFilesSize)) {
if (context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly(
maxSizeUploadByBytes
)
);
} else {
logError("IdentityCreatorController::onLocalFileDropZoneListener: context is unmounted");
}
return;
}
await Future.forEach(
listCompressedImages,
(platformFile) => _insertInlineImage(
context,
platformFile,
maxWidth.toInt(),
compressedFile: platformFile
)
);
} catch (e) {
logError("IdentityCreatorController::onLocalFileDropZoneListener: error: $e");
}
}
} }
@@ -17,14 +17,16 @@ import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.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/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/compress_image_loading_bar_widget.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_field_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_field_builder.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/insert_image_loading_indicator.dart';
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.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/state/get_all_identities_state.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -40,87 +42,126 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget bodyCreatorView = SingleChildScrollView( Widget bodyCreatorView = NotificationListener<ScrollNotification>(
controller: controller.scrollController, onNotification: (scrollInfo) {
physics: const ClampingScrollPhysics(), if (scrollInfo is ScrollEndNotification &&
child: Padding( scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24), controller.clearFocusEditor(context);
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ }
Obx(() => IdentityInputFieldBuilder( return false;
AppLocalizations.of(context).name, },
controller.errorNameIdentity.value, child: SingleChildScrollView(
AppLocalizations.of(context).required, controller: controller.scrollController,
editingController: controller.inputNameIdentityController, physics: const ClampingScrollPhysics(),
focusNode: controller.inputNameIdentityFocusNode, child: Padding(
isMandatory: true, padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24),
onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value) child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
)), Obx(() => IdentityInputFieldBuilder(
const SizedBox(height: 24), AppLocalizations.of(context).name,
Obx(() { controller.errorNameIdentity.value,
if (controller.actionType.value == IdentityActionType.create) { AppLocalizations.of(context).required,
return IdentityDropListFieldBuilder( editingController: controller.inputNameIdentityController,
controller.imagePaths, focusNode: controller.inputNameIdentityFocusNode,
AppLocalizations.of(context).email.inCaps, isMandatory: true,
controller.emailOfIdentity.value, onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value)
controller.listEmailAddressDefault, )),
onSelectItemDropList: (emailAddress) => controller.updateEmailOfIdentity(context, emailAddress) const SizedBox(height: 24),
); Obx(() {
} else { if (controller.actionType.value == IdentityActionType.create) {
return IdentityFieldNoEditableBuilder( return IdentityDropListFieldBuilder(
AppLocalizations.of(context).email.inCaps, controller.imagePaths,
controller.emailOfIdentity.value AppLocalizations.of(context).email.inCaps,
); controller.emailOfIdentity.value,
} controller.listEmailAddressDefault,
}), onSelectItemDropList: (emailAddress) => controller.updateEmailOfIdentity(context, emailAddress)
const SizedBox(height: 24), );
Obx(() => IdentityDropListFieldBuilder(
controller.imagePaths,
AppLocalizations.of(context).reply_to,
controller.replyToOfIdentity.value,
controller.listEmailAddressOfReplyTo,
onSelectItemDropList: (emailAddress) => controller.updaterReplyToOfIdentity(context, emailAddress)
)),
const SizedBox(height: 24),
Obx(() => IdentityInputWithDropListFieldBuilder(
AppLocalizations.of(context).bcc_to,
controller.errorBccIdentity.value,
controller.inputBccIdentityController,
focusNode: controller.inputBccIdentityFocusNode,
onSelectedSuggestionAction: (newEmailAddress) {
controller.inputBccIdentityController.text = newEmailAddress?.email ?? '';
controller.updateBccOfIdentity(newEmailAddress);
},
onChangeInputSuggestionAction: (pattern) {
controller.validateInputBccAddress(context, pattern);
if (pattern == null || pattern.trim().isEmpty) {
controller.updateBccOfIdentity(null);
} else { } else {
controller.updateBccOfIdentity(EmailAddress(null, pattern)); return IdentityFieldNoEditableBuilder(
AppLocalizations.of(context).email.inCaps,
controller.emailOfIdentity.value
);
} }
}, }),
onSuggestionCallbackAction: controller.getSuggestionEmailAddress const SizedBox(height: 24),
)), Obx(() => IdentityDropListFieldBuilder(
const SizedBox(height: 32), controller.imagePaths,
Text(AppLocalizations.of(context).signature, AppLocalizations.of(context).reply_to,
style: const TextStyle( controller.replyToOfIdentity.value,
fontWeight: FontWeight.normal, controller.listEmailAddressOfReplyTo,
fontSize: 14, onSelectItemDropList: (emailAddress) => controller.updaterReplyToOfIdentity(context, emailAddress)
color: AppColor.colorContentEmail, )),
const SizedBox(height: 24),
Obx(() => IdentityInputWithDropListFieldBuilder(
AppLocalizations.of(context).bcc_to,
controller.errorBccIdentity.value,
controller.inputBccIdentityController,
focusNode: controller.inputBccIdentityFocusNode,
onSelectedSuggestionAction: (newEmailAddress) {
controller.inputBccIdentityController.text = newEmailAddress?.email ?? '';
controller.updateBccOfIdentity(newEmailAddress);
},
onChangeInputSuggestionAction: (pattern) {
controller.validateInputBccAddress(context, pattern);
if (pattern == null || pattern.trim().isEmpty) {
controller.updateBccOfIdentity(null);
} else {
controller.updateBccOfIdentity(EmailAddress(null, pattern));
}
},
onSuggestionCallbackAction: controller.getSuggestionEmailAddress
)),
const SizedBox(height: 32),
Text(AppLocalizations.of(context).signature,
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
color: AppColor.colorContentEmail,
),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8), LayoutBuilder(
Container( builder: (context, constraintsEditor) {
decoration: BoxDecoration( return Stack(
borderRadius: BorderRadius.circular(12), children: [
border: Border.all(color: AppColor.colorInputBorderCreateMailbox), Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.colorInputBorderCreateMailbox),
),
padding: const EdgeInsetsDirectional.all(16),
child: _buildSignatureHtmlTemplate(context),
),
Obx(() {
if (controller.draggableAppState.value == DraggableAppState.inActive) {
return const SizedBox.shrink();
}
return Positioned.fill(
child: PointerInterceptor(
child: LocalFileDropZoneWidget(
imagePaths: controller.imagePaths,
width: constraintsEditor.maxWidth,
height: constraintsEditor.maxHeight,
margin: EdgeInsets.zero,
onLocalFileDropZoneListener: (details) =>
controller.onLocalFileDropZoneListener(
context: context,
details: details,
maxWidth: constraintsEditor.maxWidth,
),
)
),
);
})
],
);
}
), ),
padding: const EdgeInsetsDirectional.all(16), const SizedBox(height: 12),
child: _buildSignatureHtmlTemplate(context), if (controller.isMobile(context))
), _buildActionButtonMobile(context)
const SizedBox(height: 12), ]),
if (controller.isMobile(context)) ),
_buildActionButtonMobile(context)
]),
), ),
); );
@@ -400,13 +441,9 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
} }
Widget _buildSignatureHtmlTemplate(BuildContext context) { Widget _buildSignatureHtmlTemplate(BuildContext context) {
final htmlEditor = PlatformInfo.isWeb if (PlatformInfo.isWeb) {
? _buildHtmlEditorWeb(context, controller.contentHtmlEditor ?? '') return Column(
: _buildHtmlEditor(context, initialContent: controller.contentHtmlEditor ?? ''); children: [
return Column(
children: [
if (PlatformInfo.isWeb)
ToolbarRichTextWebBuilder( ToolbarRichTextWebBuilder(
richTextWebController: controller.richTextWebController!, richTextWebController: controller.richTextWebController!,
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
@@ -424,18 +461,34 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
), ),
] ]
), ),
Stack( Stack(
children: [ children: [
htmlEditor, _buildHtmlEditorWeb(
Obx(() => Center( context,
child: CompressImageLoadingBarWidget( controller.contentHtmlEditor),
isCompressing: controller.isCompressingInlineImage.value, Obx(() {
), bool isInserting = controller.publicAssetController?.isUploading.isTrue == true
)) || controller.isCompressingInlineImage.isTrue;
] return InsertImageLoadingIndicator(isInserting: isInserting);
), })
], ]
); ),
],
);
} else {
return Stack(
children: [
_buildHtmlEditor(
context,
initialContent: controller.contentHtmlEditor),
Obx(() {
bool isInserting = controller.publicAssetController?.isUploading.isTrue == true
|| controller.isCompressingInlineImage.isTrue;
return InsertImageLoadingIndicator(isInserting: isInserting);
})
]
);
}
} }
Widget _buildHtmlEditorWeb(BuildContext context, String initContent) { Widget _buildHtmlEditorWeb(BuildContext context, String initContent) {
@@ -447,6 +500,7 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
hint: '', hint: '',
darkMode: false, darkMode: false,
initialText: initContent.isEmpty ? null : initContent, initialText: initContent.isEmpty ? null : initContent,
disableDragAndDrop: true,
customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)), customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)),
), ),
htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions( htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions(
@@ -463,9 +517,11 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
} }
}, },
onInit: () { onInit: () {
controller.richTextWebController?.editorController.setOnDragDropEvent();
controller.richTextWebController?.editorController.setFullScreen(); controller.richTextWebController?.editorController.setFullScreen();
controller.updateContentHtmlEditor(initContent); controller.updateContentHtmlEditor(initContent);
}, onFocus: () { },
onFocus: () {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
controller.richTextWebController?.editorController.setFocus(); controller.richTextWebController?.editorController.setFocus();
@@ -473,7 +529,9 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
controller.richTextWebController?.closeAllMenuPopup(); controller.richTextWebController?.closeAllMenuPopup();
}, },
onChangeSelection: controller.richTextWebController?.onEditorSettingsChange, onChangeSelection: controller.richTextWebController?.onEditorSettingsChange,
onChangeCodeview: controller.updateContentHtmlEditor onChangeCodeview: controller.updateContentHtmlEditor,
onDragEnter: controller.handleOnDragEnterSignatureEditorWeb,
onDragLeave: (_) {}
), ),
); );
} }
@@ -1,8 +1,7 @@
class IdentityCreatorConstants { class IdentityCreatorConstants {
static const int maxKBSizeIdentityInlineImage = 128; // Kilobyte
static const double maxWidthInlineImageDesktop = 800; // Pixel static const double maxWidthInlineImageDesktop = 800; // Pixel
static const double maxWidthInlineImageOther = 700; // Pixel static const double maxWidthInlineImageOther = 700; // Pixel
static const String prefixCompressedInlineImageTemp = 'compressed_'; static const String prefixCompressedInlineImageTemp = 'compressed_';
static const int qualityCompressedInlineImage = 50; // 0 - 100 static const int qualityCompressedInlineImage = 80; // 0 - 100
} }
@@ -1,23 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/base/widget/circle_loading_widget.dart';
class CompressImageLoadingBarWidget extends StatelessWidget {
final bool isCompressing;
final EdgeInsetsGeometry? padding;
const CompressImageLoadingBarWidget({
super.key,
required this.isCompressing,
this.padding,
});
@override
Widget build(BuildContext context) {
if (isCompressing) {
return CircleLoadingWidget(padding: padding);
} else {
return const SizedBox.shrink();
}
}
}
@@ -0,0 +1,27 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:flutter/material.dart';
class InsertImageLoadingIndicator extends StatelessWidget {
final bool isInserting;
const InsertImageLoadingIndicator({
super.key,
required this.isInserting
});
@override
Widget build(BuildContext context) {
if (isInserting) {
return const Align(
alignment: Alignment.topCenter,
child: CircularProgressIndicator(
color: AppColor.primaryColor
)
);
} else {
return const SizedBox.shrink();
}
}
}
@@ -8,6 +8,9 @@ class CreatePublicAssetSuccessState extends UIState {
CreatePublicAssetSuccessState(this.publicAsset); CreatePublicAssetSuccessState(this.publicAsset);
final PublicAsset publicAsset; final PublicAsset publicAsset;
@override
List<Object?> get props => [publicAsset];
} }
class CreatePublicAssetFailureState extends FeatureFailure { class CreatePublicAssetFailureState extends FeatureFailure {
@@ -47,9 +47,8 @@ class PublicAssetController extends BaseController {
List<PublicAssetId> newlyPickedPublicAssetIds = []; List<PublicAssetId> newlyPickedPublicAssetIds = [];
final _publicAssetStreamGroup = StreamGroup<Either<Failure, Success>>(); final _publicAssetStreamGroup = StreamGroup<Either<Failure, Success>>();
StreamSubscription<Either<Failure, Success>>? _publicAssetStreamSubscription; StreamSubscription<Either<Failure, Success>>? _publicAssetStreamSubscription;
final isUploading = false.obs;
StreamSubscription<Either<Failure, Success>>? get publicAssetStreamSubscription
=> _publicAssetStreamSubscription;
Session? get session => arguments?.session; Session? get session => arguments?.session;
AccountId? get accountId => arguments?.accountId; AccountId? get accountId => arguments?.accountId;
Identity? get identity => arguments?.identity; Identity? get identity => arguments?.identity;
@@ -57,7 +56,7 @@ class PublicAssetController extends BaseController {
void _handleUploadState(Either<Failure, Success> uploadState) { void _handleUploadState(Either<Failure, Success> uploadState) {
log('PublicAssetController::handleUploadState::${uploadState.runtimeType}'); log('PublicAssetController::handleUploadState::${uploadState.runtimeType}');
uploadState.fold( uploadState.fold(
(failure) {}, (failure) => consumeState(Stream.value(Left(CreatePublicAssetFailureState()))),
(success) { (success) {
log('PublicAssetController::handleUploadState::success::$success'); log('PublicAssetController::handleUploadState::success::$success');
if (success is SuccessAttachmentUploadState) { if (success is SuccessAttachmentUploadState) {
@@ -71,7 +70,10 @@ class PublicAssetController extends BaseController {
final blobId = success.attachment.blobId; final blobId = success.attachment.blobId;
if (session == null if (session == null
|| accountId == null || accountId == null
|| blobId == null) return; || blobId == null) {
consumeState(Stream.value(Left(CreatePublicAssetFailureState())));
return;
}
consumeState(_createPublicAssetInteractor.execute( consumeState(_createPublicAssetInteractor.execute(
session!, session!,
@@ -108,17 +110,27 @@ class PublicAssetController extends BaseController {
} }
} }
@override
void handleFailureViewState(Failure failure) {
super.handleFailureViewState(failure);
if (failure is CreatePublicAssetFailureState) {
isUploading.value = false;
}
}
Future<void> _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async { Future<void> _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async {
await _publicAssetStreamGroup.add(success.uploadAttachment.progressState); await _publicAssetStreamGroup.add(success.uploadAttachment.progressState);
} }
void _handleCreatePublicAssetSuccessState(CreatePublicAssetSuccessState success) { void _handleCreatePublicAssetSuccessState(CreatePublicAssetSuccessState success) {
isUploading.value = false;
final publicAsset = success.publicAsset; final publicAsset = success.publicAsset;
if (publicAsset.id == null || publicAsset.publicURI == null) return; if (publicAsset.id == null || publicAsset.publicURI == null) return;
newlyPickedPublicAssetIds.add(publicAsset.id!); newlyPickedPublicAssetIds.add(publicAsset.id!);
final imageTag = '<img ' final imageTag = '<img '
'src="${publicAsset.publicURI!}" ' 'src="${publicAsset.publicURI!}" '
'style="max-width: 100%" '
'public-asset-id="${publicAsset.id!.value}">'; 'public-asset-id="${publicAsset.id!.value}">';
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
Get Get
@@ -140,6 +152,7 @@ class PublicAssetController extends BaseController {
} }
void uploadFileToBlob(PlatformFile platformFile) { void uploadFileToBlob(PlatformFile platformFile) {
isUploading.value = true;
_uploadFileToBlobAction(platformFile); _uploadFileToBlobAction(platformFile);
} }
@@ -1,4 +1,5 @@
import 'package:file_picker/file_picker.dart';
import 'package:model/upload/file_info.dart'; import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart'; import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart';
@@ -14,4 +15,12 @@ extension FileInfoExtension on FileInfo {
type: type, type: type,
isInline: true, isInline: true,
isShared: isShared); isShared: isShared);
PlatformFile toPlatformFile() => PlatformFile(
name: fileName,
path: filePath,
size: fileSize,
bytes: bytes,
readStream: readStream,
);
} }
@@ -1,7 +1,10 @@
import 'package:model/upload/file_info.dart'; import 'package:model/upload/file_info.dart';
extension ListFileInfoExtension on List<FileInfo> { extension ListFileInfoExtension on List<FileInfo> {
List<int> get listSize => map((file) => file.fileSize).toList(); List<int> get listSize => map((file) => file.fileSize).toList();
num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size); num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size);
List<FileInfo> get listInlineFiles => where((fileInfo) => fileInfo.isInline == true).toList();
} }
@@ -0,0 +1,6 @@
import 'package:file_picker/file_picker.dart';
extension ListPlatformFileExtensions on List<PlatformFile> {
int get totalFilesSize => fold<int>(0, (sum, file) => sum + file.size);
}
@@ -4180,4 +4180,11 @@ class AppLocalizations {
'Mark as spam failed', 'Mark as spam failed',
name: 'markAsSpamFailed'); name: 'markAsSpamFailed');
} }
String get canNotUploadFileToSignature {
return Intl.message(
'Can not upload this file to signature',
name: 'canNotUploadFileToSignature'
);
}
} }