TF-61 Implement rich text on web

This commit is contained in:
dab246
2022-07-18 13:46:51 +07:00
committed by Dat H. Pham
parent eecac6c46d
commit 79e165443e
9 changed files with 131 additions and 50 deletions
@@ -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<UploadAttachmentInteractor>()));
Get.lazyPut(() => RichTextWebController());
Get.lazyPut(() => ComposerController(
Get.find<SendEmailInteractor>(),
Get.find<GetAutoCompleteInteractor>(),
@@ -148,11 +150,13 @@ class ComposerBindings extends BaseBindings {
Get.find<UploadController>(),
Get.find<RemoveComposerCacheOnWebInteractor>(),
Get.find<SaveComposerCacheOnWebInteractor>(),
Get.find<RichTextWebController>(),
));
}
void dispose() {
Get.delete<UploadController>();
Get.delete<RichTextWebController>();
Get.delete<ComposerController>();
}
}
@@ -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<EmailAddress> listToEmailAddress = <EmailAddress>[];
List<EmailAddress> listCcEmailAddress = <EmailAddress>[];
List<EmailAddress> listBccEmailAddress = <EmailAddress>[];
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 = '--<br><br>${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();
}
@@ -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<ComposerController> with AppLoaderMixin {
class ComposerView extends GetWidget<ComposerController>
with AppLoaderMixin, RichTextButtonMixin {
final responsiveUtils = Get.find<ResponsiveUtils>();
final imagePaths = Get.find<ImagePaths>();
@@ -58,6 +61,7 @@ class ComposerView extends GetWidget<ComposerController> with AppLoaderMixin {
Expanded(child: Column(
children: [
_buildAttachmentsWidget(context),
_buildToolbarRichTextWidget(),
_buildEditorForm(context)
]
)),
@@ -162,11 +166,15 @@ class ComposerView extends GetWidget<ComposerController> 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<ComposerController> 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<ComposerController> with AppLoaderMixin {
child: Column(
children: [
_buildAttachmentsWidget(context),
_buildToolbarRichTextWidget(),
_buildEditorForm(context)
]
))),
@@ -474,12 +485,18 @@ class ComposerView extends GetWidget<ComposerController> 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<ComposerController> 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<ComposerController> 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()
),
);
}
}
@@ -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,
);
}
}