From b13c4e5ada82cff894de2a4654cac82a210e8167 Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 16 Jan 2024 10:32:49 +0700 Subject: [PATCH 1/5] TF-2426 Add text --- lib/l10n/intl_messages.arb | 24 ++++++++++++++++ lib/main/localizations/app_localizations.dart | 28 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index b563d04a00..f7fff0d73b 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -3873,5 +3873,29 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "insertLink": "Insert link", + "@insertLink": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textToDisplay": "Text to display", + "@textToDisplay": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toWhatURLShouldThisLinkGo": "To what URL should this link go?", + "@toWhatURLShouldThisLinkGo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pleaseEnterURL": "Please enter a URL", + "@pleaseEnterURL": { + "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 5dcf3481c2..6466e2258d 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4043,4 +4043,32 @@ class AppLocalizations { 'Show less', name: 'showLess'); } + + String get insertLink { + return Intl.message( + 'Insert link', + name: 'insertLink', + ); + } + + String get textToDisplay { + return Intl.message( + 'Text to display', + name: 'textToDisplay', + ); + } + + String get toWhatURLShouldThisLinkGo { + return Intl.message( + 'To what URL should this link go?', + name: 'toWhatURLShouldThisLinkGo', + ); + } + + String get pleaseEnterURL { + return Intl.message( + 'Please enter a URL', + name: 'pleaseEnterURL', + ); + } } \ No newline at end of file From dbaeb16f56d32a2354bb43334bfcd272e31d3968 Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 16 Jan 2024 10:33:13 +0700 Subject: [PATCH 2/5] TF-2426 Add validator to textFormFieldBuilder (cherry picked from commit a2325f64013b812a913b1cb18cccb709459ff2c7) --- .../lib/presentation/views/text/text_form_field_builder.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/lib/presentation/views/text/text_form_field_builder.dart b/core/lib/presentation/views/text/text_form_field_builder.dart index a1a21c6ce7..1d6407e30d 100644 --- a/core/lib/presentation/views/text/text_form_field_builder.dart +++ b/core/lib/presentation/views/text/text_form_field_builder.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; +typedef OnValidator = String? Function(String? value); + class TextFormFieldBuilder extends StatefulWidget { final ValueChanged? onTextChange; @@ -25,6 +27,7 @@ class TextFormFieldBuilder extends StatefulWidget { final bool readOnly; final MouseCursor? mouseCursor; final List? autofillHints; + final OnValidator? validator; const TextFormFieldBuilder({ super.key, @@ -49,6 +52,7 @@ class TextFormFieldBuilder extends StatefulWidget { this.onTap, this.onTextChange, this.onTextSubmitted, + this.validator, }); @override @@ -107,6 +111,7 @@ class _TextFieldFormBuilderState extends State { }, onSubmitted: widget.onTextSubmitted, onTap: widget.onTap, + validator: widget.validator, ); } From b3dcaf114b6bcf8d5e525b8ecffd82ca31562ff3 Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 16 Jan 2024 10:33:34 +0700 Subject: [PATCH 3/5] TF-2426 Add insert link dialog (cherry picked from commit 3e1c64c441c4de535a4e45fb0af901e03d6d095b) --- .../web/insert_link_dialog_builder_style.dart | 90 +++++++++++ .../web/insert_link_dialog_builder.dart | 146 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart create mode 100644 lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart diff --git a/lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart b/lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart new file mode 100644 index 0000000000..65d22693a1 --- /dev/null +++ b/lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart @@ -0,0 +1,90 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class InsertLinkDialogBuilderStyle { + static const double actionOverFlowButtonSpacing = 8.0; + static const double elevation = 10.0; + static const double widthRatio = 0.3; + static const double tittleToFieldSpace = 10.0; + static const double fieldToFieldSpace = 20.0; + static const double buttonRadius = 10.0; + static const double buttonHeight = 40.0; + + static const int maxLines = 1; + + static const TextStyle tittleStyle = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.black + ); + static const TextStyle fieldTitleStyle = TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: Colors.black + ); + static const TextStyle textInputStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.black + ); + static const TextStyle hintTextStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: AppColor.colorHintSearchBar + ); + static const TextStyle buttonCancelTextStyle = TextStyle( + color: AppColor.primaryColor, + fontSize: 17.0, + fontWeight: FontWeight.w500 + ); + static const TextStyle buttonInsertTextStyle = TextStyle( + color: Colors.white, + fontSize: 17.0, + fontWeight: FontWeight.w500 + ); + + static const EdgeInsetsGeometry tittlePadding = EdgeInsets.symmetric( + vertical: 16, + horizontal: 16 + ); + static const EdgeInsetsGeometry contentPadding = EdgeInsets.symmetric( + horizontal: 16 + ); + static const EdgeInsetsGeometry actionsPadding = EdgeInsets.symmetric( + vertical: 8, + horizontal: 16 + ); + static const EdgeInsetsGeometry textInputContentPadding = EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 10.0 + ); + + static const ShapeBorder shape = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)) + ); + static const InputBorder border = OutlineInputBorder( + borderSide: BorderSide( + color: AppColor.colorInputBorderCreateMailbox, + width: 1.0 + ), + borderRadius: BorderRadius.all(Radius.circular(10.0)) + ); + static const InputBorder focusedBorder = OutlineInputBorder( + borderSide: BorderSide( + color: AppColor.primaryColor, + width: 1.0 + ), + borderRadius: BorderRadius.all(Radius.circular(10.0)) + ); + + static const InputDecoration urlFieldDecoration = InputDecoration( + filled: true, + fillColor: Colors.white, + contentPadding: textInputContentPadding, + enabledBorder: border, + border: border, + focusedBorder: focusedBorder, + hintText: 'URL', + hintStyle: hintTextStyle, + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart b/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart new file mode 100644 index 0000000000..df25a569c9 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart @@ -0,0 +1,146 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/checkbox/labeled_checkbox.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/presentation/views/text/text_form_field_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnOpenNewTabChanged = void Function(bool? isOpenNewTab); + +class InsertLinkDialogBuilder { + final BuildContext context; + final ResponsiveUtils responsiveUtils; + final GlobalKey formKey; + final TextEditingController displayTextFieldController; + final TextEditingController linkTextFieldController; + final bool isOpenNewTab; + final FocusNode displayTextFieldFocusNode; + final FocusNode linkTextFieldFocusNode; + final FocusNode openNewTabFocusNode; + final VoidCallback? cancelActionCallback; + final VoidCallback? insertActionCallback; + final OnOpenNewTabChanged? onOpenNewTabChanged; + + const InsertLinkDialogBuilder({ + required this.context, + required this.responsiveUtils, + required this.formKey, + required this.displayTextFieldController, + required this.linkTextFieldController, + required this.isOpenNewTab, + required this.displayTextFieldFocusNode, + required this.linkTextFieldFocusNode, + required this.openNewTabFocusNode, + this.cancelActionCallback, + this.insertActionCallback, + this.onOpenNewTabChanged, + }); + + Future show() async { + return Get.dialog( + PointerInterceptor( + child: AlertDialog( + surfaceTintColor: Colors.white, + title: Text( + AppLocalizations.of(context).insertLink, + textAlign: TextAlign.center, + style: InsertLinkDialogBuilderStyle.tittleStyle + ), + titlePadding: InsertLinkDialogBuilderStyle.tittlePadding, + contentPadding: InsertLinkDialogBuilderStyle.contentPadding, + actionsPadding: InsertLinkDialogBuilderStyle.actionsPadding, + actionsAlignment: MainAxisAlignment.center, + actionsOverflowButtonSpacing: InsertLinkDialogBuilderStyle.actionOverFlowButtonSpacing, + shape: InsertLinkDialogBuilderStyle.shape, + scrollable: true, + elevation: InsertLinkDialogBuilderStyle.elevation, + content: SizedBox( + width: responsiveUtils.getSizeScreenWidth(context) * InsertLinkDialogBuilderStyle.widthRatio, + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).textToDisplay, + style: InsertLinkDialogBuilderStyle.fieldTitleStyle, + ), + const SizedBox(height: InsertLinkDialogBuilderStyle.tittleToFieldSpace), + TextFieldBuilder( + controller: displayTextFieldController, + focusNode: displayTextFieldFocusNode, + textInputAction: TextInputAction.next, + maxLines: InsertLinkDialogBuilderStyle.maxLines, + textStyle: InsertLinkDialogBuilderStyle.textInputStyle, + decoration: InputDecoration( + filled: true, + fillColor: Colors.white, + contentPadding: InsertLinkDialogBuilderStyle.textInputContentPadding, + enabledBorder: InsertLinkDialogBuilderStyle.border, + border: InsertLinkDialogBuilderStyle.border, + focusedBorder: InsertLinkDialogBuilderStyle.focusedBorder, + hintText: AppLocalizations.of(context).textToDisplay, + hintStyle: InsertLinkDialogBuilderStyle.hintTextStyle, + ), + ), + const SizedBox(height: InsertLinkDialogBuilderStyle.fieldToFieldSpace), + Text( + AppLocalizations.of(context).toWhatURLShouldThisLinkGo, + style: InsertLinkDialogBuilderStyle.fieldTitleStyle, + ), + const SizedBox(height: InsertLinkDialogBuilderStyle.tittleToFieldSpace), + TextFormFieldBuilder( + controller: linkTextFieldController, + focusNode: linkTextFieldFocusNode, + textInputAction: TextInputAction.done, + textStyle: InsertLinkDialogBuilderStyle.textInputStyle, + decoration: InsertLinkDialogBuilderStyle.urlFieldDecoration, + validator: (String? value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of(context).pleaseEnterURL; + } + return null; + }, + ), + const SizedBox(height: InsertLinkDialogBuilderStyle.fieldToFieldSpace), + LabeledCheckbox( + label: AppLocalizations.of(context).openInNewTab, + value: isOpenNewTab, + onChanged: onOpenNewTabChanged, + focusNode: openNewTabFocusNode, + contentPadding: EdgeInsets.zero, + activeColor: AppColor.primaryColor, + ) + ], + ), + ), + ), + actions: [ + buildButtonWrapText( + AppLocalizations.of(context).cancel, + radius: InsertLinkDialogBuilderStyle.buttonRadius, + height: InsertLinkDialogBuilderStyle.buttonHeight, + bgColor: AppColor.colorBgSearchBar, + textStyle: InsertLinkDialogBuilderStyle.buttonCancelTextStyle, + onTap: cancelActionCallback, + ), + buildButtonWrapText( + AppLocalizations.of(context).insert, + radius: InsertLinkDialogBuilderStyle.buttonRadius, + height: InsertLinkDialogBuilderStyle.buttonHeight, + textStyle: InsertLinkDialogBuilderStyle.buttonInsertTextStyle, + bgColor: AppColor.primaryColor, + onTap: insertActionCallback, + ), + ], + ) + ) + ); + } +} \ No newline at end of file From 42a19fd91afc12c9097dd77624e33a72275fd780 Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 16 Jan 2024 10:34:00 +0700 Subject: [PATCH 4/5] TF-2426 Add `onEditLink` callback to editor widget --- .../composer/presentation/view/web/web_editor_view.dart | 8 ++++++++ .../presentation/widgets/web/web_editor_widget.dart | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index 5da136ef90..b40c7842c4 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -30,6 +30,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { final double? width; final double? height; final VoidCallback? onDragEnter; + final OnEditLink? onEditLink; const WebEditorView({ super.key, @@ -47,6 +48,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { this.width, this.height, this.onDragEnter, + this.onEditLink, }); @override @@ -73,6 +75,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); case EmailActionType.editDraft: case EmailActionType.editSendingEmail: @@ -98,6 +101,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ), (success) { if (success is GetEmailContentLoading) { @@ -123,6 +127,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); } } @@ -155,6 +160,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); }, (success) { @@ -183,6 +189,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); } } @@ -202,6 +209,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); } } diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index a2ac28b523..97b97af1bc 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -12,6 +12,7 @@ typedef OnInitialContentEditorAction = Function(String text); typedef OnMouseDownEditorAction = Function(BuildContext context); typedef OnEditorSettingsChange = Function(EditorSettings settings); typedef OnEditorTextSizeChanged = Function(int? size); +typedef OnEditLink = Function(String? text, String? url, bool? isOpenNewTab, String linkTagId)?; class WebEditorWidget extends StatefulWidget { @@ -28,6 +29,7 @@ class WebEditorWidget extends StatefulWidget { final double? width; final double? height; final VoidCallback? onDragEnter; + final OnEditLink? onEditLink; const WebEditorWidget({ super.key, @@ -44,6 +46,7 @@ class WebEditorWidget extends StatefulWidget { this.width, this.height, this.onDragEnter, + this.onEditLink, }); @override @@ -176,6 +179,7 @@ class _WebEditorState extends State { ), onDragEnter: widget.onDragEnter, onDragLeave: () {}, + onEditLink: widget.onEditLink ), ); } From 50af5135c7a0d9251f9239e72d8d3fba402d5e4e Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 10 Apr 2024 11:39:28 +0700 Subject: [PATCH 5/5] TF-2426 Add insert link dialog to composer web --- .../views/text/text_form_field_builder.dart | 37 +++- .../presentation/composer_controller.dart | 22 ++ .../presentation/composer_view_web.dart | 3 + ...t => insert_link_dialog_widget_style.dart} | 2 +- .../web/insert_link_dialog_builder.dart | 146 ------------- .../web/insert_link_dialog_widget.dart | 194 ++++++++++++++++++ pubspec.lock | 4 +- pubspec.yaml | 2 +- 8 files changed, 259 insertions(+), 151 deletions(-) rename lib/features/composer/presentation/styles/web/{insert_link_dialog_builder_style.dart => insert_link_dialog_widget_style.dart} (98%) delete mode 100644 lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart create mode 100644 lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart diff --git a/core/lib/presentation/views/text/text_form_field_builder.dart b/core/lib/presentation/views/text/text_form_field_builder.dart index 1d6407e30d..a542d61a21 100644 --- a/core/lib/presentation/views/text/text_form_field_builder.dart +++ b/core/lib/presentation/views/text/text_form_field_builder.dart @@ -79,6 +79,42 @@ class _TextFieldFormBuilderState extends State { @override Widget build(BuildContext context) { + if (widget.validator != null) { + return TextFormField( + key: widget.key, + controller: _controller, + cursorColor: widget.cursorColor, + autocorrect: widget.autocorrect, + textInputAction: widget.textInputAction, + decoration: widget.decoration, + maxLines: widget.maxLines, + minLines: widget.minLines, + keyboardAppearance: widget.keyboardAppearance, + style: widget.textStyle, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + autofocus: widget.autoFocus, + focusNode: widget.focusNode, + textDirection: _textDirection, + readOnly: widget.readOnly, + mouseCursor: widget.mouseCursor, + autofillHints: widget.autofillHints, + onChanged: (value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + }, + onFieldSubmitted: widget.onTextSubmitted, + onTap: widget.onTap, + validator: widget.validator, + ); + } return TextField( key: widget.key, controller: _controller, @@ -111,7 +147,6 @@ class _TextFieldFormBuilderState extends State { }, onSubmitted: widget.onTextSubmitted, onTap: widget.onTap, - validator: widget.validator, ); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index ab5a517352..ba35661942 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -59,6 +59,7 @@ import 'package:tmail_ui_user/features/composer/presentation/styles/composer_sty import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; @@ -2167,4 +2168,25 @@ class ComposerController extends BaseController with DragDropFileMixin { ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; bccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; } + + void onEditLinkAction( + BuildContext context, + String? text, + String? url, + bool? isOpenNewTab, + String linkTagId + ) async { + Get.dialog( + PointerInterceptor( + child: InsertLinkDialogWidget( + responsiveUtils: responsiveUtils, + editorController: richTextWebController?.editorController, + linkTagId: linkTagId, + displayText: text ?? url ?? '', + link: url ?? '', + openNewTab: isOpenNewTab ?? true, + ) + ) + ); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index efe72a1680..b4e60ae232 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -191,6 +191,7 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onEditLink: (text, url, isOpenNewTab, linkTagId) => controller.onEditLinkAction(context, text, url, isOpenNewTab, linkTagId), )), ), ), @@ -432,6 +433,7 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onEditLink: (text, url, isOpenNewTab, linkTagId) => controller.onEditLinkAction(context, text, url, isOpenNewTab, linkTagId), ); }), ), @@ -694,6 +696,7 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onEditLink: (text, url, isOpenNewTab, linkTagId) => controller.onEditLinkAction(context, text, url, isOpenNewTab, linkTagId), )), ), ), diff --git a/lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart b/lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart similarity index 98% rename from lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart rename to lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart index 65d22693a1..4ff7f09b41 100644 --- a/lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart +++ b/lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart @@ -1,7 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; -class InsertLinkDialogBuilderStyle { +class InsertLinkDialogWidgetStyle { static const double actionOverFlowButtonSpacing = 8.0; static const double elevation = 10.0; static const double widthRatio = 0.3; diff --git a/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart b/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart deleted file mode 100644 index df25a569c9..0000000000 --- a/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:core/presentation/views/checkbox/labeled_checkbox.dart'; -import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/presentation/views/text/text_form_field_builder.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnOpenNewTabChanged = void Function(bool? isOpenNewTab); - -class InsertLinkDialogBuilder { - final BuildContext context; - final ResponsiveUtils responsiveUtils; - final GlobalKey formKey; - final TextEditingController displayTextFieldController; - final TextEditingController linkTextFieldController; - final bool isOpenNewTab; - final FocusNode displayTextFieldFocusNode; - final FocusNode linkTextFieldFocusNode; - final FocusNode openNewTabFocusNode; - final VoidCallback? cancelActionCallback; - final VoidCallback? insertActionCallback; - final OnOpenNewTabChanged? onOpenNewTabChanged; - - const InsertLinkDialogBuilder({ - required this.context, - required this.responsiveUtils, - required this.formKey, - required this.displayTextFieldController, - required this.linkTextFieldController, - required this.isOpenNewTab, - required this.displayTextFieldFocusNode, - required this.linkTextFieldFocusNode, - required this.openNewTabFocusNode, - this.cancelActionCallback, - this.insertActionCallback, - this.onOpenNewTabChanged, - }); - - Future show() async { - return Get.dialog( - PointerInterceptor( - child: AlertDialog( - surfaceTintColor: Colors.white, - title: Text( - AppLocalizations.of(context).insertLink, - textAlign: TextAlign.center, - style: InsertLinkDialogBuilderStyle.tittleStyle - ), - titlePadding: InsertLinkDialogBuilderStyle.tittlePadding, - contentPadding: InsertLinkDialogBuilderStyle.contentPadding, - actionsPadding: InsertLinkDialogBuilderStyle.actionsPadding, - actionsAlignment: MainAxisAlignment.center, - actionsOverflowButtonSpacing: InsertLinkDialogBuilderStyle.actionOverFlowButtonSpacing, - shape: InsertLinkDialogBuilderStyle.shape, - scrollable: true, - elevation: InsertLinkDialogBuilderStyle.elevation, - content: SizedBox( - width: responsiveUtils.getSizeScreenWidth(context) * InsertLinkDialogBuilderStyle.widthRatio, - child: Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context).textToDisplay, - style: InsertLinkDialogBuilderStyle.fieldTitleStyle, - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.tittleToFieldSpace), - TextFieldBuilder( - controller: displayTextFieldController, - focusNode: displayTextFieldFocusNode, - textInputAction: TextInputAction.next, - maxLines: InsertLinkDialogBuilderStyle.maxLines, - textStyle: InsertLinkDialogBuilderStyle.textInputStyle, - decoration: InputDecoration( - filled: true, - fillColor: Colors.white, - contentPadding: InsertLinkDialogBuilderStyle.textInputContentPadding, - enabledBorder: InsertLinkDialogBuilderStyle.border, - border: InsertLinkDialogBuilderStyle.border, - focusedBorder: InsertLinkDialogBuilderStyle.focusedBorder, - hintText: AppLocalizations.of(context).textToDisplay, - hintStyle: InsertLinkDialogBuilderStyle.hintTextStyle, - ), - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.fieldToFieldSpace), - Text( - AppLocalizations.of(context).toWhatURLShouldThisLinkGo, - style: InsertLinkDialogBuilderStyle.fieldTitleStyle, - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.tittleToFieldSpace), - TextFormFieldBuilder( - controller: linkTextFieldController, - focusNode: linkTextFieldFocusNode, - textInputAction: TextInputAction.done, - textStyle: InsertLinkDialogBuilderStyle.textInputStyle, - decoration: InsertLinkDialogBuilderStyle.urlFieldDecoration, - validator: (String? value) { - if (value == null || value.isEmpty) { - return AppLocalizations.of(context).pleaseEnterURL; - } - return null; - }, - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.fieldToFieldSpace), - LabeledCheckbox( - label: AppLocalizations.of(context).openInNewTab, - value: isOpenNewTab, - onChanged: onOpenNewTabChanged, - focusNode: openNewTabFocusNode, - contentPadding: EdgeInsets.zero, - activeColor: AppColor.primaryColor, - ) - ], - ), - ), - ), - actions: [ - buildButtonWrapText( - AppLocalizations.of(context).cancel, - radius: InsertLinkDialogBuilderStyle.buttonRadius, - height: InsertLinkDialogBuilderStyle.buttonHeight, - bgColor: AppColor.colorBgSearchBar, - textStyle: InsertLinkDialogBuilderStyle.buttonCancelTextStyle, - onTap: cancelActionCallback, - ), - buildButtonWrapText( - AppLocalizations.of(context).insert, - radius: InsertLinkDialogBuilderStyle.buttonRadius, - height: InsertLinkDialogBuilderStyle.buttonHeight, - textStyle: InsertLinkDialogBuilderStyle.buttonInsertTextStyle, - bgColor: AppColor.primaryColor, - onTap: insertActionCallback, - ), - ], - ) - ) - ); - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart b/lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart new file mode 100644 index 0000000000..819700e34c --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart @@ -0,0 +1,194 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/checkbox/labeled_checkbox.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/presentation/views/text/text_form_field_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class InsertLinkDialogWidget extends StatefulWidget { + final ResponsiveUtils responsiveUtils; + final HtmlEditorController? editorController; + final String linkTagId; + final String displayText; + final String link; + final bool openNewTab; + + const InsertLinkDialogWidget({ + super.key, + required this.responsiveUtils, + required this.editorController, + required this.linkTagId, + required this.displayText, + required this.link, + required this.openNewTab, + }); + + @override + State createState() => _InsertLinkDialogState(); +} + +class _InsertLinkDialogState extends State { + late FocusNode _displayTextFieldFocusNode; + late FocusNode _linkTextFieldFocusNode; + late FocusNode _openNewTabFocusNode; + late TextEditingController _displayTextFieldController; + late TextEditingController _linkTextFieldController; + late HtmlToolbarOptions _htmlToolbarOptions; + late GlobalKey _formKey; + late bool _openNewTab; + + @override + void initState() { + super.initState(); + _displayTextFieldFocusNode = FocusNode(); + _linkTextFieldFocusNode = FocusNode(); + _openNewTabFocusNode = FocusNode(); + _htmlToolbarOptions = const HtmlToolbarOptions(); + _formKey = GlobalKey(); + _displayTextFieldController = TextEditingController(text: widget.displayText); + _linkTextFieldController = TextEditingController(text: widget.link); + _openNewTab = widget.openNewTab; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + surfaceTintColor: Colors.white, + title: Text( + AppLocalizations.of(context).insertLink, + textAlign: TextAlign.center, + style: InsertLinkDialogWidgetStyle.tittleStyle + ), + titlePadding: InsertLinkDialogWidgetStyle.tittlePadding, + contentPadding: InsertLinkDialogWidgetStyle.contentPadding, + actionsPadding: InsertLinkDialogWidgetStyle.actionsPadding, + actionsAlignment: MainAxisAlignment.center, + actionsOverflowButtonSpacing: InsertLinkDialogWidgetStyle.actionOverFlowButtonSpacing, + shape: InsertLinkDialogWidgetStyle.shape, + scrollable: true, + elevation: InsertLinkDialogWidgetStyle.elevation, + content: SizedBox( + width: widget.responsiveUtils.getSizeScreenWidth(context) * InsertLinkDialogWidgetStyle.widthRatio, + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).textToDisplay, + style: InsertLinkDialogWidgetStyle.fieldTitleStyle, + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.tittleToFieldSpace), + TextFieldBuilder( + controller: _displayTextFieldController, + focusNode: _displayTextFieldFocusNode, + textInputAction: TextInputAction.next, + maxLines: InsertLinkDialogWidgetStyle.maxLines, + textStyle: InsertLinkDialogWidgetStyle.textInputStyle, + decoration: InputDecoration( + filled: true, + fillColor: Colors.white, + contentPadding: InsertLinkDialogWidgetStyle.textInputContentPadding, + enabledBorder: InsertLinkDialogWidgetStyle.border, + border: InsertLinkDialogWidgetStyle.border, + focusedBorder: InsertLinkDialogWidgetStyle.focusedBorder, + hintText: AppLocalizations.of(context).textToDisplay, + hintStyle: InsertLinkDialogWidgetStyle.hintTextStyle, + ), + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.fieldToFieldSpace), + Text( + AppLocalizations.of(context).toWhatURLShouldThisLinkGo, + style: InsertLinkDialogWidgetStyle.fieldTitleStyle, + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.tittleToFieldSpace), + TextFormFieldBuilder( + controller: _linkTextFieldController, + focusNode: _linkTextFieldFocusNode, + textInputAction: TextInputAction.done, + textStyle: InsertLinkDialogWidgetStyle.textInputStyle, + decoration: InsertLinkDialogWidgetStyle.urlFieldDecoration, + validator: (String? value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of(context).pleaseEnterURL; + } + return null; + }, + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.fieldToFieldSpace), + LabeledCheckbox( + label: AppLocalizations.of(context).openInNewTab, + value: _openNewTab, + onChanged: (value) { + if (value != null) { + setState(() { + _openNewTab = value; + }); + } + }, + focusNode: _openNewTabFocusNode, + contentPadding: EdgeInsets.zero, + activeColor: AppColor.primaryColor, + ) + ], + ), + ), + ), + actions: [ + buildButtonWrapText( + AppLocalizations.of(context).cancel, + radius: InsertLinkDialogWidgetStyle.buttonRadius, + height: InsertLinkDialogWidgetStyle.buttonHeight, + bgColor: AppColor.colorBgSearchBar, + textStyle: InsertLinkDialogWidgetStyle.buttonCancelTextStyle, + onTap: () => Get.back(), + ), + buildButtonWrapText( + AppLocalizations.of(context).insert, + radius: InsertLinkDialogWidgetStyle.buttonRadius, + height: InsertLinkDialogWidgetStyle.buttonHeight, + textStyle: InsertLinkDialogWidgetStyle.buttonInsertTextStyle, + bgColor: AppColor.primaryColor, + onTap: () async { + if (_formKey.currentState != null && _formKey.currentState!.validate()) { + var proceed = await _htmlToolbarOptions.linkInsertInterceptor?.call( + _displayTextFieldController.text.isEmpty + ? _linkTextFieldController.text + : _displayTextFieldController.text, + _linkTextFieldController.text, + _openNewTab, + ) ?? true; + if (proceed) { + widget.editorController?.updateLink( + _displayTextFieldController.text.isEmpty + ? _linkTextFieldController.text + : _displayTextFieldController.text, + _linkTextFieldController.text, + _openNewTab, + widget.linkTagId + ); + } + Get.back(); + } + }, + ), + ], + ); + } + + @override + void dispose() { + _displayTextFieldFocusNode.dispose(); + _linkTextFieldFocusNode.dispose(); + _openNewTabFocusNode.dispose(); + _displayTextFieldController.dispose(); + _linkTextFieldController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 942919c5a5..5c03661a9f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1058,8 +1058,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_supported - resolved-ref: "978886d768e6540fc7dbe016dd83733c56ffb220" + ref: cherry-pick-insert-link-dialog + resolved-ref: "0286c90e75ae903bbb06ceab425f7cc8496d910b" url: "https://github.com/linagora/html-editor-enhanced.git" source: git version: "2.5.1" diff --git a/pubspec.yaml b/pubspec.yaml index dd8335bff2..fe26b690a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: html_editor_enhanced: git: url: https://github.com/linagora/html-editor-enhanced.git - ref: cnb_supported + ref: cherry-pick-insert-link-dialog # TODO: We will change it when the PR in upstream repository will be merged # https://github.com/linagora/jmap-dart-client/pull/87