From d072f6a243b67d6efaa8e7ae85f7ca9194aa0cc9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 12 Jul 2023 19:04:32 +0700 Subject: [PATCH] TF-1486 Add insert image in signature on web (cherry picked from commit 50f0e54b2a47fe2e857372f56293d1c1800ded2b) --- assets/images/ic_add_picture.svg | 5 ++ .../presentation/resources/image_paths.dart | 1 + .../controller/rich_text_web_controller.dart | 14 ++++++ .../widgets/toolbar_rich_text_builder.dart | 4 ++ .../extesions/size_extension.dart | 5 ++ .../identity_creator_controller.dart | 46 ++++++++++++++++++- .../presentation/identity_creator_view.dart | 25 +++++++--- .../utils/identity_creator_constants.dart | 4 ++ lib/l10n/intl_messages.arb | 12 ++++- lib/main/localizations/app_localizations.dart | 7 +++ 10 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 assets/images/ic_add_picture.svg create mode 100644 lib/features/identity_creator/presentation/extesions/size_extension.dart create mode 100644 lib/features/identity_creator/presentation/utils/identity_creator_constants.dart diff --git a/assets/images/ic_add_picture.svg b/assets/images/ic_add_picture.svg new file mode 100644 index 000000000..9db70fafc --- /dev/null +++ b/assets/images/ic_add_picture.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index a58751a4b..39bc83c9e 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -193,6 +193,7 @@ class ImagePaths { String get icArrowBottom => _getImagePath('ic_arrow_bottom.svg'); String get icArrowLeft => _getImagePath('ic_arrow_left.svg'); String get icArrowRight => _getImagePath('ic_arrow_right.svg'); + String get icAddPicture => _getImagePath('ic_add_picture.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/lib/features/composer/presentation/controller/rich_text_web_controller.dart b/lib/features/composer/presentation/controller/rich_text_web_controller.dart index ccf47f7df..b35b34c4b 100644 --- a/lib/features/composer/presentation/controller/rich_text_web_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_web_controller.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:core/presentation/extensions/color_extension.dart'; import 'package:custom_pop_up_menu/custom_pop_up_menu.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart'; @@ -257,6 +260,17 @@ class RichTextWebController extends BaseRichTextController { menuOrderListController.hideMenu(); } + void insertImageAsBase64({required PlatformFile platformFile}) { + if (platformFile.bytes != null) { + final base64Data = base64Encode(platformFile.bytes!); + editorController.insertHtml( + 'Image in my signature' + ); + } else { + logError("RichTextWebController::insertImageAsBase64: bytes is null"); + } + } + @override void onClose() { menuParagraphController.dispose(); diff --git a/lib/features/composer/presentation/widgets/toolbar_rich_text_builder.dart b/lib/features/composer/presentation/widgets/toolbar_rich_text_builder.dart index ebfc74875..a69618772 100644 --- a/lib/features/composer/presentation/widgets/toolbar_rich_text_builder.dart +++ b/lib/features/composer/presentation/widgets/toolbar_rich_text_builder.dart @@ -22,11 +22,13 @@ class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin final RichTextWebController richTextWebController; final ImagePaths _imagePaths = Get.find(); final EdgeInsetsGeometry? padding; + final List? extendedOption; ToolbarRichTextWebBuilder({ Key? key, required this.richTextWebController, this.padding, + this.extendedOption, }) : super(key: key); @override @@ -42,6 +44,8 @@ class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin crossAxisAlignment: WrapCrossAlignment.center, runSpacing: 8, children: [ + if (extendedOption?.isNotEmpty == true) + ...extendedOption!, AbsorbPointer( absorbing: codeViewEnabled, child: DropDownMenuHeaderStyleWidget( diff --git a/lib/features/identity_creator/presentation/extesions/size_extension.dart b/lib/features/identity_creator/presentation/extesions/size_extension.dart new file mode 100644 index 000000000..21c56f87d --- /dev/null +++ b/lib/features/identity_creator/presentation/extesions/size_extension.dart @@ -0,0 +1,5 @@ + +extension SizeExtension on int { + + int get kiloByteToBytes => this * 1024; +} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index 97876538c..2af63fcd4 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -1,9 +1,10 @@ - +import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -20,7 +21,9 @@ import 'package:model/user/user_profile.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/base/base_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/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'; import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart'; @@ -34,6 +37,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/extensions/id import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.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:uuid/uuid.dart'; @@ -44,6 +48,7 @@ class IdentityCreatorController extends BaseController { final IdentityUtils _identityUtils; final _uuid = Get.find(); + final _appToast = Get.find(); final noneEmailAddress = EmailAddress(null, 'None'); final listEmailAddressDefault = [].obs; @@ -302,12 +307,14 @@ class IdentityCreatorController extends BaseController { final error = _getErrorInputNameString(context); if (error?.isNotEmpty == true) { errorNameIdentity.value = error; + inputNameIdentityFocusNode.requestFocus(); return; } final errorBcc = _getErrorInputAddressString(context); if (errorBcc?.isNotEmpty == true) { errorBccIdentity.value = errorBcc; + inputBccIdentityFocusNode.requestFocus(); return; } @@ -456,4 +463,41 @@ class IdentityCreatorController extends BaseController { ); } } + + void pickImage(BuildContext context) async { + final filePickerResult = await FilePicker.platform.pickFiles( + type: FileType.image, + withData: true + ); + + if (context.mounted) { + final platformFile = filePickerResult?.files.single; + if (platformFile != null) { + _insertInlineImage(context, platformFile); + } else { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).cannotSelectThisImage + ); + } + } else { + logError("IdentityCreatorController::pickImage: context is unmounted"); + } + } + + bool _isExceedMaxSizeInlineImage(int fileSize) => + fileSize > IdentityCreatorConstants.maxSizeIdentityInlineImage.kiloByteToBytes; + + void _insertInlineImage(BuildContext context, PlatformFile platformFile) { + if (_isExceedMaxSizeInlineImage(platformFile.size)) { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly( + IdentityCreatorConstants.maxSizeIdentityInlineImage + ) + ); + } else { + richTextWebController.insertImageAsBase64(platformFile: platformFile); + } + } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_view.dart b/lib/features/identity_creator/presentation/identity_creator_view.dart index dae98848b..b0dda3957 100644 --- a/lib/features/identity_creator/presentation/identity_creator_view.dart +++ b/lib/features/identity_creator/presentation/identity_creator_view.dart @@ -8,7 +8,6 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/presentation/views/responsive/responsive_widget.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -18,6 +17,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; 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/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/identity_drop_list_field_builder.dart'; @@ -30,7 +30,8 @@ import 'package:tmail_ui_user/features/manage_account/presentation/model/identit import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; -class IdentityCreatorView extends GetWidget { +class IdentityCreatorView extends GetWidget + with RichTextButtonMixin { final _imagePaths = Get.find(); final _responsiveUtils = Get.find(); @@ -163,7 +164,7 @@ class IdentityCreatorView extends GetWidget { controller.errorNameIdentity.value, AppLocalizations.of(context).required, editingController: controller.inputNameIdentityController, - focusNode: PlatformInfo.isWeb ? null : controller.inputNameIdentityFocusNode, + focusNode: controller.inputNameIdentityFocusNode, isMandatory: true, onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value) )), @@ -195,7 +196,7 @@ class IdentityCreatorView extends GetWidget { AppLocalizations.of(context).bcc_to, controller.errorBccIdentity.value, controller.inputBccIdentityController, - focusNode: PlatformInfo.isWeb ? null : controller.inputBccIdentityFocusNode, + focusNode: controller.inputBccIdentityFocusNode, onSelectedSuggestionAction: (newEmailAddress) { controller.inputBccIdentityController.text = newEmailAddress?.email ?? ''; controller.updateBccOfIdentity(newEmailAddress); @@ -306,7 +307,20 @@ class IdentityCreatorView extends GetWidget { if (PlatformInfo.isWeb) ToolbarRichTextWebBuilder( richTextWebController: controller.richTextWebController, - padding: const EdgeInsets.only(bottom: 12) + padding: const EdgeInsets.only(bottom: 12), + extendedOption: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: buildWrapIconStyleText( + icon: buildIconWithTooltip( + path: _imagePaths.icAddPicture, + tooltip: AppLocalizations.of(context).insertImage + ), + hasDropdown: false, + onTap: () => controller.pickImage(context) + ), + ), + ] ), htmlEditor, ], @@ -314,7 +328,6 @@ class IdentityCreatorView extends GetWidget { } Widget _buildHtmlEditorWeb(BuildContext context, String initContent) { - log('IdentityCreatorView::_buildHtmlEditorWeb(): initContent: $initContent'); return html_editor_browser.HtmlEditor( key: const Key('identity_create_editor_web'), controller: controller.richTextWebController.editorController, diff --git a/lib/features/identity_creator/presentation/utils/identity_creator_constants.dart b/lib/features/identity_creator/presentation/utils/identity_creator_constants.dart new file mode 100644 index 000000000..8d7a6ccfb --- /dev/null +++ b/lib/features/identity_creator/presentation/utils/identity_creator_constants.dart @@ -0,0 +1,4 @@ + +class IdentityCreatorConstants { + static const int maxSizeIdentityInlineImage = 16; // Kilobyte +} \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 6bb4b653e..b5af62273 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-07-07T16:07:02.053606", + "@@last_modified": "2023-07-12T18:53:48.611802", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -2965,5 +2965,15 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "pleaseChooseAnImageSizeCorrectly": "Please choose an image size <= {maxSize}KB", + "@pleaseChooseAnImageSizeCorrectly": { + "type": "text", + "placeholders_order": [ + "maxSize" + ], + "placeholders": { + "maxSize": {} + } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index a1f5d7799..98215ff52 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3064,4 +3064,11 @@ class AppLocalizations { name: 'archiveMailboxDisplayName', ); } + + String pleaseChooseAnImageSizeCorrectly(int maxSize) { + return Intl.message( + 'Please choose an image size <= ${maxSize}KB', + name: 'pleaseChooseAnImageSizeCorrectly', + args: [maxSize]); + } } \ No newline at end of file