TF-261 Add HtmlEditor for web browser

This commit is contained in:
dab246
2022-02-17 16:09:03 +07:00
committed by Dat H. Pham
parent 541cdb64f5
commit 2450cc9c6b
14 changed files with 136 additions and 68 deletions
+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0C18.6274 0 24 5.37258 24 12Z" fill="#007AFF"/>
<path d="M8.15912 14.1694C7.70202 15.406 7.41479 16.2748 7.29741 16.7759C6.92851 18.3507 6.66028 18.7054 8.03519 17.957C9.41011 17.2086 16.0663 13.5012 17.5513 12.6773C19.4871 11.6033 19.513 11.6872 17.4474 10.5469C15.8743 9.6784 9.29614 6.06777 8.03519 5.35883C6.77425 4.64989 6.92851 4.96514 7.29741 6.53991C7.41631 7.04748 7.70771 7.92406 8.17163 9.16964C8.4961 10.0408 9.25082 10.6801 10.1635 10.8569L14.0065 11.6013C14.0426 11.6085 14.066 11.6436 14.0588 11.6797C14.0535 11.7061 14.0329 11.7268 14.0065 11.732L10.1548 12.4757C9.2392 12.6525 8.48245 13.2947 8.15912 14.1694Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 839 B

+7 -2
View File
@@ -1,6 +1,7 @@
import 'dart:core'; import 'dart:core';
import 'package:device_info/device_info.dart'; import 'package:device_info_plus/device_info_plus.dart';
class DeviceManager { class DeviceManager {
final DeviceInfoPlugin _deviceInfoPlugin; final DeviceInfoPlugin _deviceInfoPlugin;
@@ -9,6 +10,10 @@ class DeviceManager {
Future<bool> isNeedRequestStoragePermissionOnAndroid() async { Future<bool> isNeedRequestStoragePermissionOnAndroid() async {
final androidInfo = await _deviceInfoPlugin.androidInfo; final androidInfo = await _deviceInfoPlugin.androidInfo;
return androidInfo.version.sdkInt <= 28; final sdkInt = androidInfo.version.sdkInt;
if (sdkInt != null) {
return sdkInt <= 28;
}
return false;
} }
} }
@@ -79,6 +79,7 @@ class ImagePaths {
String get icFilterMessageAll => _getImagePath('ic_filter_message_all.svg'); String get icFilterMessageAll => _getImagePath('ic_filter_message_all.svg');
String get icFilterMessageAttachments => _getImagePath('ic_filter_message_attachments.svg'); String get icFilterMessageAttachments => _getImagePath('ic_filter_message_attachments.svg');
String get icLogoTMailV2 => _getImagePath('logo_tmail_v2.png'); String get icLogoTMailV2 => _getImagePath('logo_tmail_v2.png');
String get icSendToast => _getImagePath('ic_send_toast.svg');
String _getImagePath(String imageName) { String _getImagePath(String imageName) {
return AssetsPaths.images + imageName; return AssetsPaths.images + imageName;
+4 -3
View File
@@ -61,7 +61,8 @@ class AppToast {
} }
void showToastWithIcon(BuildContext context, void showToastWithIcon(BuildContext context,
{String? message, String? icon, Color? bgColor, double? radius, EdgeInsets? padding, TextStyle? textStyle}) { {String? message, String? icon, Color? bgColor,
Color? textColor, double? radius, EdgeInsets? padding, TextStyle? textStyle}) {
final toast = Material( final toast = Material(
color: bgColor ?? Colors.white, color: bgColor ?? Colors.white,
elevation: 10, elevation: 10,
@@ -71,7 +72,7 @@ class AppToast {
padding: padding ?? EdgeInsets.symmetric(horizontal: 12.0, vertical: 14), padding: padding ?? EdgeInsets.symmetric(horizontal: 12.0, vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius ?? 10.0), borderRadius: BorderRadius.circular(radius ?? 10.0),
color: Colors.white, color: bgColor ?? Colors.white,
), ),
width: 320, width: 320,
child: Row( child: Row(
@@ -81,7 +82,7 @@ class AppToast {
SizedBox(width: 10.0), SizedBox(width: 10.0),
Expanded(child: Text( Expanded(child: Text(
message ?? '', message ?? '',
style: textStyle ?? TextStyle(fontSize: 15, color: Colors.black))), style: textStyle ?? TextStyle(fontSize: 15, color: textColor ?? Colors.black))),
], ],
), ),
), ),
+2 -2
View File
@@ -55,8 +55,8 @@ dependencies:
# GetX # GetX
get: 4.1.4 get: 4.1.4
# device_info # device_info_plus
device_info: 2.0.2 device_info_plus: 3.2.2
# getwidget # getwidget
getwidget: 2.0.4 getwidget: 2.0.4
@@ -1,4 +1,5 @@
import 'package:core/core.dart'; import 'package:core/core.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tmail_ui_user/features/composer/data/datasource/autocomplete_datasource.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/autocomplete_datasource.dart';
@@ -70,7 +71,9 @@ class ComposerBindings extends Bindings {
Get.find<GetAutoCompleteInteractor>(), Get.find<GetAutoCompleteInteractor>(),
Get.find<GetAutoCompleteWithDeviceContactInteractor>(), Get.find<GetAutoCompleteWithDeviceContactInteractor>(),
Get.find<AppToast>(), Get.find<AppToast>(),
Get.find<ImagePaths>(),
Get.find<Uuid>(), Get.find<Uuid>(),
Get.find<DeviceInfoPlugin>(),
Get.find<TextEditingController>(), Get.find<TextEditingController>(),
Get.find<LocalFilePickerInteractor>(), Get.find<LocalFilePickerInteractor>(),
Get.find<UploadMultipleAttachmentInteractor>())); Get.find<UploadMultipleAttachmentInteractor>()));
@@ -3,13 +3,15 @@ import 'dart:async';
import 'package:core/core.dart'; import 'package:core/core.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:enough_html_editor/enough_html_editor.dart'; import 'package:enough_html_editor/enough_html_editor.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fk_user_agent/fk_user_agent.dart'; import 'package:fk_user_agent/fk_user_agent.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_chips_input/flutter_chips_input.dart'; import 'package:flutter_chips_input/flutter_chips_input.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:html_editor_enhanced/html_editor.dart' as HtmlEditorBrowser;
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart';
@@ -39,7 +41,6 @@ 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/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/utils/app_logger.dart'; import 'package:tmail_ui_user/main/utils/app_logger.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'dart:developer' as developer;
class ComposerController extends BaseController { class ComposerController extends BaseController {
@@ -55,10 +56,12 @@ class ComposerController extends BaseController {
final GetAutoCompleteInteractor _getAutoCompleteInteractor; final GetAutoCompleteInteractor _getAutoCompleteInteractor;
final GetAutoCompleteWithDeviceContactInteractor _getAutoCompleteWithDeviceContactInteractor; final GetAutoCompleteWithDeviceContactInteractor _getAutoCompleteWithDeviceContactInteractor;
final AppToast _appToast; final AppToast _appToast;
final ImagePaths _imagePaths;
final Uuid _uuid; final Uuid _uuid;
final TextEditingController subjectEmailInputController; final TextEditingController subjectEmailInputController;
final LocalFilePickerInteractor _localFilePickerInteractor; final LocalFilePickerInteractor _localFilePickerInteractor;
final UploadMultipleAttachmentInteractor _uploadMultipleAttachmentInteractor; final UploadMultipleAttachmentInteractor _uploadMultipleAttachmentInteractor;
final DeviceInfoPlugin _deviceInfoPlugin;
List<EmailAddress> listToEmailAddress = []; List<EmailAddress> listToEmailAddress = [];
List<EmailAddress> listCcEmailAddress = []; List<EmailAddress> listCcEmailAddress = [];
@@ -66,19 +69,30 @@ class ComposerController extends BaseController {
String? _subjectEmail; String? _subjectEmail;
ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.localContact; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.localContact;
HtmlEditorApi? htmlEditorApi; HtmlEditorApi? htmlEditorApi;
final HtmlEditorBrowser.HtmlEditorController htmlControllerBrowser = HtmlEditorBrowser.HtmlEditorController();
final keyToEmailAddress = GlobalKey<ChipsInputState>(); final keyToEmailAddress = GlobalKey<ChipsInputState>();
final keyCcEmailAddress = GlobalKey<ChipsInputState>(); final keyCcEmailAddress = GlobalKey<ChipsInputState>();
final keyBccEmailAddress = GlobalKey<ChipsInputState>(); final keyBccEmailAddress = GlobalKey<ChipsInputState>();
void setSubjectEmail(String subject) => _subjectEmail = subject; void setSubjectEmail(String subject) => _subjectEmail = subject;
Future<String> _getEmailBodyText() async {
if (kIsWeb) {
return await htmlControllerBrowser.getText();
} else {
return (await htmlEditorApi?.getFullHtml()) ?? '';
}
}
ComposerController( ComposerController(
this._sendEmailInteractor, this._sendEmailInteractor,
this._saveEmailAddressInteractor, this._saveEmailAddressInteractor,
this._getAutoCompleteInteractor, this._getAutoCompleteInteractor,
this._getAutoCompleteWithDeviceContactInteractor, this._getAutoCompleteWithDeviceContactInteractor,
this._appToast, this._appToast,
this._imagePaths,
this._uuid, this._uuid,
this._deviceInfoPlugin,
this.subjectEmailInputController, this.subjectEmailInputController,
this._localFilePickerInteractor, this._localFilePickerInteractor,
this._uploadMultipleAttachmentInteractor, this._uploadMultipleAttachmentInteractor,
@@ -124,7 +138,13 @@ class ComposerController extends BaseController {
@override @override
void onError(error) { void onError(error) {
_appToast.showErrorToast(AppLocalizations.of(Get.context!).error_message_sent); if (Get.overlayContext != null && Get.context != null) {
_appToast.showToastWithIcon(
Get.overlayContext!,
textColor: AppColor.toastErrorBackgroundColor,
message: AppLocalizations.of(Get.context!).message_has_been_sent_failure,
icon: _imagePaths.icSendToast);
}
popBack(); popBack();
} }
@@ -261,7 +281,8 @@ class ComposerController extends BaseController {
final generatePartId = PartId(_uuid.v1()); final generatePartId = PartId(_uuid.v1());
final generateBlobId = Id(_uuid.v1()); final generateBlobId = Id(_uuid.v1());
final emailBodyText = (await htmlEditorApi?.getFullHtml()) ?? ''; final emailBodyText = await _getEmailBodyText();
final userAgent = await userAgentPlatform;
return Email( return Email(
generateEmailId, generateEmailId,
@@ -280,17 +301,22 @@ class ComposerController extends BaseController {
bodyValues: { bodyValues: {
generatePartId: EmailBodyValue(emailBodyText, false, false) generatePartId: EmailBodyValue(emailBodyText, false, false)
}, },
headerUserAgent: {IndividualHeaderIdentifier.headerUserAgent : userAgentPlatform}, headerUserAgent: {IndividualHeaderIdentifier.headerUserAgent : userAgent},
attachments: attachments.isNotEmpty ? _generateAttachments() : null, attachments: attachments.isNotEmpty ? _generateAttachments() : null,
); );
} }
String get userAgentPlatform { Future<String> get userAgentPlatform async {
String userAgent; String userAgent;
try { try {
userAgent = FkUserAgent.userAgent ?? ''; if (kIsWeb) {
final webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo;
userAgent = webBrowserInfo.userAgent ?? '';
} else {
userAgent = FkUserAgent.userAgent ?? '';
}
log('ComposerController - userAgentPlatform(): userAgent: $userAgent'); log('ComposerController - userAgentPlatform(): userAgent: $userAgent');
} on PlatformException { } on Exception {
userAgent = ''; userAgent = '';
} }
return userAgent; return userAgent;
@@ -1,6 +1,8 @@
import 'package:core/core.dart'; import 'package:core/core.dart';
import 'package:enough_html_editor/enough_html_editor.dart'; import 'package:enough_html_editor/enough_html_editor.dart';
import 'package:html_editor_enhanced/html_editor.dart' as HtmlEditorBrowser;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -22,8 +24,7 @@ class ComposerView extends GetWidget<ComposerController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
FocusManager.instance.primaryFocus?.unfocus(); controller.htmlEditorApi?.unfocus(context);
controller.htmlEditorApi?.unfocus();
}, },
child: Scaffold( child: Scaffold(
backgroundColor: AppColor.primaryLightColor, backgroundColor: AppColor.primaryLightColor,
@@ -225,28 +226,36 @@ class ComposerView extends GetWidget<ComposerController> {
Widget _buildComposerEditor(BuildContext context) { Widget _buildComposerEditor(BuildContext context) {
return Obx(() { return Obx(() {
if (controller.composerArguments.value?.emailActionType == EmailActionType.compose) { if (kIsWeb) {
return HtmlEditor( return HtmlEditorBrowser.HtmlEditor(
key: Key('composer_editor'), key: Key('composer_editor'),
minHeight: 100, controller: controller.htmlControllerBrowser,
supportZoom: true, htmlEditorOptions: HtmlEditorBrowser.HtmlEditorOptions(
disableHorizontalScroll: false, shouldEnsureVisible: true,
disableVerticalScroll: false, initialText: controller.getContentEmail(),
onCreated: (editorApi) => controller.htmlEditorApi = editorApi, autoAdjustHeight: controller.getContentEmail().isNotEmpty,
),
htmlToolbarOptions: HtmlEditorBrowser.HtmlToolbarOptions(
toolbarPosition: HtmlEditorBrowser.ToolbarPosition.custom
),
otherOptions: HtmlEditorBrowser.OtherOptions(height: 550),
); );
} else { } else {
final message = controller.getContentEmail(); if (controller.composerArguments.value?.emailActionType == EmailActionType.compose) {
return message.isNotEmpty return HtmlEditor(
? HtmlEditor(
key: Key('composer_editor'), key: Key('composer_editor'),
minHeight: 100, minHeight: 100,
supportZoom: true, onCreated: (editorApi) => controller.htmlEditorApi = editorApi);
disableHorizontalScroll: false, } else {
disableVerticalScroll: false, final message = controller.getContentEmail();
onCreated: (editorApi) => controller.htmlEditorApi = editorApi, return message.isNotEmpty
initialContent: message, ? HtmlEditor(
) key: Key('composer_editor'),
: SizedBox.shrink(); minHeight: 100,
onCreated: (editorApi) => controller.htmlEditorApi = editorApi,
initialContent: message)
: SizedBox.shrink();
}
} }
}); });
} }
@@ -61,13 +61,13 @@ class EmailView extends GetView {
Widget _buildBottomBar(BuildContext context) { Widget _buildBottomBar(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.only(left: 6, top: 6, right: 6, bottom: 6), padding: EdgeInsets.only(left: 6, top: 6, right: 6, bottom: 6),
child: (BottomBarMailWidgetBuilder( child: Obx(() => (BottomBarMailWidgetBuilder(
context, context,
imagePaths, imagePaths,
responsiveUtils, responsiveUtils,
emailController.mailboxDashBoardController.selectedEmail.value) emailController.mailboxDashBoardController.selectedEmail.value)
..addOnPressEmailAction((emailActionType) => emailController.pressEmailAction(emailActionType))) ..addOnPressEmailAction((emailActionType) => emailController.pressEmailAction(emailActionType)))
.build()); .build()));
} }
Widget _buildEmailBody(BuildContext context) { Widget _buildEmailBody(BuildContext context) {
@@ -16,11 +16,12 @@ class MailboxDashBoardBindings extends Bindings {
Get.lazyPut(() => CredentialRepositoryImpl(Get.find<SharedPreferences>())); Get.lazyPut(() => CredentialRepositoryImpl(Get.find<SharedPreferences>()));
Get.lazyPut<CredentialRepository>(() => Get.find<CredentialRepositoryImpl>()); Get.lazyPut<CredentialRepository>(() => Get.find<CredentialRepositoryImpl>());
Get.lazyPut(() => GetUserProfileInteractor(Get.find<CredentialRepository>())); Get.lazyPut(() => GetUserProfileInteractor(Get.find<CredentialRepository>()));
Get.lazyPut(() => AppToast());
Get.put(MailboxDashBoardController( Get.put(MailboxDashBoardController(
Get.find<GetUserProfileInteractor>(), Get.find<GetUserProfileInteractor>(),
Get.find<AppToast>())); Get.find<AppToast>(),
Get.find<ImagePaths>(),
));
MailboxBindings().dependencies(); MailboxBindings().dependencies();
ThreadBindings().dependencies(); ThreadBindings().dependencies();
@@ -21,6 +21,7 @@ class MailboxDashBoardController extends BaseController {
final GetUserProfileInteractor _getUserProfileInteractor; final GetUserProfileInteractor _getUserProfileInteractor;
final AppToast _appToast; final AppToast _appToast;
final ImagePaths _imagePaths;
final scaffoldKey = GlobalKey<ScaffoldState>(); final scaffoldKey = GlobalKey<ScaffoldState>();
final selectedMailbox = Rxn<PresentationMailbox>(); final selectedMailbox = Rxn<PresentationMailbox>();
@@ -37,7 +38,11 @@ class MailboxDashBoardController extends BaseController {
Map<Role, MailboxId> mapDefaultMailboxId = Map(); Map<Role, MailboxId> mapDefaultMailboxId = Map();
Map<MailboxId, PresentationMailbox> mapMailbox = Map(); Map<MailboxId, PresentationMailbox> mapMailbox = Map();
MailboxDashBoardController(this._getUserProfileInteractor, this._appToast); MailboxDashBoardController(
this._getUserProfileInteractor,
this._appToast,
this._imagePaths,
);
@override @override
void onReady() { void onReady() {
@@ -51,8 +56,11 @@ class MailboxDashBoardController extends BaseController {
super.onData(newState); super.onData(newState);
viewState.value.map((success) { viewState.value.map((success) {
if (success is SendingEmailState) { if (success is SendingEmailState) {
if (Get.context != null) { if (Get.overlayContext != null && Get.context != null) {
_appToast.showToast(AppLocalizations.of(Get.context!).your_email_being_sent); _appToast.showToastWithIcon(
Get.overlayContext!,
message: AppLocalizations.of(Get.context!).your_email_being_sent,
icon: _imagePaths.icSendToast);
} }
} }
}); });
@@ -63,20 +71,28 @@ class MailboxDashBoardController extends BaseController {
viewState.value.fold( viewState.value.fold(
(failure) { (failure) {
if (failure is SendEmailFailure) { if (failure is SendEmailFailure) {
if (Get.context != null) { if (Get.overlayContext != null && Get.context != null) {
_appToast.showErrorToast(AppLocalizations.of(Get.context!).error_message_sent); _appToast.showToastWithIcon(
clearState(); Get.overlayContext!,
textColor: AppColor.toastErrorBackgroundColor,
message: AppLocalizations.of(Get.context!).message_has_been_sent_failure,
icon: _imagePaths.icSendToast);
} }
clearState();
} }
}, },
(success) { (success) {
if (success is GetUserProfileSuccess) { if (success is GetUserProfileSuccess) {
userProfile.value = success.userProfile; userProfile.value = success.userProfile;
} else if (success is SendEmailSuccess) { } else if (success is SendEmailSuccess) {
if (Get.context != null) { if (Get.overlayContext != null && Get.context != null) {
_appToast.showSuccessToast(AppLocalizations.of(Get.context!).message_sent); _appToast.showToastWithIcon(
clearState(); Get.overlayContext!,
textColor: AppColor.toastSuccessBackgroundColor,
message: AppLocalizations.of(Get.context!).message_has_been_sent_successfully,
icon: _imagePaths.icSendToast);
} }
clearState();
} }
} }
); );
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:core/core.dart'; import 'package:core/core.dart';
import 'package:device_info/device_info.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
+14 -14
View File
@@ -214,20 +214,6 @@ class AppLocalizations {
); );
} }
String get message_sent {
return Intl.message(
'Message sent',
name: 'message_sent',
);
}
String get error_message_sent {
return Intl.message(
'Error message sent',
name: 'error_message_sent',
);
}
String count_email_selected(int count) { String count_email_selected(int count) {
return Intl.message( return Intl.message(
'$count selected', '$count selected',
@@ -585,4 +571,18 @@ class AppLocalizations {
'With Starred', 'With Starred',
name: 'with_starred'); name: 'with_starred');
} }
String get message_has_been_sent_successfully {
return Intl.message(
'Message has been sent successfully',
name: 'message_has_been_sent_successfully',
);
}
String get message_has_been_sent_failure {
return Intl.message(
'Message has been sent failure',
name: 'message_has_been_sent_failure',
);
}
} }
+8 -6
View File
@@ -95,8 +95,8 @@ dependencies:
# path_provider # path_provider
path_provider: 2.0.3 path_provider: 2.0.3
# device_info # device_info_plus
device_info: 2.0.2 device_info_plus: 3.2.2
# permission_handler # permission_handler
permission_handler: 8.1.6 permission_handler: 8.1.6
@@ -111,16 +111,18 @@ dependencies:
ref: master ref: master
# file_picker # file_picker
file_picker: 3.0.2+2 file_picker: 4.2.0
# hive # hive
hive: 2.0.4 hive: 2.0.4
# enough_html_editor # enough_html_editor: Compose email for Mobile & Tablet
enough_html_editor: enough_html_editor:
git: git:
url: https://github.com/dab246/enough_html_editor.git url: https://github.com/Enough-Software/enough_html_editor.git
ref: fix_bug_enough_platform_widgets
# html_editor_enhanced: Compose email for Web Browser
html_editor_enhanced: 2.4.0+1
# fk_user_agent # fk_user_agent
fk_user_agent: 2.1.0 fk_user_agent: 2.1.0