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/capitalize_extension.dart';
export 'presentation/extensions/list_extensions.dart';
export 'presentation/extensions/list_nullable_extensions.dart';
export 'domain/extensions/datetime_extension.dart';
export 'presentation/extensions/html_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) {
event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) {
if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
}
});
@@ -393,7 +393,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_subscriptionOnDragOver = html.window.onDragOver.listen((event) {
event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) {
if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
}
});
@@ -401,7 +401,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_subscriptionOnDragLeave = html.window.onDragLeave.listen((event) {
event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) {
if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
}
});
@@ -409,16 +409,12 @@ class ComposerController extends BaseController with DragDropFileMixin implement
_subscriptionOnDrop = html.window.onDrop.listen((event) {
event.preventDefault();
if (_validateFilesTransfer(event.dataTransfer.types)) {
if (event.dataTransfer.types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
}
});
}
bool _validateFilesTransfer(List<dynamic>? types) {
return types?.any((type) => type == 'Files') ?? false;
}
Future<void> _saveComposerCacheOnWebAction() async {
_autoCreateEmailTag();
@@ -2130,7 +2126,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
}
void handleOnDragEnterHtmlEditorWeb(List<dynamic>? types) {
if (_validateFilesTransfer(types)) {
if (types.validateFilesTransfer) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
}
}
@@ -14,6 +14,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
final ImagePaths imagePaths;
final double? width;
final double? height;
final EdgeInsetsGeometry margin;
final OnLocalFileDropZoneListener? onLocalFileDropZoneListener;
const LocalFileDropZoneWidget({
@@ -21,6 +22,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
required this.imagePaths,
this.width,
this.height,
this.margin = DropZoneWidgetStyle.margin,
this.onLocalFileDropZoneListener
});
@@ -32,7 +34,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
width: width,
height: height,
child: Padding(
padding: DropZoneWidgetStyle.margin,
padding: margin,
child: DottedBorder(
borderType: BorderType.RRect,
radius: const Radius.circular(DropZoneWidgetStyle.radius),
@@ -52,7 +54,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(imagePaths.icDropZoneIcon),
Flexible(child: SvgPicture.asset(imagePaths.icDropZoneIcon)),
const SizedBox(height: DropZoneWidgetStyle.space),
Text(
AppLocalizations.of(context).dropFileHereToAttachThem,
@@ -3,12 +3,15 @@ import 'dart:io';
import 'dart:math' as math;
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/success.dart';
import 'package:core/presentation/utils/keyboard_utils.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/file_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.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/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/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/utils/identity_creator_constants.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/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_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/edit_identity_request.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/public_asset_bindings.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/error/capability_validator.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/universal_import/html_stub.dart' as html hide File;
class IdentityCreatorController extends BaseController {
class IdentityCreatorController extends BaseController with DragDropFileMixin {
final VerifyNameInteractor _verifyNameInteractor;
final GetAllIdentitiesInteractor _getAllIdentitiesInteractor;
@@ -74,6 +82,7 @@ class IdentityCreatorController extends BaseController {
final isDefaultIdentity = RxBool(false);
final isDefaultIdentitySupported = RxBool(false);
final isCompressingInlineImage = RxBool(false);
final draggableAppState = DraggableAppState.inActive.obs;
final TextEditingController inputNameIdentityController = TextEditingController();
final TextEditingController inputBccIdentityController = TextEditingController();
@@ -83,6 +92,10 @@ class IdentityCreatorController extends BaseController {
RichTextMobileTabletController? richTextMobileTabletController;
RichTextWebController? richTextWebController;
StreamSubscription<html.Event>? _subscriptionOnDragEnter;
StreamSubscription<html.Event>? _subscriptionOnDragOver;
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
StreamSubscription<html.Event>? _subscriptionOnDrop;
String? _nameIdentity;
String? _contentHtmlEditor;
@@ -103,14 +116,23 @@ class IdentityCreatorController extends BaseController {
void updateContentHtmlEditor(String? text) => _contentHtmlEditor = text;
String? get contentHtmlEditor {
String get contentHtmlEditor {
if (_contentHtmlEditor != null) {
return _contentHtmlEditor;
return _contentHtmlEditor ?? '';
} 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(
this._verifyNameInteractor,
this._getAllIdentitiesInteractor,
@@ -138,10 +160,14 @@ class IdentityCreatorController extends BaseController {
session = arguments!.session;
identity = arguments!.identity;
actionType.value = arguments!.actionType;
if (actionType.value == IdentityActionType.create) {
isLoadSignatureCompleted = true;
}
_checkDefaultIdentityIsSupported();
_checkPublicAssetCapability();
_setUpValueFromIdentity();
_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
void onClose() {
log('IdentityCreatorController::onClose():');
@@ -170,6 +236,10 @@ class IdentityCreatorController extends BaseController {
}
Get.delete<PublicAssetController>(tag: BindingTag.publicAssetBindingsTag);
publicAssetController = null;
_subscriptionOnDragEnter?.cancel();
_subscriptionOnDragOver?.cancel();
_subscriptionOnDragLeave?.cancel();
_subscriptionOnDrop?.cancel();
super.onClose();
}
@@ -177,7 +247,7 @@ class IdentityCreatorController extends BaseController {
void handleSuccessViewState(Success success) {
super.handleSuccessViewState(success);
if (success is GetAllIdentitiesSuccess) {
_getALlIdentitiesSuccess(success);
_getAllIdentitiesSuccess(success);
}
}
@@ -185,7 +255,7 @@ class IdentityCreatorController extends BaseController {
void handleFailureViewState(Failure failure) {
super.handleFailureViewState(failure);
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) {
listEmailAddressDefault.value = success.identities!
.map((identity) => identity.toEmailAddressNoName())
@@ -236,7 +306,7 @@ class IdentityCreatorController extends BaseController {
}
}
void _getALlIdentitiesFailure(GetAllIdentitiesFailure failure) {
void _getAllIdentitiesFailure(GetAllIdentitiesFailure failure) {
_setDefaultEmailAddressList();
}
@@ -339,11 +409,11 @@ class IdentityCreatorController extends BaseController {
bccOfIdentity.value = newEmailAddress;
}
Future<String?> _getSignatureHtmlText() async {
Future<String> _getSignatureHtmlText() async {
if (PlatformInfo.isWeb) {
return richTextWebController?.editorController.getText();
return (await richTextWebController?.editorController.getText()) ?? '';
} else {
return richTextMobileTabletController?.richTextController.htmlEditorApi?.getText();
return (await richTextMobileTabletController?.richTextController.htmlEditorApi?.getText()) ?? '';
}
}
@@ -380,7 +450,7 @@ class IdentityCreatorController extends BaseController {
: null;
final publicAssetsInIdentityArguments = PublicAssetsInIdentityArguments(
htmlSignature: signatureHtmlText ?? '',
htmlSignature: signatureHtmlText,
preExistingPublicAssetIds: List.from(publicAssetController?.preExistingPublicAssetIds ?? []),
newlyPickedPublicAssetIds: List.from(publicAssetController?.newlyPickedPublicAssetIds ?? []),
);
@@ -390,7 +460,7 @@ class IdentityCreatorController extends BaseController {
email: emailOfIdentity.value?.email,
replyTo: replyToAddress,
bcc: bccAddress,
htmlSignature: Signature(signatureHtmlText ?? ''),
htmlSignature: Signature(signatureHtmlText),
sortOrder: sortOrder);
final generateCreateId = Id(uuid.v1());
@@ -470,6 +540,8 @@ class IdentityCreatorController extends BaseController {
}
void clearFocusEditor(BuildContext context) {
inputNameIdentityFocusNode.unfocus();
inputBccIdentityFocusNode.unfocus();
if (PlatformInfo.isMobile) {
richTextMobileTabletController?.richTextController.htmlEditorApi?.unfocus();
}
@@ -549,47 +621,31 @@ class IdentityCreatorController extends BaseController {
logError("IdentityCreatorController::pickImage: context is unmounted");
}
}
bool _isExceedMaxSizeInlineImage(int fileSize) =>
fileSize > IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes;
bool _isExceedMaxUploadSize(int fileSize) {
final coreCapability = session?.getCapabilityProperties<CoreCapability>(
accountId!,
CapabilityIdentifier.jmapCore
);
int maxUploadSize = coreCapability?.maxSizeUpload?.value.toInt() ?? 0;
return fileSize > maxUploadSize;
return fileSize > maxSizeUploadByBytes;
}
void _insertInlineImage(
Future<void> _insertInlineImage(
BuildContext context,
PlatformFile platformFile,
int maxWidth
int maxWidth,
{PlatformFile? compressedFile}
) async {
final PlatformFile file;
try {
isCompressingInlineImage.value = true;
file = await _compressImage(platformFile, maxWidth);
isCompressingInlineImage.value = false;
} catch (e) {
logError("IdentityCreatorController::_insertInlineImage: compress image error: $e");
isCompressingInlineImage.value = false;
if (context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).cannotCompressInlineImage);
}
return;
final PlatformFile? file;
if (compressedFile != null) {
file = compressedFile;
} else {
file = await _compressFileAction(context, originalFile: platformFile, maxWidth: maxWidth);
}
if (_isExceedMaxSizeInlineImage(file.size) || _isExceedMaxUploadSize(file.size)) {
if (file == null) return;
if (_isExceedMaxUploadSize(file.size)) {
if (context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly(
IdentityCreatorConstants.maxKBSizeIdentityInlineImage
maxSizeUploadByBytes
)
);
} 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 {
if (originalFile.size <= IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes) {
if (originalFile.size <= maxSizeUploadByBytes) {
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/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/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/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_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_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/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/presentation/model/identity_action_type.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -40,87 +42,126 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
@override
Widget build(BuildContext context) {
Widget bodyCreatorView = SingleChildScrollView(
controller: controller.scrollController,
physics: const ClampingScrollPhysics(),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Obx(() => IdentityInputFieldBuilder(
AppLocalizations.of(context).name,
controller.errorNameIdentity.value,
AppLocalizations.of(context).required,
editingController: controller.inputNameIdentityController,
focusNode: controller.inputNameIdentityFocusNode,
isMandatory: true,
onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value)
)),
const SizedBox(height: 24),
Obx(() {
if (controller.actionType.value == IdentityActionType.create) {
return IdentityDropListFieldBuilder(
controller.imagePaths,
AppLocalizations.of(context).email.inCaps,
controller.emailOfIdentity.value,
controller.listEmailAddressDefault,
onSelectItemDropList: (emailAddress) => controller.updateEmailOfIdentity(context, emailAddress)
);
} else {
return IdentityFieldNoEditableBuilder(
AppLocalizations.of(context).email.inCaps,
controller.emailOfIdentity.value
);
}
}),
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);
Widget bodyCreatorView = NotificationListener<ScrollNotification>(
onNotification: (scrollInfo) {
if (scrollInfo is ScrollEndNotification &&
scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
controller.clearFocusEditor(context);
}
return false;
},
child: SingleChildScrollView(
controller: controller.scrollController,
physics: const ClampingScrollPhysics(),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Obx(() => IdentityInputFieldBuilder(
AppLocalizations.of(context).name,
controller.errorNameIdentity.value,
AppLocalizations.of(context).required,
editingController: controller.inputNameIdentityController,
focusNode: controller.inputNameIdentityFocusNode,
isMandatory: true,
onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value)
)),
const SizedBox(height: 24),
Obx(() {
if (controller.actionType.value == IdentityActionType.create) {
return IdentityDropListFieldBuilder(
controller.imagePaths,
AppLocalizations.of(context).email.inCaps,
controller.emailOfIdentity.value,
controller.listEmailAddressDefault,
onSelectItemDropList: (emailAddress) => controller.updateEmailOfIdentity(context, emailAddress)
);
} else {
controller.updateBccOfIdentity(EmailAddress(null, pattern));
return IdentityFieldNoEditableBuilder(
AppLocalizations.of(context).email.inCaps,
controller.emailOfIdentity.value
);
}
},
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: 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 {
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),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.colorInputBorderCreateMailbox),
const SizedBox(height: 8),
LayoutBuilder(
builder: (context, constraintsEditor) {
return Stack(
children: [
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),
child: _buildSignatureHtmlTemplate(context),
),
const SizedBox(height: 12),
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) {
final htmlEditor = PlatformInfo.isWeb
? _buildHtmlEditorWeb(context, controller.contentHtmlEditor ?? '')
: _buildHtmlEditor(context, initialContent: controller.contentHtmlEditor ?? '');
return Column(
children: [
if (PlatformInfo.isWeb)
if (PlatformInfo.isWeb) {
return Column(
children: [
ToolbarRichTextWebBuilder(
richTextWebController: controller.richTextWebController!,
padding: const EdgeInsets.only(bottom: 12),
@@ -424,18 +461,34 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
),
]
),
Stack(
children: [
htmlEditor,
Obx(() => Center(
child: CompressImageLoadingBarWidget(
isCompressing: controller.isCompressingInlineImage.value,
),
))
]
),
],
);
Stack(
children: [
_buildHtmlEditorWeb(
context,
controller.contentHtmlEditor),
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) {
@@ -447,6 +500,7 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
hint: '',
darkMode: false,
initialText: initContent.isEmpty ? null : initContent,
disableDragAndDrop: true,
customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)),
),
htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions(
@@ -463,9 +517,11 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
}
},
onInit: () {
controller.richTextWebController?.editorController.setOnDragDropEvent();
controller.richTextWebController?.editorController.setFullScreen();
controller.updateContentHtmlEditor(initContent);
}, onFocus: () {
},
onFocus: () {
FocusManager.instance.primaryFocus?.unfocus();
Future.delayed(const Duration(milliseconds: 500), () {
controller.richTextWebController?.editorController.setFocus();
@@ -473,7 +529,9 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
controller.richTextWebController?.closeAllMenuPopup();
},
onChangeSelection: controller.richTextWebController?.onEditorSettingsChange,
onChangeCodeview: controller.updateContentHtmlEditor
onChangeCodeview: controller.updateContentHtmlEditor,
onDragEnter: controller.handleOnDragEnterSignatureEditorWeb,
onDragLeave: (_) {}
),
);
}
@@ -1,8 +1,7 @@
class IdentityCreatorConstants {
static const int maxKBSizeIdentityInlineImage = 128; // Kilobyte
static const double maxWidthInlineImageDesktop = 800; // Pixel
static const double maxWidthInlineImageOther = 700; // Pixel
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);
final PublicAsset publicAsset;
@override
List<Object?> get props => [publicAsset];
}
class CreatePublicAssetFailureState extends FeatureFailure {
@@ -47,9 +47,8 @@ class PublicAssetController extends BaseController {
List<PublicAssetId> newlyPickedPublicAssetIds = [];
final _publicAssetStreamGroup = StreamGroup<Either<Failure, Success>>();
StreamSubscription<Either<Failure, Success>>? _publicAssetStreamSubscription;
final isUploading = false.obs;
StreamSubscription<Either<Failure, Success>>? get publicAssetStreamSubscription
=> _publicAssetStreamSubscription;
Session? get session => arguments?.session;
AccountId? get accountId => arguments?.accountId;
Identity? get identity => arguments?.identity;
@@ -57,7 +56,7 @@ class PublicAssetController extends BaseController {
void _handleUploadState(Either<Failure, Success> uploadState) {
log('PublicAssetController::handleUploadState::${uploadState.runtimeType}');
uploadState.fold(
(failure) {},
(failure) => consumeState(Stream.value(Left(CreatePublicAssetFailureState()))),
(success) {
log('PublicAssetController::handleUploadState::success::$success');
if (success is SuccessAttachmentUploadState) {
@@ -71,7 +70,10 @@ class PublicAssetController extends BaseController {
final blobId = success.attachment.blobId;
if (session == null
|| accountId == null
|| blobId == null) return;
|| blobId == null) {
consumeState(Stream.value(Left(CreatePublicAssetFailureState())));
return;
}
consumeState(_createPublicAssetInteractor.execute(
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 {
await _publicAssetStreamGroup.add(success.uploadAttachment.progressState);
}
void _handleCreatePublicAssetSuccessState(CreatePublicAssetSuccessState success) {
isUploading.value = false;
final publicAsset = success.publicAsset;
if (publicAsset.id == null || publicAsset.publicURI == null) return;
newlyPickedPublicAssetIds.add(publicAsset.id!);
final imageTag = '<img '
'src="${publicAsset.publicURI!}" '
'style="max-width: 100%" '
'public-asset-id="${publicAsset.id!.value}">';
if (PlatformInfo.isWeb) {
Get
@@ -140,6 +152,7 @@ class PublicAssetController extends BaseController {
}
void uploadFileToBlob(PlatformFile platformFile) {
isUploading.value = true;
_uploadFileToBlobAction(platformFile);
}
@@ -1,4 +1,5 @@
import 'package:file_picker/file_picker.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart';
@@ -14,4 +15,12 @@ extension FileInfoExtension on FileInfo {
type: type,
isInline: true,
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';
extension ListFileInfoExtension on List<FileInfo> {
List<int> get listSize => map((file) => file.fileSize).toList();
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',
name: 'markAsSpamFailed');
}
String get canNotUploadFileToSignature {
return Intl.message(
'Can not upload this file to signature',
name: 'canNotUploadFileToSignature'
);
}
}