From 79e165443e7102e99d63be973d57775282e8fc4d Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 18 Jul 2022 13:46:51 +0700 Subject: [PATCH] TF-61 Implement rich text on web --- assets/images/ic_style_bold.svg | 3 + assets/images/ic_style_italic.svg | 3 + assets/images/ic_style_underline.svg | 3 + .../presentation/resources/image_paths.dart | 3 + .../views/button/icon_button_web.dart | 8 +- .../presentation/composer_bindings.dart | 4 + .../presentation/composer_controller.dart | 17 +-- .../presentation/composer_view_web.dart | 113 +++++++++++------- .../mixin/rich_text_button_mixin.dart | 27 +++++ 9 files changed, 131 insertions(+), 50 deletions(-) create mode 100644 assets/images/ic_style_bold.svg create mode 100644 assets/images/ic_style_italic.svg create mode 100644 assets/images/ic_style_underline.svg create mode 100644 lib/features/composer/presentation/mixin/rich_text_button_mixin.dart diff --git a/assets/images/ic_style_bold.svg b/assets/images/ic_style_bold.svg new file mode 100644 index 000000000..cfc08f1c9 --- /dev/null +++ b/assets/images/ic_style_bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_style_italic.svg b/assets/images/ic_style_italic.svg new file mode 100644 index 000000000..222753ded --- /dev/null +++ b/assets/images/ic_style_italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_style_underline.svg b/assets/images/ic_style_underline.svg new file mode 100644 index 000000000..aa7a8506e --- /dev/null +++ b/assets/images/ic_style_underline.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 1a8f5ebe4..6eb5c819a 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -128,6 +128,9 @@ class ImagePaths { String get icCloseAdvancedSearch => _getImagePath('ic_close_advanced_search.svg'); String get icLanguage => _getImagePath('ic_language.svg'); String get icChecked => _getImagePath('ic_checked.svg'); + 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 _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 7280becb6..d476ccf36 100644 --- a/core/lib/presentation/views/button/icon_button_web.dart +++ b/core/lib/presentation/views/button/icon_button_web.dart @@ -13,12 +13,16 @@ Widget buildIconWeb({ double? iconSize, double? splashRadius, double? minSize, + Color? colorSelected, + Color? colorFocus, + ShapeBorder? shapeBorder, }) { return Material( - color: Colors.transparent, - shape: CircleBorder(), + color: colorSelected ?? Colors.transparent, + shape: shapeBorder ?? CircleBorder(), child: IconButton( icon: icon, + focusColor: colorFocus, iconSize: iconSize, constraints: minSize != null ? BoxConstraints(minWidth: minSize, minHeight: minSize) : null, padding: iconPadding ?? EdgeInsets.all(8.0), diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index e46e5eff7..3686ce093 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -19,6 +19,7 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachmen import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/session_extension.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; @@ -135,6 +136,7 @@ class ComposerBindings extends BaseBindings { @override void bindingsController() { Get.lazyPut(() => UploadController(Get.find())); + Get.lazyPut(() => RichTextWebController()); Get.lazyPut(() => ComposerController( Get.find(), Get.find(), @@ -148,11 +150,13 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } void dispose() { Get.delete(); + Get.delete(); Get.delete(); } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 320d03ba9..de0cebcaf 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -12,7 +12,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:html_editor_enhanced/html_editor.dart' as editor_web; import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; @@ -34,6 +33,7 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_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/screen_display_mode.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; @@ -87,13 +87,13 @@ class ComposerController extends BaseController { final UploadController uploadController; final RemoveComposerCacheOnWebInteractor _removeComposerCacheOnWebInteractor; final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor; + final RichTextWebController richTextWebController; List listToEmailAddress = []; List listCcEmailAddress = []; List listBccEmailAddress = []; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; HtmlEditorApi? htmlEditorApi; - final htmlControllerBrowser = editor_web.HtmlEditorController(processNewLineAsBr: true); final subjectEmailInputController = TextEditingController(); final toEmailAddressController = TextEditingController(); @@ -119,7 +119,7 @@ class ComposerController extends BaseController { screenDisplayMode.value == ScreenDisplayMode.minimize) { contentHtml = textEditorWeb ?? ''; } else { - contentHtml = await htmlControllerBrowser.getText(); + contentHtml = await richTextWebController.editorController.getText(); } log('ComposerController::_getEmailBodyText():WEB: contentHtml: $contentHtml'); final newContentHtml = contentHtml.removeEditorStartTag(); @@ -155,6 +155,7 @@ class ComposerController extends BaseController { this.uploadController, this._removeComposerCacheOnWebInteractor, this._saveComposerCacheOnWebInteractor, + this.richTextWebController, ); @override @@ -318,7 +319,7 @@ class ComposerController extends BaseController { } void setFullScreenEditor() { - htmlControllerBrowser.setFullScreen(); + richTextWebController.editorController.setFullScreen(); } String? _getHeaderEmailQuoted(BuildContext context, ComposerArguments arguments) { @@ -872,8 +873,8 @@ class ComposerController extends BaseController { } void _updateTextForEditor() async { - final textCurrent = await htmlControllerBrowser.getText(); - htmlControllerBrowser.setText(textCurrent); + final textCurrent = await richTextWebController.editorController.getText(); + richTextWebController.editorController.setText(textCurrent); } void deleteComposer() { @@ -1036,7 +1037,7 @@ class ComposerController extends BaseController { final signatureAsHtml = '--

${signature.value}'; log('ComposerController::_applySignature(): $signatureAsHtml'); if (BuildUtils.isWeb) { - htmlControllerBrowser.insertSignature(signatureAsHtml); + richTextWebController.editorController.insertSignature(signatureAsHtml); } else { htmlEditorApi?.insertSignature(signatureAsHtml); } @@ -1045,7 +1046,7 @@ class ComposerController extends BaseController { void _removeSignature() { log('ComposerController::_removeSignature():'); if (BuildUtils.isWeb) { - htmlControllerBrowser.removeSignature(); + richTextWebController.editorController.removeSignature(); } else { htmlEditorApi?.removeSignature(); } diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index e20a84ce8..ca94557ca 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -6,13 +6,15 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:html_editor_enhanced/html_editor.dart' as editor_web; +import 'package:html_editor_enhanced/html_editor.dart'; 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/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'; @@ -21,7 +23,8 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -class ComposerView extends GetWidget with AppLoaderMixin { +class ComposerView extends GetWidget + with AppLoaderMixin, RichTextButtonMixin { final responsiveUtils = Get.find(); final imagePaths = Get.find(); @@ -58,6 +61,7 @@ class ComposerView extends GetWidget with AppLoaderMixin { Expanded(child: Column( children: [ _buildAttachmentsWidget(context), + _buildToolbarRichTextWidget(), _buildEditorForm(context) ] )), @@ -162,11 +166,15 @@ class ComposerView extends GetWidget with AppLoaderMixin { return Row( children: [ buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, icon: SvgPicture.asset(imagePaths.icCloseMailbox, fit: BoxFit.fill), tooltip: AppLocalizations.of(context).saveAndClose, onTap: () => controller.saveEmailAsDrafts(context)), if (responsiveUtils.isDesktop(context)) Obx(() => buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, icon: SvgPicture.asset( controller.screenDisplayMode.value == ScreenDisplayMode.fullScreen ? imagePaths.icFullScreenExit @@ -178,6 +186,8 @@ class ComposerView extends GetWidget with AppLoaderMixin { : ScreenDisplayMode.fullScreen))), if (responsiveUtils.isDesktop(context)) buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, icon: SvgPicture.asset(imagePaths.icMinimize, fit: BoxFit.fill), tooltip: AppLocalizations.of(context).minimize, onTap: () => controller.displayScreenTypeComposerAction(ScreenDisplayMode.minimize)), @@ -284,6 +294,7 @@ class ComposerView extends GetWidget with AppLoaderMixin { child: Column( children: [ _buildAttachmentsWidget(context), + _buildToolbarRichTextWidget(), _buildEditorForm(context) ] ))), @@ -474,12 +485,18 @@ class ComposerView extends GetWidget with AppLoaderMixin { Widget _buildListButton(BuildContext context) { return Transform( - transform: Matrix4.translationValues(-10.0, 0.0, 0.0), + transform: Matrix4.translationValues(-5.0, 0.0, 0.0), child: Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children: [ buildIconWeb( - icon: SvgPicture.asset(imagePaths.icAttachmentsComposer, color: AppColor.colorTextButton, fit: BoxFit.fill), + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset(imagePaths.icAttachmentsComposer, + width: 24, + height: 24, + color: AppColor.colorTextButton, + fit: BoxFit.fill), tooltip: AppLocalizations.of(context).attach_file, onTap: () => controller.openFilePickerByType(context, FileType.any)), ]) @@ -546,46 +563,48 @@ class ComposerView extends GetWidget with AppLoaderMixin { Widget _buildHtmlEditor(BuildContext context, String initContent) { log('ComposerView::_buildHtmlEditor(): initContent: $initContent'); - return Expanded( child: Padding( - padding: EdgeInsets.symmetric( - horizontal: responsiveUtils.isMobile(context) ? 8 : 10), - child: editor_web.HtmlEditor( - key: const Key('composer_editor_web'), - controller: controller.htmlControllerBrowser, - htmlEditorOptions: const editor_web.HtmlEditorOptions( - hint: '', - darkMode: false, - ), - blockQuotedContent: initContent, - htmlToolbarOptions: const editor_web.HtmlToolbarOptions( - toolbarPosition: editor_web.ToolbarPosition.custom), - otherOptions: const editor_web.OtherOptions(height: 550), - callbacks: editor_web.Callbacks( - onBeforeCommand: (String? currentHtml) { - log('ComposerView::_buildComposerEditor(): onBeforeCommand : $currentHtml'); - controller.setTextEditorWeb(currentHtml); - }, onChangeContent: (String? changed) { - log('ComposerView::_buildComposerEditor(): onChangeContent : $changed'); - controller.setTextEditorWeb(changed); - }, onInit: () { - log('ComposerView::_buildComposerEditor(): onInit'); - controller.setTextEditorWeb(initContent); - controller.setFullScreenEditor(); - }, onFocus: () { - FocusManager.instance.primaryFocus?.unfocus(); - Future.delayed(const Duration(milliseconds: 500), () { - controller.htmlControllerBrowser.setFocus(); - }); - }, onBlur: () { - controller.onEditorFocusChange(false); - }, onMouseDown: () { - controller.onEditorFocusChange(true); - }), - ) + padding: EdgeInsets.symmetric( + horizontal: responsiveUtils.isMobile(context) ? 8 : 10), + child: HtmlEditor( + key: const Key('composer_editor_web'), + controller: controller.richTextWebController.editorController, + htmlEditorOptions: const HtmlEditorOptions( + hint: '', + darkMode: false, + ), + blockQuotedContent: initContent, + htmlToolbarOptions: const HtmlToolbarOptions( + toolbarType: ToolbarType.hide, + defaultToolbarButtons: []), + otherOptions: const OtherOptions(height: 550), + callbacks: Callbacks(onBeforeCommand: (String? currentHtml) { + log('ComposerView::_buildHtmlEditor(): onBeforeCommand : $currentHtml'); + controller.setTextEditorWeb(currentHtml); + }, onChangeContent: (String? changed) { + log('ComposerView::_buildHtmlEditor(): onChangeContent : $changed'); + controller.setTextEditorWeb(changed); + }, onInit: () { + log('ComposerView::_buildHtmlEditor(): onInit'); + controller.setTextEditorWeb(initContent); + controller.setFullScreenEditor(); + }, onFocus: () { + FocusManager.instance.primaryFocus?.unfocus(); + Future.delayed(const Duration(milliseconds: 500), () { + controller.richTextWebController.editorController.setFocus(); + }); + }, onBlur: () { + controller.onEditorFocusChange(false); + }, onMouseDown: () { + controller.onEditorFocusChange(true); + }, onChangeSelection: (settings) { + controller.richTextWebController.onEditorSettingsChange(settings); + }), + ) ) ); + // }); } Widget _buildAttachmentsWidget(BuildContext context) { @@ -713,4 +732,18 @@ class ComposerView extends GetWidget with AppLoaderMixin { return constraints.maxHeight > 0 ? constraints.maxHeight * 0.4 : 150.0; } } + + Widget _buildToolbarRichTextWidget() { + return Padding( + padding: const EdgeInsets.only(left: 20, top: 4, bottom: 8), + child: Row( + children: RichTextStyleType.values.map((textType) => Obx(() { + return buildIconStyleText( + path: textType.getIcon(imagePaths), + isSelected: controller.richTextWebController.isTextStyleTypeSelected(textType), + onTap: () => controller.richTextWebController.applyRichTextStyle(textType)); + })).toList() + ), + ); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/mixin/rich_text_button_mixin.dart b/lib/features/composer/presentation/mixin/rich_text_button_mixin.dart new file mode 100644 index 000000000..24ebda281 --- /dev/null +++ b/lib/features/composer/presentation/mixin/rich_text_button_mixin.dart @@ -0,0 +1,27 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +mixin RichTextButtonMixin { + + Widget buildIconStyleText({ + required String path, + required bool? isSelected, + required VoidCallback onTap, + }){ + return buildIconWeb( + icon: SvgPicture.asset( + path, + color: isSelected == true + ? Colors.black + : AppColor.colorDividerMailbox, + fit: BoxFit.fill), + iconPadding: const EdgeInsets.all(4), + colorFocus: Colors.white, + minSize: 26, + onTap: onTap, + ); + } +} \ No newline at end of file