Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TF-1486 Support insert image for identity #2032

Merged
merged 5 commits into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions assets/images/ic_add_picture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions core/lib/presentation/extensions/color_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ extension AppColor on Color {
static const colorCloseButton = Color(0xFF818C99);
static const colorDropShadow = Color(0x0F000000);
static const colorBackgroundKeyboard = Color(0xFFD2D5DC);
static const colorBackgroundKeyboardAndroid = Color(0xFFF2F0F4);
static const colorShadowLayerBottom = Color(0x29000000);
static const colorShadowLayerTop = Color(0x1F000000);
static const colorDividerHorizontal = Color(0x1F000000);
Expand Down
1 change: 1 addition & 0 deletions core/lib/presentation/resources/image_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class _TypeAheadFormFieldBuilderState<T> extends State<TypeAheadFormFieldBuilder
autofillHints: widget.autofillHints,
keyboardType: widget.keyboardType,
decoration: widget.decoration,
focusNode: widget.focusNode,
textDirection: _textDirection,
onChanged: (value) {
widget.onTextChange?.call(value);
Expand Down
9 changes: 7 additions & 2 deletions lib/features/composer/presentation/composer_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:core/presentation/views/button/icon_button_web.dart';
import 'package:core/presentation/views/context_menu/simple_context_menu_action_builder.dart';
import 'package:core/presentation/views/image/avatar_builder.dart';
import 'package:core/presentation/views/responsive/responsive_widget.dart';
import 'package:core/utils/platform_info.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -49,7 +50,9 @@ class ComposerView extends BaseComposerView {
return KeyboardRichText(
richTextController: controller.keyboardRichTextController,
keyBroadToolbar: RichTextKeyboardToolBar(
backgroundKeyboardToolBarColor: AppColor.colorBackgroundKeyboard,
backgroundKeyboardToolBarColor: PlatformInfo.isIOS
? AppColor.colorBackgroundKeyboard
: AppColor.colorBackgroundKeyboardAndroid,
isLandScapeMode: responsiveUtils.isLandscapeMobile(context),
insertAttachment: controller.isNetworkConnectionAvailable
? () => controller.openPickAttachmentMenu(context, _pickAttachmentsActionTiles(context))
Expand Down Expand Up @@ -98,7 +101,9 @@ class ComposerView extends BaseComposerView {
return KeyboardRichText(
richTextController: controller.keyboardRichTextController,
keyBroadToolbar: RichTextKeyboardToolBar(
backgroundKeyboardToolBarColor: AppColor.colorBackgroundKeyboard,
backgroundKeyboardToolBarColor: PlatformInfo.isIOS
? AppColor.colorBackgroundKeyboard
: AppColor.colorBackgroundKeyboardAndroid,
insertAttachment: () => controller.openPickAttachmentMenu(context, _pickAttachmentsActionTiles(context)),
insertImage: () => controller.insertImage(context, constraints.maxWidth),
richTextController: controller.keyboardRichTextController,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import 'package:core/utils/app_logger.dart';
import 'package:file_picker/file_picker.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:tmail_ui_user/features/composer/presentation/controller/base_rich_text_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart';
Expand All @@ -23,4 +23,16 @@ class RichTextMobileTabletController extends BaseRichTextController {
final styleSelected = newStyle ?? HeaderStyleType.normal;
htmlEditorApi?.formatHeader(styleSelected.styleValue);
}

void insertImageAsBase64({required PlatformFile platformFile, int? maxWidth}) async {
if (platformFile.bytes != null) {
await htmlEditorApi?.insertImageData(
platformFile.bytes!,
'image/${platformFile.extension}',
maxWidth: maxWidth
);
} else {
logError("RichTextWebController::insertImageAsBase64: bytes is null");
}
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
'<img src="data:image/${platformFile.extension};base64,$base64Data" data-filename="${platformFile.name}" alt="Image in my signature" style="max-width: 100%"/>'
);
} else {
logError("RichTextWebController::insertImageAsBase64: bytes is null");
}
}

@override
void onClose() {
menuParagraphController.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin
final RichTextWebController richTextWebController;
final ImagePaths _imagePaths = Get.find<ImagePaths>();
final EdgeInsetsGeometry? padding;
final List<Widget>? extendedOption;

ToolbarRichTextWebBuilder({
Key? key,
required this.richTextWebController,
this.padding,
this.extendedOption,
}) : super(key: key);

@override
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

extension SizeExtension on int {

int get toBytes => this * 1024;
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,8 +20,11 @@ import 'package:model/extensions/identity_extension.dart';
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_mobile_tablet_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';
Expand All @@ -34,6 +38,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';

Expand All @@ -44,6 +49,7 @@ class IdentityCreatorController extends BaseController {
final IdentityUtils _identityUtils;

final _uuid = Get.find<Uuid>();
final _appToast = Get.find<AppToast>();

final noneEmailAddress = EmailAddress(null, 'None');
final listEmailAddressDefault = <EmailAddress>[].obs;
Expand All @@ -56,8 +62,10 @@ class IdentityCreatorController extends BaseController {
final actionType = IdentityActionType.create.obs;
final isDefaultIdentity = RxBool(false);
final isDefaultIdentitySupported = RxBool(false);
final isMobileEditorFocus = RxBool(false);

final RichTextController keyboardRichTextController = RichTextController();
final RichTextMobileTabletController richTextMobileTabletController = RichTextMobileTabletController();
final RichTextWebController richTextWebController = RichTextWebController();
final TextEditingController inputNameIdentityController = TextEditingController();
final TextEditingController inputBccIdentityController = TextEditingController();
Expand All @@ -74,7 +82,7 @@ class IdentityCreatorController extends BaseController {
IdentityCreatorArguments? arguments;

final GlobalKey htmlKey = GlobalKey();
final htmlEditorMinHeight = 160;
final htmlEditorMinHeight = 150;

void updateNameIdentity(BuildContext context, String? value) {
_nameIdentity = value;
Expand Down Expand Up @@ -302,12 +310,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;
}

Expand Down Expand Up @@ -421,17 +431,23 @@ class IdentityCreatorController extends BaseController {
}

void initRichTextForMobile(BuildContext context, HtmlEditorApi editorApi) {
richTextMobileTabletController.htmlEditorApi = editorApi;
keyboardRichTextController.onCreateHTMLEditor(
editorApi,
onEnterKeyDown: _onEnterKeyDownOnMobile,
onFocus: _onFocusHTMLEditorOnMobile,
context: context
);
keyboardRichTextController.htmlEditorApi?.onFocusOut = () {
keyboardRichTextController.hideRichTextView();
isMobileEditorFocus.value = false;
};
}

void _onFocusHTMLEditorOnMobile() async {
inputBccIdentityFocusNode.unfocus();
inputNameIdentityFocusNode.unfocus();
isMobileEditorFocus.value = true;
if (htmlKey.currentContext != null) {
await Scrollable.ensureVisible(htmlKey.currentContext!);
}
Expand All @@ -456,4 +472,53 @@ class IdentityCreatorController extends BaseController {
);
}
}

void pickImage(BuildContext context, {int? maxWidth}) async {
clearFocusEditor(context);

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, maxWidth: maxWidth);
} else {
_appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).cannotSelectThisImage
);
}
} else {
logError("IdentityCreatorController::pickImage: context is unmounted");
}
}

bool _isExceedMaxSizeInlineImage(int fileSize) =>
fileSize > IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes;

void _insertInlineImage(
BuildContext context,
PlatformFile platformFile,
{int? maxWidth}
) {
if (_isExceedMaxSizeInlineImage(platformFile.size)) {
_appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly(
IdentityCreatorConstants.maxKBSizeIdentityInlineImage
)
);
} else {
if (PlatformInfo.isWeb) {
richTextWebController.insertImageAsBase64(platformFile: platformFile);
} else if (PlatformInfo.isMobile) {
sherlockvn marked this conversation as resolved.
Show resolved Hide resolved
richTextMobileTabletController.insertImageAsBase64(platformFile: platformFile, maxWidth: maxWidth);
} else {
logError("IdentityCreatorController::_insertInlineImage: Platform not supported");
}
}
}
}
Loading
Loading