From e6f1ced4cd67b0fa9e22eae225758d9568e5ef6c Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 20 Jul 2022 16:51:16 +0700 Subject: [PATCH] TF-725 Implement UI/UX for insert image to composer on web --- assets/images/ic_insert_image.svg | 3 + .../presentation/resources/image_paths.dart | 1 + .../views/button/icon_button_web.dart | 34 +++++ lib/features/base/mixin/app_loader_mixin.dart | 8 ++ .../presentation/composer_bindings.dart | 11 +- .../presentation/composer_controller.dart | 117 +++++++++++++++++- .../composer/presentation/composer_view.dart | 18 --- .../presentation/composer_view_web.dart | 61 +++++---- lib/l10n/intl_messages.arb | 44 ++++++- lib/main/localizations/app_localizations.dart | 45 +++++++ .../lib/extensions/attachment_extension.dart | 22 +++- model/lib/upload/file_info.dart | 2 + pubspec.yaml | 3 + 13 files changed, 321 insertions(+), 48 deletions(-) create mode 100644 assets/images/ic_insert_image.svg diff --git a/assets/images/ic_insert_image.svg b/assets/images/ic_insert_image.svg new file mode 100644 index 000000000..07da9c41b --- /dev/null +++ b/assets/images/ic_insert_image.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 6eb5c819a..449201dde 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -131,6 +131,7 @@ class ImagePaths { String get icStyleBold => _getImagePath('ic_style_bold.svg'); String get icStyleItalic => _getImagePath('ic_style_italic.svg'); String get icStyleUnderline => _getImagePath('ic_style_underline.svg'); + String get icInsertImage => _getImagePath('ic_insert_image.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/views/button/icon_button_web.dart b/core/lib/presentation/views/button/icon_button_web.dart index d476ccf36..e5d8c8e2e 100644 --- a/core/lib/presentation/views/button/icon_button_web.dart +++ b/core/lib/presentation/views/button/icon_button_web.dart @@ -125,3 +125,37 @@ Widget buildTextButton(String text, { ), ); } + +Widget buildButtonWrapText(String name, { + TextStyle? textStyle, + Color? bgColor, + double? radius, + double? height, + EdgeInsets? padding, + IconWebCallback? onTap +}) { + return Container( + height: height ?? 40, + padding: padding, + child: ElevatedButton( + onPressed: () => onTap?.call(), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith( + (Set states) => bgColor ?? AppColor.colorTextButton), + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius ?? 8), + side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton))), + padding: MaterialStateProperty.resolveWith( + (Set states) => const EdgeInsets.symmetric(horizontal: 16)), + elevation: MaterialStateProperty.resolveWith( + (Set states) => 0)), + child: Text(name, + textAlign: TextAlign.center, + style: textStyle ?? + const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: Colors.white)), + ), + ); +} \ No newline at end of file diff --git a/lib/features/base/mixin/app_loader_mixin.dart b/lib/features/base/mixin/app_loader_mixin.dart index ff0f5225c..220ad24c0 100644 --- a/lib/features/base/mixin/app_loader_mixin.dart +++ b/lib/features/base/mixin/app_loader_mixin.dart @@ -31,4 +31,12 @@ mixin AppLoaderMixin { progressColor: AppColor.primaryColor, )); } + + Widget loadingWidgetWithSizeColor({double? size, Color? color}) { + return Center(child: SizedBox( + width: size ?? 24, + height: size ?? 24, + child: CircularProgressIndicator( + color: color ?? AppColor.colorLoading))); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index b6382b5c7..19d04e586 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -3,7 +3,9 @@ import 'package:core/core.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/contact_datasource.dart'; +import 'package:tmail_ui_user/features/composer/data/datasource_impl/composer_datasource_impl.dart'; import 'package:tmail_ui_user/features/composer/data/datasource_impl/contact_datasource_impl.dart'; import 'package:tmail_ui_user/features/composer/data/repository/auto_complete_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/data/repository/composer_repository_impl.dart'; @@ -11,6 +13,7 @@ import 'package:tmail_ui_user/features/composer/data/repository/contact_reposito import 'package:tmail_ui_user/features/composer/domain/repository/auto_complete_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_with_device_contact_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; @@ -74,6 +77,7 @@ class ComposerBindings extends BaseBindings { } Get.lazyPut(() => AttachmentUploadDataSourceImpl(Get.find())); + Get.lazyPut(() => ComposerDataSourceImpl(Get.find())); Get.lazyPut(() => ContactDataSourceImpl()); Get.lazyPut(() => EmailDataSourceImpl(Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( @@ -88,6 +92,7 @@ class ComposerBindings extends BaseBindings { @override void bindingsDataSource() { Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); @@ -96,7 +101,9 @@ class ComposerBindings extends BaseBindings { @override void bindingsRepositoryImpl() { - Get.lazyPut(() => ComposerRepositoryImpl(Get.find())); + Get.lazyPut(() => ComposerRepositoryImpl( + Get.find(), + Get.find())); Get.lazyPut(() => AutoCompleteRepositoryImpl(dataSources)); Get.lazyPut(() => ContactRepositoryImpl(Get.find())); Get.lazyPut(() => EmailRepositoryImpl( @@ -132,6 +139,7 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => GetAllIdentitiesInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); + Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); } @override @@ -153,6 +161,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index a78e91068..adec96dc7 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -27,7 +27,9 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_with_device_contact_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; @@ -36,7 +38,10 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_dra 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_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/insert_image_dialog_builder.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -46,6 +51,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; @@ -90,6 +96,7 @@ class ComposerController extends BaseController { final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor; final RichTextWebController richTextWebController; + final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; List listToEmailAddress = []; List listCcEmailAddress = []; @@ -104,6 +111,8 @@ class ComposerController extends BaseController { List initialAttachments = []; String? _textEditorWeb; List? _emailContents; + double? maxWithEditor; + late Worker uploadInlineImageWorker; void setTextEditorWeb(String? text) => _textEditorWeb = text; @@ -159,11 +168,13 @@ class ComposerController extends BaseController { this._removeComposerCacheOnWebInteractor, this._saveComposerCacheOnWebInteractor, this.richTextWebController, + this._downloadImageAsBase64Interactor, ); @override void onInit() { super.onInit(); + _listenWorker(); if (!BuildUtils.isWeb) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await FkUserAgent.init(); @@ -200,6 +211,7 @@ class ComposerController extends BaseController { toEmailAddressController.dispose(); ccEmailAddressController.dispose(); bccEmailAddressController.dispose(); + uploadInlineImageWorker.dispose(); super.dispose(); } @@ -228,13 +240,22 @@ class ComposerController extends BaseController { _pickFileSuccess(success); } else if (success is GetEmailContentSuccess) { _getEmailContentSuccess(success); - } if (success is GetAllIdentitiesSuccess) { + } else if (success is GetAllIdentitiesSuccess) { if (success.identities?.isNotEmpty == true) { listIdentities.value = success.identities! .where((identity) => identity.mayDelete == true) .toList(); selectIdentity(listIdentities.first); } + } else if (success is DownloadImageAsBase64Success) { + richTextWebController.insertImage( + InlineImage( + ImageSource.local, + fileInfo: success.fileInfo, + cid: success.cid, + base64: success.imageAsBase64), + maxWithEditor: maxWithEditor); + maxWithEditor = null; } }); } @@ -251,6 +272,19 @@ class ComposerController extends BaseController { popBack(); } + void _listenWorker() { + uploadInlineImageWorker = ever(uploadController.uploadInlineViewState, (state) { + log('ComposerController::_listenWorker(): $state'); + if (state is Either) { + state.fold((failure) => null, (success) { + if (success is SuccessAttachmentUploadState) { + _handleUploadInlineSuccess(success); + } + }); + } + }); + } + void _listenBrowserTabRefresh() { html.window.onBeforeUnload.listen((event) async { final userProfile = mailboxDashBoardController.userProfile.value; @@ -497,7 +531,21 @@ class ComposerController extends BaseController { final generateBlobId = Id(_uuid.v1()); var emailBodyText = await _getEmailBodyText(context); - log('ComposerController::_generateEmail(): $emailBodyText'); + final mapContents = await richTextWebController.refactorContentHasInlineImage( + emailBodyText, + uploadController.mapInlineAttachments); + log('ComposerController::_generateEmail(): mapContents: $mapContents'); + emailBodyText = mapContents.value1; + final listInlineAttachment = mapContents.value2; + final listInlineEmailBodyPart = listInlineAttachment + .map((attachment) => attachment.toEmailBodyPart(charset: 'base64')) + .toSet(); + + final attachments = {}; + attachments.addAll(uploadController.generateAttachments() ?? []); + attachments.addAll(listInlineEmailBodyPart); + log('ComposerController::_generateEmail(): listInlineEmailBodyPart: $listInlineEmailBodyPart'); + final userAgent = await userAgentPlatform; log('ComposerController::_generateEmail(): userAgent: $userAgent'); @@ -521,7 +569,7 @@ class ComposerController extends BaseController { generatePartId: EmailBodyValue(emailBodyText, false, false) }, headerUserAgent: {IndividualHeaderIdentifier.headerUserAgent : userAgent}, - attachments: uploadController.generateAttachments(), + attachments: attachments.isNotEmpty ? attachments : null, ); } @@ -623,7 +671,7 @@ class ComposerController extends BaseController { final email = await _generateEmail(context, mapDefaultMailboxId, userProfile); final accountId = session.accounts.keys.first; final sentMailboxId = mapDefaultMailboxId[PresentationMailbox.roleSent]; - final submissionCreateId = Id(const Uuid().v1()); + final submissionCreateId = Id(_uuid.v1()); mailboxDashBoardController.consumeState(_sendEmailInteractor.execute( accountId, @@ -631,6 +679,8 @@ class ComposerController extends BaseController { emailIdDestroyed: arguments.emailActionType == EmailActionType.edit ? arguments.presentationEmail?.id : null))); + + uploadController.clearInlineFileUploaded(); } if (kIsWeb) { @@ -1055,6 +1105,65 @@ class ComposerController extends BaseController { } } + void insertImage(BuildContext context, {double? maxWithEditor}) async { + await InsertImageDialogBuilder( + context, + insertActionCallback: (inlineImage) { + log('ComposerController::insertImage(): ${inlineImage.source}|$maxWithEditor'); + if (BuildUtils.isWeb) { + this.maxWithEditor = maxWithEditor; + _insertImageOnWeb(inlineImage); + } + } + ).show(); + } + + void _insertImageOnWeb(InlineImage inlineImage) { + if (inlineImage.source == ImageSource.local) { + _uploadInlineAttachmentsAction(inlineImage.fileInfo!); + } else { + richTextWebController.insertImage(inlineImage); + } + } + + void _uploadInlineAttachmentsAction(FileInfo pickedFile) async { + if (uploadController.hasEnoughMaxAttachmentSize(listFiles: [pickedFile])) { + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + if (session != null && accountId != null) { + final uploadUri = session.getUploadUri(accountId); + uploadController.uploadInlineImage(pickedFile, uploadUri); + } + } else { + if (currentContext != null) { + showConfirmDialogAction( + currentContext!, + AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), + AppLocalizations.of(currentContext!).got_it, + () => {}, + title: AppLocalizations.of(currentContext!).maximum_files_size, + hasCancelButton: false); + } + } + } + + void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { + uploadController.clearUploadInlineViewState(); + + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(); + final accountId = mailboxDashBoardController.accountId.value; + + if (baseDownloadUrl != null && accountId != null) { + final imageUrl = uploadState.attachment.getDownloadUrl(baseDownloadUrl, accountId); + log('ComposerController::_handleUploadInlineSuccess(): imageUrl: $imageUrl'); + consumeState(_downloadImageAsBase64Interactor.execute( + imageUrl, + uploadState.attachment.cid!, + uploadState.fileInfo)); + } + } + void closeComposer() { FocusManager.instance.primaryFocus?.unfocus(); popBack(); diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 58a2b230f..a28e6f71e 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -3,7 +3,6 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:enough_html_editor/enough_html_editor.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -11,7 +10,6 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/rich_text_style_type.dart'; @@ -412,7 +410,6 @@ class ComposerView extends GetWidget with AppLoaderMixin, Ri child: _buildAttachmentsTitle(context, uploadAttachments, controller.expandModeAttachments.value)), - _buildAttachmentsLoadingView(), Padding( padding: const EdgeInsets.only(bottom: 8, left: 16, right: 16), child: _buildAttachmentsList(context, @@ -479,7 +476,6 @@ class ComposerView extends GetWidget with AppLoaderMixin, Ri child: _buildAttachmentsTitle(context, uploadAttachments, controller.expandModeAttachments.value)), - _buildAttachmentsLoadingView(), Padding( padding: const EdgeInsets.only(bottom: 8, left: 16, right: 16), child: _buildAttachmentsList(context, @@ -589,19 +585,6 @@ class ComposerView extends GetWidget with AppLoaderMixin, Ri }); } - Widget _buildAttachmentsLoadingView({EdgeInsets? padding, double? size}) { - return Obx(() => controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) => success is UploadingAttachmentState - ? Center(child: Padding( - padding: padding ?? const EdgeInsets.all(10), - child: SizedBox( - width: size ?? 20, - height: size ?? 20, - child: const CupertinoActivityIndicator(color: AppColor.colorTextButton)))) - : const SizedBox.shrink())); - } - Widget _buildAttachmentsTitle( BuildContext context, List uploadFilesState, @@ -612,7 +595,6 @@ class ComposerView extends GetWidget with AppLoaderMixin, Ri Text( '${AppLocalizations.of(context).attachments} (${filesize(uploadFilesState.totalSize, 0)}):', style: const TextStyle(fontSize: 12, color: AppColor.colorHintEmailAddressInput, fontWeight: FontWeight.normal)), - _buildAttachmentsLoadingView(padding: const EdgeInsets.only(left: 16), size: 16), const Spacer(), Material( type: MaterialType.circle, diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index ca94557ca..eb600799e 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -2,7 +2,6 @@ import 'package:core/core.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:file_picker/file_picker.dart'; import 'package:filesize/filesize.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -11,13 +10,14 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:model/model.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/rich_text_style_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_file_composer_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_input_builder.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; @@ -54,14 +54,19 @@ class ComposerView extends GetWidget : const SizedBox.shrink()), _buildEmailAddress(context, constraints), const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: _buildSubjectEmail(context)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildSubjectEmail(context)), const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: _buildListButton(context)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildListButton(context, constraints)), const Divider(color: AppColor.colorDividerComposer, height: 1), Expanded(child: Column( children: [ _buildAttachmentsWidget(context), _buildToolbarRichTextWidget(), + _buildInlineLoadingView(), _buildEditorForm(context) ] )), @@ -282,7 +287,7 @@ class ComposerView extends GetWidget const Divider(color: AppColor.colorDividerComposer, height: 1), Padding(padding: const EdgeInsets.only(right: 16), child: _buildSubjectEmail(context)), const Divider(color: AppColor.colorDividerComposer, height: 1), - _buildListButton(context), + _buildListButton(context, constraints), ]), )) ])), @@ -295,6 +300,7 @@ class ComposerView extends GetWidget children: [ _buildAttachmentsWidget(context), _buildToolbarRichTextWidget(), + _buildInlineLoadingView(), _buildEditorForm(context) ] ))), @@ -483,7 +489,7 @@ class ComposerView extends GetWidget ); } - Widget _buildListButton(BuildContext context) { + Widget _buildListButton(BuildContext context, BoxConstraints constraints) { return Transform( transform: Matrix4.translationValues(-5.0, 0.0, 0.0), child: Padding( @@ -499,6 +505,17 @@ class ComposerView extends GetWidget fit: BoxFit.fill), tooltip: AppLocalizations.of(context).attach_file, onTap: () => controller.openFilePickerByType(context, FileType.any)), + const SizedBox(width: 4), + buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset(imagePaths.icInsertImage, + color: AppColor.colorTextButton, + fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).insertImage, + onTap: () => controller.insertImage( + context, + maxWithEditor: constraints.maxWidth - 120)), ]) ) ); @@ -604,7 +621,6 @@ class ComposerView extends GetWidget ) ) ); - // }); } Widget _buildAttachmentsWidget(BuildContext context) { @@ -622,7 +638,6 @@ class ComposerView extends GetWidget context, attachments, controller.expandModeAttachments.value)), - _buildAttachmentsLoadingView(), Padding( padding: EdgeInsets.only( bottom: 8, @@ -639,19 +654,6 @@ class ComposerView extends GetWidget }); } - Widget _buildAttachmentsLoadingView({EdgeInsets? padding, double? size}) { - return Obx(() => controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) => success is UploadingAttachmentState - ? Center(child: Padding( - padding: padding ?? const EdgeInsets.all(10), - child: SizedBox( - width: size ?? 20, - height: size ?? 20, - child: const CupertinoActivityIndicator(color: AppColor.colorTextButton)))) - : const SizedBox.shrink())); - } - Widget _buildAttachmentsTitle( BuildContext context, List uploadFilesState, @@ -661,7 +663,6 @@ class ComposerView extends GetWidget Text( '${AppLocalizations.of(context).attachments} (${filesize(uploadFilesState.totalSize, 0)}):', style: const TextStyle(fontSize: 12, color: AppColor.colorHintEmailAddressInput, fontWeight: FontWeight.normal)), - _buildAttachmentsLoadingView(padding: const EdgeInsets.only(left: 16), size: 16), const Spacer(), Material( type: MaterialType.circle, @@ -746,4 +747,20 @@ class ComposerView extends GetWidget ), ); } + + Widget _buildInlineLoadingView() { + return Obx(() => controller.uploadController.uploadInlineViewState.value.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is UploadingAttachmentUploadState || + success is DownloadingImageAsBase64) { + return Padding( + padding: const EdgeInsets.all(5), + child: loadingWidgetWithSizeColor( + size: 30, + color: AppColor.primaryColor)); + } + return const SizedBox.shrink(); + })); + } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index b7ee47594..c2a2645de 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-07-11T12:17:57.489417", + "@@last_modified": "2022-07-18T16:31:40.598846", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -1573,5 +1573,47 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "insertImage": "Insert image", + "@insertImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectFromFile": "Select from file", + "@selectFromFile": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "chooseImage": "Choose image", + "@chooseImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "urlLink": "URL", + "@urlLink": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insert": "Insert", + "@insert": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insertImageErrorFileEmpty": "Please either choose an image or enter an image URL", + "@insertImageErrorFileEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insertImageErrorDuplicate": "Please input either an image or an image URL, not both", + "@insertImageErrorDuplicate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index df9d5d5e3..bac8f55a5 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1614,4 +1614,49 @@ class AppLocalizations { 'Save & close', name: 'saveAndClose'); } + + String get insertImage { + return Intl.message( + 'Insert image', + name: 'insertImage'); + } + + String get selectFromFile { + return Intl.message( + 'Select from file', + name: 'selectFromFile', + ); + } + + String get chooseImage { + return Intl.message( + 'Choose image', + name: 'chooseImage', + ); + } + + String get urlLink { + return Intl.message( + 'URL', + name: 'urlLink', + ); + } + + String get insert { + return Intl.message( + 'Insert', + name: 'insert'); + } + + String get insertImageErrorFileEmpty { + return Intl.message( + 'Please either choose an image or enter an image URL', + name: 'insertImageErrorFileEmpty'); + } + + String get insertImageErrorDuplicate { + return Intl.message( + 'Please input either an image or an image URL, not both', + name: 'insertImageErrorDuplicate'); + } } \ No newline at end of file diff --git a/model/lib/extensions/attachment_extension.dart b/model/lib/extensions/attachment_extension.dart index 783b11dba..74c799e41 100644 --- a/model/lib/extensions/attachment_extension.dart +++ b/model/lib/extensions/attachment_extension.dart @@ -3,10 +3,28 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/model.dart'; extension AttachmentExtension on Attachment { - EmailBodyPart toEmailBodyPart(String disposition) => EmailBodyPart( + EmailBodyPart toEmailBodyPart({String? disposition, String? charset}) => EmailBodyPart( + partId: partId, blobId: blobId, size: size, name: name, type: type, - disposition: disposition); + cid: cid, + charset: charset, + disposition: disposition ?? this.disposition?.value); + + Attachment toAttachmentWithDisposition({ + ContentDisposition? disposition, + String? cid + }) { + return Attachment( + partId: partId, + blobId: blobId, + size: size, + name: name, + type: type, + cid: cid ?? this.cid, + disposition: disposition ?? this.disposition + ); + } } \ No newline at end of file diff --git a/model/lib/upload/file_info.dart b/model/lib/upload/file_info.dart index c2fdcaefb..89183c30b 100644 --- a/model/lib/upload/file_info.dart +++ b/model/lib/upload/file_info.dart @@ -15,6 +15,8 @@ class FileInfo with EquatableMixin { return FileInfo('', '', 0); } + String get fileExtension => fileName.split('.').last; + String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/json; charset=UTF-8'; @override diff --git a/pubspec.yaml b/pubspec.yaml index 0349919bd..61434e5f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -171,6 +171,9 @@ dependencies: async: 2.8.2 + # HTML5 parser + html: 0.15.0 + dev_dependencies: flutter_test: sdk: flutter