From 58d22cbc0bb9632e3a6612b17dd4191acb6587ae Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 11 Jul 2025 19:36:14 +0700 Subject: [PATCH] Apply new style for create identity view on desktop Signed-off-by: dab246 --- .../extensions/color_extension.dart | 1 + .../custom_icon_labeled_checkbox.dart | 76 +- .../views/text/input_decoration_builder.dart | 4 +- .../text/type_ahead_form_field_builder.dart | 26 +- ...efault_email_address_drop_down_button.dart | 151 ++++ .../default_horizontal_field_widget.dart | 39 + .../web/toolbar_rich_text_builder.dart | 459 +++++----- .../presentation/identity_creator_view.dart | 807 +++++++----------- .../identity_creator_form_bottom_view.dart | 132 +++ ...identity_creator_form_desktop_builder.dart | 77 ++ .../identity_creator_form_mobile_builder.dart | 62 ++ .../identity_creator_form_title_widget.dart | 29 + .../identity_drop_list_field_builder.dart | 103 --- .../identity_field_no_editable_builder.dart | 51 -- .../identity_input_decoration_builder.dart | 3 +- .../widgets/identity_input_field_builder.dart | 66 -- ...ty_input_with_drop_list_field_builder.dart | 88 -- ...identity_signature_input_field_widget.dart | 212 +++++ .../model/identity_action_type.dart | 22 +- lib/l10n/intl_messages.arb | 12 + lib/main/localizations/app_localizations.dart | 14 + 21 files changed, 1368 insertions(+), 1066 deletions(-) create mode 100644 lib/features/base/widget/default_field/default_email_address_drop_down_button.dart create mode 100644 lib/features/base/widget/default_field/default_horizontal_field_widget.dart create mode 100644 lib/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart create mode 100644 lib/features/identity_creator/presentation/widgets/identity_creator_form_desktop_builder.dart create mode 100644 lib/features/identity_creator/presentation/widgets/identity_creator_form_mobile_builder.dart create mode 100644 lib/features/identity_creator/presentation/widgets/identity_creator_form_title_widget.dart create mode 100644 lib/features/identity_creator/presentation/widgets/identity_signature_input_field_widget.dart diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index cd450c208..a0fef1d3b 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -259,6 +259,7 @@ extension AppColor on Color { static const iconFolder = Color(0xFF297EF2); static const folderDivider = Color(0xFFE4E8EC); static const gray424244 = Color(0xFF424244); + static const lightGrayF4F4F4 = Color(0xFFF4F4F4); static const redFF3347 = Color(0xFFFF3347); static const gray686E76 = Color(0xFF686E76); static const lightGrayEBEDF0 = Color(0xFFEBEDF0); diff --git a/core/lib/presentation/views/checkbox/custom_icon_labeled_checkbox.dart b/core/lib/presentation/views/checkbox/custom_icon_labeled_checkbox.dart index ca827983c..7b221856e 100644 --- a/core/lib/presentation/views/checkbox/custom_icon_labeled_checkbox.dart +++ b/core/lib/presentation/views/checkbox/custom_icon_labeled_checkbox.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/checkbox/labeled_checkbox.dart'; +import 'package:core/presentation/views/semantics/checkbox_semantics.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -7,7 +8,8 @@ class CustomIconLabeledCheckbox extends LabeledCheckbox { final String svgIconPath; final String selectedSvgIconPath; - final FocusNode focusNode; + final FocusNode? focusNode; + final String? semanticsLabel; const CustomIconLabeledCheckbox({ super.key, @@ -15,31 +17,63 @@ class CustomIconLabeledCheckbox extends LabeledCheckbox { required super.onChanged, required this.svgIconPath, required this.selectedSvgIconPath, - required this.focusNode, + this.focusNode, + this.semanticsLabel, super.value, super.gap = 16.0, super.padding, }); @override - Widget get buildCheckboxWidget => FocusableActionDetector( - focusNode: focusNode, - autofocus: false, - child: Material( - type: MaterialType.transparency, - child: InkWell( - canRequestFocus: true, - focusColor: AppColor.colorMailboxHovered, - borderRadius: const BorderRadius.all(Radius.circular(20)), - onTap: () => onChanged(!(value)), - child: SvgPicture.asset( - value ? selectedSvgIconPath : svgIconPath, - width: 20, - height: 20, - colorFilter: AppColor.primaryColor.asFilter(), - fit: BoxFit.fill, + Widget get buildCheckboxWidget { + Widget bodyWidget; + + if (focusNode != null) { + bodyWidget = FocusableActionDetector( + focusNode: focusNode, + autofocus: false, + child: Material( + type: MaterialType.transparency, + child: InkWell( + canRequestFocus: true, + focusColor: AppColor.colorMailboxHovered, + borderRadius: const BorderRadius.all(Radius.circular(20)), + onTap: () => onChanged(!(value)), + child: SvgPicture.asset( + value ? selectedSvgIconPath : svgIconPath, + width: 20, + height: 20, + colorFilter: AppColor.primaryColor.asFilter(), + fit: BoxFit.fill, + ), + ), ), - ), - ), - ); + ); + } else { + bodyWidget = Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(20)), + onTap: () => onChanged(!(value)), + child: SvgPicture.asset( + value ? selectedSvgIconPath : svgIconPath, + width: 20, + height: 20, + colorFilter: AppColor.primaryColor.asFilter(), + fit: BoxFit.fill, + ), + ), + ); + } + + if (semanticsLabel != null) { + return CheckboxSemantics( + label: semanticsLabel!, + value: value, + child: bodyWidget, + ); + } else { + return bodyWidget; + } + } } diff --git a/core/lib/presentation/views/text/input_decoration_builder.dart b/core/lib/presentation/views/text/input_decoration_builder.dart index 5b5a7dcfc..d07856ff8 100644 --- a/core/lib/presentation/views/text/input_decoration_builder.dart +++ b/core/lib/presentation/views/text/input_decoration_builder.dart @@ -63,8 +63,8 @@ abstract class InputDecorationBuilder { errorTextStyle = newStyle; } - void setFocusBorder(OutlineInputBorder focusBorder) { - focusBorder = focusBorder; + void setFocusBorder(OutlineInputBorder newFocusBorder) { + focusBorder = newFocusBorder; } InputDecoration build() { diff --git a/core/lib/presentation/views/text/type_ahead_form_field_builder.dart b/core/lib/presentation/views/text/type_ahead_form_field_builder.dart index 3d3a63eec..3aa80b5cb 100644 --- a/core/lib/presentation/views/text/type_ahead_form_field_builder.dart +++ b/core/lib/presentation/views/text/type_ahead_form_field_builder.dart @@ -4,6 +4,8 @@ import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; +typedef SuggestionBoxDecorationBuilder = Widget Function(Widget child); + class TypeAheadFormFieldBuilder extends StatefulWidget { final TextDirection textDirection; @@ -11,7 +13,7 @@ class TypeAheadFormFieldBuilder extends StatefulWidget { final SuggestionsCallback suggestionsCallback; final ItemBuilder itemBuilder; final SuggestionSelectionCallback onSuggestionSelected; - final Widget? suggestionsBoxDecoration; + final SuggestionBoxDecorationBuilder? suggestionsBoxDecoration; final WidgetBuilder? noItemsFoundBuilder; final bool hideOnEmpty; final bool hideOnError; @@ -26,6 +28,7 @@ class TypeAheadFormFieldBuilder extends StatefulWidget { final TextInputType keyboardType; final InputDecoration decoration; final Color cursorColor; + final TextStyle? textStyle; const TypeAheadFormFieldBuilder({ super.key, @@ -47,6 +50,7 @@ class TypeAheadFormFieldBuilder extends StatefulWidget { this.cursorColor = AppColor.primaryColor, this.autofillHints, this.textInputAction, + this.textStyle, this.onTextChange, this.onTextSubmitted, }); @@ -73,13 +77,14 @@ class _TypeAheadFormFieldBuilderState extends State extends State emailAddresses; + final EmailAddress? emailAddressSelected; + final OnEmailAddressSelected onEmailAddressSelected; + final bool isEnabled; + + const DefaultEmailAddressDropDownButton({ + Key? key, + required this.imagePaths, + required this.emailAddresses, + required this.onEmailAddressSelected, + this.emailAddressSelected, + this.isEnabled = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (isEnabled) { + return DropdownButtonHideUnderline( + child: PointerInterceptor( + child: DropdownButton2( + isExpanded: true, + items: emailAddresses.map(_buildItemMenu).toList(), + value: emailAddressSelected, + customButton: Container( + height: 40, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10)), + border: Border.all( + color: AppColor.m3Neutral90, + width: 1, + ), + color: Colors.white, + ), + padding: const EdgeInsetsDirectional.only(start: 12, end: 8), + child: Row(children: [ + Expanded( + child: Text( + emailAddressSelected?.emailAddress ?? '', + style: ThemeUtils.textStyleBodyBody3( + color: AppColor.m3SurfaceBackground, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + SvgPicture.asset(imagePaths.icDropDown) + ]), + ), + onChanged: (value) { + if (value != null) { + onEmailAddressSelected.call(value); + } + }, + dropdownStyleData: DropdownStyleData( + maxHeight: 332, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: Colors.white, + border: Border.all(color: AppColor.m3Tertiary60), + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 24) + ], + ), + padding: const EdgeInsets.all(12), + elevation: 0, + offset: const Offset(0.0, -3.0), + scrollbarTheme: ScrollbarThemeData( + radius: const Radius.circular(40), + thickness: WidgetStateProperty.all(6), + thumbVisibility: WidgetStateProperty.all(true), + ), + ), + menuItemStyleData: const MenuItemStyleData( + height: 44, + padding: EdgeInsets.symmetric(horizontal: 12), + ), + ), + ), + ); + } else { + return Container( + height: 40, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10)), + border: Border.all( + color: AppColor.m3Neutral90, + width: 1, + ), + color: AppColor.lightGrayF4F4F4, + ), + padding: const EdgeInsetsDirectional.only(start: 12, end: 8), + alignment: AlignmentDirectional.centerStart, + child: Text( + emailAddressSelected?.emailAddress ?? '', + style: ThemeUtils.textStyleBodyBody3( + color: AppColor.m3SurfaceBackground, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + } + } + + DropdownMenuItem _buildItemMenu(EmailAddress emailAddress) { + return DropdownMenuItem( + value: emailAddress, + enabled: emailAddress != emailAddressSelected, + child: PointerInterceptor( + child: Row( + children: [ + Expanded( + child: Text( + emailAddress.emailAddress, + style: ThemeUtils.textStyleInter400.copyWith( + fontSize: 15, + height: 20 / 15, + letterSpacing: -0.15, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (emailAddress == emailAddressSelected) + SvgPicture.asset( + imagePaths.icChecked, + width: 20, + height: 20, + fit: BoxFit.fill, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/base/widget/default_field/default_horizontal_field_widget.dart b/lib/features/base/widget/default_field/default_horizontal_field_widget.dart new file mode 100644 index 000000000..a4ca50684 --- /dev/null +++ b/lib/features/base/widget/default_field/default_horizontal_field_widget.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_label_field_widget.dart'; + +class DefaultHorizontalFieldWidget extends StatelessWidget { + + final String label; + final Widget child; + final double labelMaxWidth; + final bool useHeight; + + const DefaultHorizontalFieldWidget({ + super.key, + required this.label, + required this.child, + this.labelMaxWidth = 112, + this.useHeight = false, + }); + + @override + Widget build(BuildContext context) { + final bodyWidget = Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: labelMaxWidth, + child: DefaultLabelFieldWidget(label: label), + ), + const SizedBox(width: 12), + Expanded(child: child), + ], + ); + + if (useHeight) { + return SizedBox(height: 40, child: bodyWidget); + } else { + return bodyWidget; + } + } +} diff --git a/lib/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart b/lib/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart index c9a560c30..b01198720 100644 --- a/lib/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart +++ b/lib/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart @@ -27,16 +27,16 @@ class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin final ResponsiveUtils _responsiveUtils = Get.find(); final EdgeInsetsGeometry? padding; final List? extendedOption; - final AlignmentGeometry? alignment; final Decoration? decoration; + final bool isHorizontalArrange; ToolbarRichTextWebBuilder({ Key? key, required this.richTextWebController, this.padding, this.extendedOption, - this.alignment, this.decoration, + this.isHorizontalArrange = false, }) : super(key: key); @override @@ -45,222 +45,255 @@ class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin final codeViewEnabled = richTextWebController.codeViewEnabled; final opacity = codeViewEnabled ? 0.5 : 1.0; + final listAction = [ + if (extendedOption?.isNotEmpty == true) ...extendedOption!, + AbsorbPointer( + absorbing: codeViewEnabled, + child: DropDownMenuHeaderStyleWidget( + icon: buildWrapIconStyleText( + isSelected: richTextWebController.isMenuHeaderStyleOpen, + icon: SvgPicture.asset( + RichTextStyleType.headerStyle.getIcon(_imagePaths), + colorFilter: AppColor.colorDefaultRichTextButton + .withValues(alpha: opacity) + .asFilter(), + fit: BoxFit.fill, + ), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), + tooltip: RichTextStyleType.headerStyle.getTooltipButton(context), + ), + items: HeaderStyleType.values, + onMenuStateChange: (isOpen) { + final newStatus = isOpen + ? DropdownMenuFontStatus.open + : DropdownMenuFontStatus.closed; + richTextWebController.menuHeaderStyleStatus.value = newStatus; + }, + onChanged: richTextWebController.applyHeaderStyle, + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: DropdownMenuFontSizeWidget( + onChanged: richTextWebController.applyNewFontSize, + selectedFontSize: richTextWebController.selectedFontSize.value, + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: SizedBox( + width: 130, + child: DropDownButtonWidget( + items: FontNameType.values, + itemSelected: richTextWebController.selectedFontName.value, + onChanged: (newFont) => + richTextWebController.applyNewFontStyle(newFont), + onMenuStateChange: (isOpen) { + final newStatus = isOpen + ? DropdownMenuFontStatus.open + : DropdownMenuFontStatus.closed; + richTextWebController.menuFontStatus.value = newStatus; + }, + heightItem: 40, + sizeIconChecked: 16, + radiusButton: 8, + opacity: opacity, + dropdownWidth: 200, + colorButton: richTextWebController.isMenuFontOpen + ? AppColor.colorBackgroundWrapIconStyleCode + : Colors.white, + iconArrowDown: SvgPicture.asset(_imagePaths.icStyleArrowDown), + tooltip: RichTextStyleType.fontName.getTooltipButton(context), + supportSelectionIcon: true, + ), + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildWrapIconStyleText( + icon: buildIconWithTooltip( + path: RichTextStyleType.textColor.getIcon(_imagePaths), + color: richTextWebController.selectedTextColor.value, + tooltip: RichTextStyleType.textColor.getTooltipButton(context), + opacity: opacity, + ), + onTap: () => richTextWebController.applyRichTextStyle( + context, + RichTextStyleType.textColor, + ), + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildWrapIconStyleText( + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 7), + spacing: 3, + icon: buildIconColorBackgroundText( + iconData: RichTextStyleType.textBackgroundColor.getIconData(), + colorSelected: + richTextWebController.selectedTextBackgroundColor.value, + tooltip: RichTextStyleType.textBackgroundColor + .getTooltipButton(context), + opacity: opacity, + ), + onTap: () => richTextWebController.applyRichTextStyle( + context, + RichTextStyleType.textBackgroundColor, + ), + ), + ), + buildWrapIconStyleText( + hasDropdown: false, + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), + icon: Wrap( + children: [ + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.bold.getIcon(_imagePaths), + isSelected: richTextWebController + .isTextStyleTypeSelected(RichTextStyleType.bold), + tooltip: RichTextStyleType.bold.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle( + context, + RichTextStyleType.bold, + ), + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.italic.getIcon(_imagePaths), + isSelected: richTextWebController + .isTextStyleTypeSelected(RichTextStyleType.italic), + tooltip: RichTextStyleType.italic.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle( + context, + RichTextStyleType.italic, + ), + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.underline.getIcon(_imagePaths), + isSelected: richTextWebController + .isTextStyleTypeSelected(RichTextStyleType.underline), + tooltip: + RichTextStyleType.underline.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle( + context, + RichTextStyleType.underline, + ), + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.strikeThrough.getIcon(_imagePaths), + isSelected: richTextWebController + .isTextStyleTypeSelected(RichTextStyleType.strikeThrough), + tooltip: + RichTextStyleType.strikeThrough.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle( + context, + RichTextStyleType.strikeThrough, + ), + ), + ), + ], + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: PopupMenuOverlayWidget( + controller: richTextWebController.menuParagraphController, + listButtonAction: ParagraphType.values + .map((paragraph) => paragraph.buildButtonWidget( + context, + _imagePaths, + (paragraph) => + richTextWebController.applyParagraphType(paragraph))) + .toList(), + padding: const EdgeInsets.symmetric(horizontal: 5), + iconButton: buildWrapIconStyleText( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), + spacing: 3, + isSelected: richTextWebController.focusMenuParagraph.value, + icon: buildIconWithTooltip( + path: richTextWebController.selectedParagraph.value + .getIcon(_imagePaths), + color: AppColor.colorDefaultRichTextButton, + opacity: opacity, + tooltip: RichTextStyleType.paragraph.getTooltipButton(context), + ), + ), + position: _responsiveUtils.isMobile(context) + ? PreferredPosition.top + : PreferredPosition.bottom, + ), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: PopupMenuOverlayWidget( + controller: richTextWebController.menuOrderListController, + listButtonAction: OrderListType.values + .map((orderType) => orderType.buildButtonWidget( + context, + _imagePaths, + (orderType) => + richTextWebController.applyOrderListType(orderType))) + .toList(), + padding: const EdgeInsets.symmetric(horizontal: 5), + iconButton: buildWrapIconStyleText( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), + spacing: 3, + isSelected: richTextWebController.focusMenuOrderList.value, + icon: buildIconWithTooltip( + path: richTextWebController.selectedOrderList.value + .getIcon(_imagePaths), + color: AppColor.colorDefaultRichTextButton, + opacity: opacity, + tooltip: RichTextStyleType.orderList.getTooltipButton(context), + ), + ), + position: _responsiveUtils.isMobile(context) + ? PreferredPosition.top + : PreferredPosition.bottom, + ), + ), + ]; + + Widget bodyWidget; + + if (isHorizontalArrange) { + bodyWidget = SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const ClampingScrollPhysics(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: listAction, + ), + ); + } else { + bodyWidget = Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: ToolbarRichTextBuilderStyle.itemVerticalSpace, + spacing: ToolbarRichTextBuilderStyle.itemHorizontalSpace, + children: listAction, + ); + } + return PointerInterceptor( child: Container( padding: padding ?? ToolbarRichTextBuilderStyle.padding, decoration: decoration, width: double.infinity, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runSpacing: ToolbarRichTextBuilderStyle.itemVerticalSpace, - spacing: ToolbarRichTextBuilderStyle.itemHorizontalSpace, - children: [ - if (extendedOption?.isNotEmpty == true) - ...extendedOption!, - AbsorbPointer( - absorbing: codeViewEnabled, - child: DropDownMenuHeaderStyleWidget( - icon: buildWrapIconStyleText( - isSelected: richTextWebController.isMenuHeaderStyleOpen, - icon: SvgPicture.asset( - RichTextStyleType.headerStyle.getIcon(_imagePaths), - colorFilter: AppColor.colorDefaultRichTextButton.withValues(alpha: opacity).asFilter(), - fit: BoxFit.fill - ), - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), - tooltip: RichTextStyleType.headerStyle.getTooltipButton(context) - ), - items: HeaderStyleType.values, - onMenuStateChange: (isOpen) { - final newStatus = isOpen - ? DropdownMenuFontStatus.open - : DropdownMenuFontStatus.closed; - richTextWebController.menuHeaderStyleStatus.value = newStatus; - }, - onChanged: richTextWebController.applyHeaderStyle - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: DropdownMenuFontSizeWidget( - onChanged: richTextWebController.applyNewFontSize, - selectedFontSize: richTextWebController.selectedFontSize.value - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: SizedBox( - width: 130, - child: DropDownButtonWidget( - items: FontNameType.values, - itemSelected: richTextWebController.selectedFontName.value, - onChanged: (newFont) => richTextWebController.applyNewFontStyle(newFont), - onMenuStateChange: (isOpen) { - final newStatus = isOpen - ? DropdownMenuFontStatus.open - : DropdownMenuFontStatus.closed; - richTextWebController.menuFontStatus.value = newStatus; - }, - heightItem: 40, - sizeIconChecked: 16, - radiusButton: 8, - opacity: opacity, - dropdownWidth: 200, - colorButton: richTextWebController.isMenuFontOpen - ? AppColor.colorBackgroundWrapIconStyleCode - : Colors.white, - iconArrowDown: SvgPicture.asset(_imagePaths.icStyleArrowDown), - tooltip: RichTextStyleType.fontName.getTooltipButton(context), - supportSelectionIcon: true - ) - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildWrapIconStyleText( - icon: buildIconWithTooltip( - path: RichTextStyleType.textColor.getIcon(_imagePaths), - color: richTextWebController.selectedTextColor.value, - tooltip: RichTextStyleType.textColor.getTooltipButton(context), - opacity: opacity - ), - onTap: () => richTextWebController.applyRichTextStyle( - context, - RichTextStyleType.textColor - ) - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildWrapIconStyleText( - padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 7), - spacing: 3, - icon: buildIconColorBackgroundText( - iconData: RichTextStyleType.textBackgroundColor.getIconData(), - colorSelected: richTextWebController.selectedTextBackgroundColor.value, - tooltip: RichTextStyleType.textBackgroundColor.getTooltipButton(context), - opacity: opacity - ), - onTap: () => richTextWebController.applyRichTextStyle( - context, - RichTextStyleType.textBackgroundColor - ) - ), - ), - buildWrapIconStyleText( - hasDropdown: false, - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), - icon: Wrap(children: [ - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.bold.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.bold), - tooltip: RichTextStyleType.bold.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle( - context, - RichTextStyleType.bold - ) - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.italic.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.italic), - tooltip: RichTextStyleType.italic.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle( - context, - RichTextStyleType.italic - ) - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.underline.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.underline), - tooltip: RichTextStyleType.underline.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle( - context, - RichTextStyleType.underline - ) - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.strikeThrough.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.strikeThrough), - tooltip: RichTextStyleType.strikeThrough.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle( - context, - RichTextStyleType.strikeThrough - ) - ), - ) - ]) - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: PopupMenuOverlayWidget( - controller: richTextWebController.menuParagraphController, - listButtonAction: ParagraphType.values - .map((paragraph) => paragraph.buildButtonWidget( - context, - _imagePaths, - (paragraph) => richTextWebController.applyParagraphType(paragraph))) - .toList(), - padding: const EdgeInsets.symmetric(horizontal: 5), - iconButton: buildWrapIconStyleText( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), - spacing: 3, - isSelected: richTextWebController.focusMenuParagraph.value, - icon: buildIconWithTooltip( - path: richTextWebController.selectedParagraph.value.getIcon(_imagePaths), - color: AppColor.colorDefaultRichTextButton, - opacity: opacity, - tooltip: RichTextStyleType.paragraph.getTooltipButton(context) - ) - ), - position: _responsiveUtils.isMobile(context) - ? PreferredPosition.top - : PreferredPosition.bottom, - ), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: PopupMenuOverlayWidget( - controller: richTextWebController.menuOrderListController, - listButtonAction: OrderListType.values - .map((orderType) => orderType.buildButtonWidget( - context, - _imagePaths, - (orderType) => richTextWebController.applyOrderListType(orderType))) - .toList(), - padding: const EdgeInsets.symmetric(horizontal: 5), - iconButton: buildWrapIconStyleText( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), - spacing: 3, - isSelected: richTextWebController.focusMenuOrderList.value, - icon: buildIconWithTooltip( - path: richTextWebController.selectedOrderList.value.getIcon(_imagePaths), - color: AppColor.colorDefaultRichTextButton, - opacity: opacity, - tooltip: RichTextStyleType.orderList.getTooltipButton(context) - ) - ), - position: _responsiveUtils.isMobile(context) - ? PreferredPosition.top - : PreferredPosition.bottom, - ), - ) - ] - ), + child: bodyWidget, ), ); }); diff --git a/lib/features/identity_creator/presentation/identity_creator_view.dart b/lib/features/identity_creator/presentation/identity_creator_view.dart index bb63d2557..1e22c44bd 100644 --- a/lib/features/identity_creator/presentation/identity_creator_view.dart +++ b/lib/features/identity_creator/presentation/identity_creator_view.dart @@ -1,38 +1,32 @@ import 'dart:math' as math; -import 'package:core/presentation/constants/constants_ui.dart'; import 'package:core/presentation/extensions/capitalize_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/utils/theme_utils.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/presentation/views/responsive/responsive_widget.dart'; -import 'package:core/utils/html/html_utils.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:html_editor_enhanced/html_editor.dart' as html_editor_browser; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_email_address_drop_down_button.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_horizontal_field_widget.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_label_field_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_field_builder.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/insert_image_loading_indicator.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_creator_form_desktop_builder.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_creator_form_mobile_builder.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_signature_input_field_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:tmail_ui_user/main/utils/app_utils.dart'; class IdentityCreatorView extends GetWidget with RichTextButtonMixin { @@ -44,125 +38,285 @@ class IdentityCreatorView extends GetWidget @override Widget build(BuildContext context) { - Widget bodyCreatorView = SingleChildScrollView( - controller: controller.scrollController, - physics: const ClampingScrollPhysics(), - child: Padding( - padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx(() => IdentityInputFieldBuilder( - AppLocalizations.of(context).nameToBeDisplayed, - controller.errorNameIdentity.value, - AppLocalizations.of(context).required, - editingController: controller.inputNameIdentityController, - focusNode: controller.inputNameIdentityFocusNode, - isMandatory: true, - onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value) - )), - const SizedBox(height: 24), - Obx(() { - if (controller.actionType.value == IdentityActionType.create) { - return IdentityDropListFieldBuilder( - controller.imagePaths, - AppLocalizations.of(context).email.inCaps, - controller.emailOfIdentity.value, - controller.listEmailAddressDefault, - onSelectItemDropList: (emailAddress) => controller.updateEmailOfIdentity(context, emailAddress) - ); - } else { - return IdentityFieldNoEditableBuilder( - AppLocalizations.of(context).email.inCaps, - controller.emailOfIdentity.value - ); - } - }), - const SizedBox(height: 24), - Obx(() => IdentityDropListFieldBuilder( - controller.imagePaths, - AppLocalizations.of(context).reply_to, - controller.replyToOfIdentity.value, - controller.listEmailAddressOfReplyTo, - onSelectItemDropList: (emailAddress) => controller.updaterReplyToOfIdentity(context, emailAddress) - )), - const SizedBox(height: 24), - Obx(() => IdentityInputWithDropListFieldBuilder( - AppLocalizations.of(context).bcc_to, - controller.errorBccIdentity.value, - controller.inputBccIdentityController, - focusNode: controller.inputBccIdentityFocusNode, - onSelectedSuggestionAction: (newEmailAddress) { - controller.inputBccIdentityController.text = newEmailAddress?.email ?? ''; - controller.updateBccOfIdentity(newEmailAddress); - }, - onChangeInputSuggestionAction: (pattern) { - controller.validateInputBccAddress(context, pattern); - if (pattern == null || pattern.trim().isEmpty) { - controller.updateBccOfIdentity(null); - } else { - controller.updateBccOfIdentity(EmailAddress(null, pattern)); - } - }, - onSuggestionCallbackAction: controller.getSuggestionEmailAddress - )), - const SizedBox(height: 32), - Text(AppLocalizations.of(context).signature, - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontWeight: FontWeight.normal, - fontSize: 14, - color: AppColor.colorContentEmail, - ), - ), - const SizedBox(height: 8), - LayoutBuilder( - builder: (context, constraintsEditor) { - return Stack( - alignment: Alignment.center, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColor.colorInputBorderCreateMailbox), - ), - padding: const EdgeInsetsDirectional.all(16), - child: _buildSignatureHtmlTemplate(context, constraintsEditor.maxWidth), - ), - Obx(() { - if (controller.draggableAppState.value == DraggableAppState.inActive) { - return const SizedBox.shrink(); - } + final appLocalizations = AppLocalizations.of(context); + Widget bodyCreatorView; - return Positioned.fill( - child: PointerInterceptor( - child: LocalFileDropZoneWidget( - imagePaths: controller.imagePaths, - width: constraintsEditor.maxWidth, - height: constraintsEditor.maxHeight, - margin: EdgeInsets.zero, - onLocalFileDropZoneListener: (details) => - controller.onLocalFileDropZoneListener( - context: context, - details: details, - maxWidth: constraintsEditor.maxWidth, - ), - ) + if (controller.responsiveUtils.isMobile(context)) { + bodyCreatorView = Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 17), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + DefaultLabelFieldWidget( + label: '${appLocalizations.name} (${appLocalizations.required})', + ), + Obx(() => TextFieldBuilder( + onTextChange: (value) => controller.updateNameIdentity( + context, + value, + ), + textInputAction: TextInputAction.next, + autoFocus: true, + maxLines: 1, + textDirection: DirectionUtils.getDirectionByLanguage(context), + controller: controller.inputNameIdentityController, + focusNode: controller.inputNameIdentityFocusNode, + textStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.m3SurfaceBackground, + ), + keyboardType: TextInputType.text, + semanticLabel: 'Identity input field', + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + horizontal: 12, + vertical: 15, + )) + ..setErrorText(controller.errorNameIdentity.value) + ..setHintText(appLocalizations.enterName) + ..setHintStyle(ThemeUtils.textStyleBodyBody3( + color: AppColor.m3Tertiary, + )) + ).build(), + )), + const SizedBox(height: 15), + DefaultLabelFieldWidget(label: appLocalizations.email.inCaps), + Obx(() => DefaultEmailAddressDropDownButton( + imagePaths: controller.imagePaths, + emailAddresses: controller.listEmailAddressDefault, + emailAddressSelected: controller.emailOfIdentity.value, + isEnabled: controller.actionType.value == IdentityActionType.create, + onEmailAddressSelected: (emailAddress) => + controller.updateEmailOfIdentity( + context, + emailAddress, + ), + )), + const SizedBox(height: 15), + DefaultLabelFieldWidget(label: appLocalizations.reply_to), + Obx(() => DefaultEmailAddressDropDownButton( + imagePaths: controller.imagePaths, + emailAddresses: controller.listEmailAddressOfReplyTo, + emailAddressSelected: controller.replyToOfIdentity.value, + onEmailAddressSelected: (emailAddress) => + controller.updaterReplyToOfIdentity( + context, + emailAddress, + ), + )), + const SizedBox(height: 15), + DefaultLabelFieldWidget(label: appLocalizations.bcc_to), + Obx(() { + return TypeAheadFormFieldBuilder( + focusNode: controller.inputBccIdentityFocusNode, + controller: controller.inputBccIdentityController, + textInputAction: TextInputAction.done, + textStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.m3SurfaceBackground, + ), + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + horizontal: 12, + vertical: 15, + )) + ..setErrorText(controller.errorBccIdentity.value) + ..setHintText(appLocalizations.enterEmailAddress) + ..setHintStyle(ThemeUtils.textStyleBodyBody3( + color: AppColor.m3Tertiary, + )) + ).build(), + debounceDuration: const Duration(milliseconds: 500), + suggestionsCallback: (pattern) async { + controller.validateInputBccAddress(context, pattern); + if (pattern.trim().isEmpty) { + controller.updateBccOfIdentity(null); + } else { + controller.updateBccOfIdentity(EmailAddress(null, pattern)); + } + return controller.getSuggestionEmailAddress(pattern); + }, + itemBuilder: (_, emailAddress) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + alignment: AlignmentDirectional.centerStart, + child: Text( + emailAddress.emailAddress, + style: ThemeUtils.textStyleInter400.copyWith( + fontSize: 15, + height: 20 / 15, + letterSpacing: -0.15, + color: Colors.black, ), - ); - }), - Obx(() { - bool isInserting = controller.publicAssetController?.isUploading.isTrue == true - || controller.isCompressingInlineImage.isTrue; - return InsertImageLoadingIndicator(isInserting: isInserting); - }) - ], + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + onSuggestionSelected: (emailSelected) { + controller.inputBccIdentityController.text = emailSelected.emailAddress; + controller.updateBccOfIdentity(emailSelected); + }, + noItemsFoundBuilder: (_) => const SizedBox.shrink(), + hideOnEmpty: true, + hideOnError: true, + hideOnLoading: true, ); - } + }), + const SizedBox(height: 15), + DefaultLabelFieldWidget(label: appLocalizations.signature), + IdentitySignatureInputFieldWidget(controller: controller), + const SizedBox(height: 15), + ], + ), + ); + } else { + bodyCreatorView = SingleChildScrollView( + controller: controller.scrollController, + physics: const ClampingScrollPhysics(), + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: 32, + end: 32, + bottom: 24, ), - const SizedBox(height: 12), - if (controller.isMobile(context)) - _buildActionButtonMobile(context) - ]), - ), - ); + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + DefaultHorizontalFieldWidget( + label: '${appLocalizations.name} (${appLocalizations.required})', + child: Obx(() => TextFieldBuilder( + onTextChange: (value) => controller.updateNameIdentity( + context, + value, + ), + textInputAction: TextInputAction.next, + autoFocus: true, + maxLines: 1, + textDirection: DirectionUtils.getDirectionByLanguage(context), + controller: controller.inputNameIdentityController, + focusNode: controller.inputNameIdentityFocusNode, + textStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.m3SurfaceBackground, + ), + keyboardType: TextInputType.text, + semanticLabel: 'Identity input field', + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + horizontal: 12, + vertical: 15, + )) + ..setErrorText(controller.errorNameIdentity.value) + ..setHintText(appLocalizations.enterName) + ..setHintStyle(ThemeUtils.textStyleBodyBody3( + color: AppColor.m3Tertiary, + )) + ).build(), + )), + ), + const SizedBox(height: 12), + DefaultHorizontalFieldWidget( + label: appLocalizations.email.inCaps, + child: Obx(() => DefaultEmailAddressDropDownButton( + imagePaths: controller.imagePaths, + emailAddresses: controller.listEmailAddressDefault, + emailAddressSelected: controller.emailOfIdentity.value, + isEnabled: controller.actionType.value == IdentityActionType.create, + onEmailAddressSelected: (emailAddress) => + controller.updateEmailOfIdentity( + context, + emailAddress, + ), + )), + ), + const SizedBox(height: 12), + DefaultHorizontalFieldWidget( + label: appLocalizations.reply_to, + child: Obx(() => DefaultEmailAddressDropDownButton( + imagePaths: controller.imagePaths, + emailAddresses: controller.listEmailAddressOfReplyTo, + emailAddressSelected: controller.replyToOfIdentity.value, + onEmailAddressSelected: (emailAddress) => + controller.updaterReplyToOfIdentity( + context, + emailAddress, + ), + )), + ), + const SizedBox(height: 12), + DefaultHorizontalFieldWidget( + label: appLocalizations.bcc_to, + child: Obx(() { + return TypeAheadFormFieldBuilder( + focusNode: controller.inputBccIdentityFocusNode, + controller: controller.inputBccIdentityController, + textInputAction: TextInputAction.done, + textStyle: ThemeUtils.textStyleBodyBody3( + color: AppColor.m3SurfaceBackground, + ), + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + horizontal: 12, + vertical: 15, + )) + ..setErrorText(controller.errorBccIdentity.value) + ..setHintText(appLocalizations.enterEmailAddress) + ..setHintStyle(ThemeUtils.textStyleBodyBody3( + color: AppColor.m3Tertiary, + )) + ).build(), + debounceDuration: const Duration(milliseconds: 500), + suggestionsCallback: (pattern) async { + controller.validateInputBccAddress(context, pattern); + if (pattern.trim().isEmpty) { + controller.updateBccOfIdentity(null); + } else { + controller.updateBccOfIdentity(EmailAddress(null, pattern)); + } + return controller.getSuggestionEmailAddress(pattern); + }, + itemBuilder: (_, emailAddress) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + alignment: AlignmentDirectional.centerStart, + child: Text( + emailAddress.emailAddress, + style: ThemeUtils.textStyleInter400.copyWith( + fontSize: 15, + height: 20 / 15, + letterSpacing: -0.15, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + onSuggestionSelected: (emailSelected) { + controller.inputBccIdentityController.text = emailSelected.emailAddress; + controller.updateBccOfIdentity(emailSelected); + }, + noItemsFoundBuilder: (_) => const SizedBox.shrink(), + hideOnEmpty: true, + hideOnError: true, + hideOnLoading: true, + ); + }), + ), + const SizedBox(height: 12), + DefaultHorizontalFieldWidget( + label: appLocalizations.signature, + child: IdentitySignatureInputFieldWidget( + controller: controller, + ), + ), + ], + ), + ), + ); + } if (PlatformInfo.isWeb) { bodyCreatorView = NotificationListener( @@ -177,103 +331,16 @@ class IdentityCreatorView extends GetWidget ); return PointerInterceptor( - child: GestureDetector( - onTap: () => controller.clearFocusEditor(context), - child: ResponsiveWidget( - responsiveUtils: controller.responsiveUtils, - mobile: Scaffold( - backgroundColor: Colors.black38, - body: Card( - margin: EdgeInsets.zero, - borderOnForeground: false, - color: Colors.transparent, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topRight: Radius.circular(16), - topLeft: Radius.circular(16)), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - topLeft: Radius.circular(16) - ), - ), - child: Column(children: [ - _buildHeaderView(context), - Expanded(child: bodyCreatorView) - ]), - ), - ), - ), - ), - landscapeMobile: Scaffold( - backgroundColor: Colors.white, - body: Column(children: [ - _buildHeaderView(context), - Expanded(child: bodyCreatorView) - ]) - ), - tablet: Scaffold( - backgroundColor: Colors.black38, - body: Center( - child: Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 24), - child: Card( - color: Colors.transparent, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)) - ), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(16)) - ), - width: math.max(controller.responsiveUtils.getSizeScreenWidth(context) * 0.4, 700), - height: controller.responsiveUtils.getSizeScreenHeight(context) * 0.8, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: Column(children: [ - _buildHeaderView(context), - Expanded(child: bodyCreatorView), - const SizedBox(height: 12), - _buildActionButtonDesktop(context) - ]) - ) - ) - ), - ) - ), - ), - desktop: Scaffold( - backgroundColor: Colors.black38, - body: Center( - child: Card( - color: Colors.transparent, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)) - ), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(16)) - ), - width: math.max(controller.responsiveUtils.getSizeScreenWidth(context) * 0.4, 800), - height: controller.responsiveUtils.getSizeScreenHeight(context) * 0.8, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: Column(children: [ - _buildHeaderView(context), - Expanded(child: bodyCreatorView), - const SizedBox(height: 12), - _buildActionButtonDesktop(context) - ]) - ) - ) - ) - ) - ) + child: ResponsiveWidget( + responsiveUtils: controller.responsiveUtils, + mobile: IdentityCreatorFormMobileBuilder( + controller: controller, + formView: bodyCreatorView, ), + tablet: IdentityCreatorFormDesktopBuilder( + controller: controller, + formView: bodyCreatorView, + ) ), ); } else { @@ -319,7 +386,6 @@ class IdentityCreatorView extends GetWidget ), ), child: Column(children: [ - _buildHeaderView(context), Expanded(child: bodyCreatorView) ]), ), @@ -328,38 +394,6 @@ class IdentityCreatorView extends GetWidget ), ), ), - landscapeMobile: GestureDetector( - onTap: () => controller.clearFocusEditor(context), - child: Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - left: false, - right: false, - child: KeyboardRichText( - keyBroadToolbar: RichTextKeyboardToolBar( - rootContext: context, - titleBack: AppLocalizations.of(context).titleFormat, - backgroundKeyboardToolBarColor: PlatformInfo.isIOS - ? AppColor.colorBackgroundKeyboard - : AppColor.colorBackgroundKeyboardAndroid, - richTextController: controller.richTextMobileTabletController!.richTextController, - quickStyleLabel: AppLocalizations.of(context).titleQuickStyles, - backgroundLabel: AppLocalizations.of(context).titleBackground, - foregroundLabel: AppLocalizations.of(context).titleForeground, - formatLabel: AppLocalizations.of(context).titleFormat, - insertImage: () => controller.pickImage(context), - ), - richTextController: controller.richTextMobileTabletController!.richTextController, - child: SafeArea( - child: Column(children: [ - _buildHeaderView(context), - Expanded(child: bodyCreatorView) - ]), - ), - ), - ) - ), - ), tablet: Portal( child: GestureDetector( onTap: () => controller.clearFocusEditor(context), @@ -398,10 +432,7 @@ class IdentityCreatorView extends GetWidget width: math.max(controller.responsiveUtils.getSizeScreenWidth(context) * 0.4, 700), height: controller.responsiveUtils.getSizeScreenHeight(context) * 0.8, child: Column(children: [ - _buildHeaderView(context), Expanded(child: bodyCreatorView), - const SizedBox(height: 12), - _buildActionButtonDesktop(context) ]) ) ), @@ -415,252 +446,4 @@ class IdentityCreatorView extends GetWidget ); } } - - Widget _buildHeaderView(BuildContext context) { - return Container( - color: Colors.white, - height: 52, - child: Row(children: [ - const SizedBox(width: 40), - Expanded(child: Obx(() { - return Text( - controller.actionType.value == IdentityActionType.create - ? AppLocalizations.of(context).createNewIdentity.inCaps - : AppLocalizations.of(context).edit_identity.inCaps, - maxLines: 1, - textAlign: TextAlign.center, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.black - )); - })), - buildIconWeb( - iconSize: 24, - icon: SvgPicture.asset( - controller.imagePaths.icComposerClose, - fit: BoxFit.fill, - colorFilter: AppColor.colorDeleteContactIcon.asFilter() - ), - tooltip: AppLocalizations.of(context).close, - onTap: () => controller.closeView(context)), - ]), - ); - } - - Widget _buildSignatureHtmlTemplate( - BuildContext context, - double maxWidth - ) { - if (PlatformInfo.isWeb) { - return Column( - children: [ - ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController!, - padding: const EdgeInsets.only(bottom: 12), - extendedOption: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 4.0), - child: buildWrapIconStyleText( - icon: buildIconWithTooltip( - path: controller.imagePaths.icAddPicture, - tooltip: AppLocalizations.of(context).insertImage - ), - hasDropdown: false, - onTap: () => controller.pickImage(context) - ), - ), - ] - ), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: 300, - ), - child: _buildHtmlEditorWeb( - context, - controller.contentHtmlEditor, - maxWidth), - ), - ], - ); - } else { - return _buildHtmlEditor( - context, - initialContent: controller.contentHtmlEditor); - } - } - - Widget _buildHtmlEditorWeb( - BuildContext context, - String initContent, - double maxWidth - ) { - return html_editor_browser.HtmlEditor( - key: const Key('identity_create_editor_web'), - controller: controller.richTextWebController!.editorController, - htmlEditorOptions: html_editor_browser.HtmlEditorOptions( - shouldEnsureVisible: true, - hint: '', - darkMode: false, - cacheHTMLAssetOffline: true, - initialText: initContent.isEmpty ? null : initContent, - disableDragAndDrop: true, - spellCheck: true, - customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)), - ), - htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions( - toolbarType: html_editor_browser.ToolbarType.hide, - defaultToolbarButtons: [] - ), - otherOptions: const html_editor_browser.OtherOptions(height: 200), - callbacks: html_editor_browser.Callbacks( - onBeforeCommand: controller.updateContentHtmlEditor, - onChangeContent: (content) { - controller.updateContentHtmlEditor(content); - if (!controller.isLoadSignatureCompleted) { - controller.onLoadSignatureCompleted(content); - } - }, - onInit: () { - controller.richTextWebController?.editorController.setOnDragDropEvent(); - controller.richTextWebController?.editorController.setFullScreen(); - controller.updateContentHtmlEditor(initContent); - }, - onFocus: () { - FocusManager.instance.primaryFocus?.unfocus(); - Future.delayed(const Duration(milliseconds: 500), () { - controller.richTextWebController?.editorController.setFocus(); - }); - controller.richTextWebController?.closeAllMenuPopup(); - }, - onChangeSelection: controller.richTextWebController?.onEditorSettingsChange, - onChangeCodeview: controller.updateContentHtmlEditor, - onDragEnter: controller.handleOnDragEnterSignatureEditorWeb, - onDragLeave: (_) {}, - onImageUpload: (listFileUpload) => controller.onPasteImageSuccess( - context, - listFileUpload, - maxWidth: maxWidth), - onImageUploadError: (listFileUpload, base64, uploadError) => - controller.onPasteImageFailure( - context, - listFileUpload, - base64: base64, - uploadError: uploadError - ), - ), - ); - } - - Widget _buildHtmlEditor(BuildContext context, {String? initialContent}) { - return HtmlEditor( - key: controller.htmlKey, - minHeight: ConstantsUI.htmlContentMinHeight.toInt(), - maxHeight: PlatformInfo.isIOS ? ConstantsUI.composerHtmlContentMaxHeight : null, - addDefaultSelectionMenuItems: false, - initialContent: initialContent ?? '', - customStyleCss: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)), - onCreated: (editorApi) => controller.initRichTextForMobile(context, editorApi), - ); - } - - Widget _buildActionButtonDesktop(BuildContext context) { - return Padding( - padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24), - child: Row( - children: [ - Expanded(child: _buildCheckboxIdentityDefault(context)), - const SizedBox(width: 12), - _buildCancelButton(context, width: 156), - const SizedBox(width: 12), - _buildSaveButton(context, width: 156) - ], - ), - ); - } - - Widget _buildActionButtonMobile(BuildContext context) { - return Column(children: [ - _buildCheckboxIdentityDefault(context), - const SizedBox(height: 24), - SafeArea( - child: Row(children: [ - Expanded(child: _buildCancelButton(context)), - const SizedBox(width: 12), - Expanded(child: _buildSaveButton(context)) - ]), - ), - if (controller.richTextMobileTabletController != null) - ValueListenableBuilder( - valueListenable: controller.richTextMobileTabletController!.richTextController.richTextToolbarNotifier, - builder: (_, isShowing, __) { - if (isShowing) { - return const SizedBox(height: 48); - } else { - return const SizedBox.shrink(); - } - }) - ]); - } - - Widget _buildCheckboxIdentityDefault(BuildContext context) { - return Obx(() { - if (controller.isDefaultIdentitySupported.isTrue) { - return SetDefaultIdentityCheckboxBuilder( - imagePaths: controller.imagePaths, - isCheck: controller.isDefaultIdentity.value, - onCheckboxChanged: controller.onCheckboxChanged); - } else { - return const SizedBox.shrink(); - } - }); - } - - Widget _buildCancelButton(BuildContext context, {double? width}) { - return buildTextButton( - AppLocalizations.of(context).cancel, - textStyle: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontWeight: FontWeight.w500, - fontSize: 17, - color: AppColor.colorTextButton, - ), - backgroundColor: AppColor.emailAddressChipColor, - width: width ?? 128, - height: 44, - radius: 10, - onTap: () => controller.closeView(context), - ); - } - - Widget _buildSaveButton(BuildContext context, {double? width}) { - return Obx(() => controller.viewState.value.fold( - (failure) => buildTextButton( - controller.actionType.value == IdentityActionType.create - ? AppLocalizations.of(context).create - : AppLocalizations.of(context).save, - width: width ?? 128, - height: 44, - radius: 10, - onTap: () => controller.createNewIdentity(context)), - (success) { - if (success is GetAllIdentitiesLoading) { - return const Center( - key: Key('create_loading_icon'), - child: CircularProgressIndicator(color: AppColor.primaryColor)); - } else { - return buildTextButton( - controller.actionType.value == IdentityActionType.create - ? AppLocalizations.of(context).create - : AppLocalizations.of(context).save, - width: width ?? 128, - height: 44, - radius: 10, - onTap: () => controller.createNewIdentity(context)); - } - } - )); - } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart b/lib/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart new file mode 100644 index 000000000..398ea7628 --- /dev/null +++ b/lib/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart @@ -0,0 +1,132 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/checkbox/custom_icon_labeled_checkbox.dart'; +import 'package:core/presentation/views/dialog/confirm_dialog_button.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class IdentityCreatorFormBottomView extends StatelessWidget { + final IdentityCreatorController controller; + + const IdentityCreatorFormBottomView({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + + final isMobile = controller.responsiveUtils.isMobile(context); + + final checkboxWidget = Obx(() { + if (controller.isDefaultIdentitySupported.isTrue) { + return Expanded( + child: Obx(() { + return CustomIconLabeledCheckbox( + label: appLocalizations.setDefaultIdentity, + svgIconPath: controller.imagePaths.icCheckboxUnselected, + selectedSvgIconPath: controller.imagePaths.icCheckboxSelected, + value: controller.isDefaultIdentity.value, + semanticsLabel: 'Set default identity checkbox', + onChanged: (_) => controller.onCheckboxChanged(), + ); + }), + ); + } else { + return const SizedBox.shrink(); + } + }); + + final cancelButtonWidget = Container( + constraints: isMobile ? null : const BoxConstraints(minWidth: 67), + height: 48, + child: ConfirmDialogButton( + label: appLocalizations.cancel, + onTapAction: () => controller.closeView(context), + ), + ); + + final saveButtonWidget = Container( + constraints: isMobile ? null : const BoxConstraints(minWidth: 110), + height: 48, + child: Obx(() { + final viewState = controller.viewState.value; + final actionType = controller.actionType.value; + final isLoading = viewState.fold( + (failure) => false, + (success) => success is GetAllIdentitiesLoading, + ); + + if (isLoading) { + return const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: AppColor.primaryColor, + strokeWidth: 2.0, + ), + ), + ); + } else { + return ConfirmDialogButton( + label: actionType.getPositiveButtonTitle(appLocalizations), + backgroundColor: AppColor.primaryMain, + textColor: Colors.white, + onTapAction: () => controller.createNewIdentity(context), + ); + } + }), + ); + + if (isMobile) { + return Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 17), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + checkboxWidget, + Padding( + padding: const EdgeInsets.symmetric(vertical: 44), + child: Row( + children: [ + Expanded(child: cancelButtonWidget), + const SizedBox(width: 8), + Expanded(child: saveButtonWidget), + ], + ), + ), + ], + ), + ); + } else { + return Padding( + padding: const EdgeInsetsDirectional.only( + start: 32, + end: 37, + top: 12, + bottom: 24, + ), + child: Row( + children: [ + checkboxWidget, + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible(child: cancelButtonWidget), + const SizedBox(width: 8), + Flexible(child: saveButtonWidget), + ], + ), + ), + ], + ), + ); + } + } +} diff --git a/lib/features/identity_creator/presentation/widgets/identity_creator_form_desktop_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_creator_form_desktop_builder.dart new file mode 100644 index 000000000..3bba327f4 --- /dev/null +++ b/lib/features/identity_creator/presentation/widgets/identity_creator_form_desktop_builder.dart @@ -0,0 +1,77 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_close_button_widget.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_creator_form_title_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class IdentityCreatorFormDesktopBuilder extends StatelessWidget { + final IdentityCreatorController controller; + final Widget formView; + + const IdentityCreatorFormDesktopBuilder({ + super.key, + required this.controller, + required this.formView, + }); + + @override + Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + + return Scaffold( + backgroundColor: Colors.black.withOpacity(0.2), + body: GestureDetector( + onTap: () => controller.clearFocusEditor(context), + child: Center( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 24, + offset: const Offset(0, 2), + ), + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 2, + ), + ], + ), + width: min( + controller.responsiveUtils.getSizeScreenWidth(context) - 48, + 784, + ), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Obx(() { + final actionType = controller.actionType.value; + return IdentityCreatorFormTitleWidget( + title: actionType.getTitle(appLocalizations), + ); + }), + Flexible(child: formView), + const SizedBox(height: 24), + IdentityCreatorFormBottomView(controller: controller), + ], + ), + DefaultCloseButtonWidget( + iconClose: controller.imagePaths.icCloseDialog, + onTapActionCallback: () => controller.closeView(context), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/identity_creator/presentation/widgets/identity_creator_form_mobile_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_creator_form_mobile_builder.dart new file mode 100644 index 000000000..7ace4051b --- /dev/null +++ b/lib/features/identity_creator/presentation/widgets/identity_creator_form_mobile_builder.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_app_bar_widget.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_creator_form_bottom_view.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class IdentityCreatorFormMobileBuilder extends StatelessWidget { + final IdentityCreatorController controller; + final Widget formView; + + const IdentityCreatorFormMobileBuilder({ + super.key, + required this.controller, + required this.formView, + }); + + @override + Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + + return Scaffold( + backgroundColor: Colors.white, + body: GestureDetector( + onTap: () => controller.clearFocusEditor(context), + child: SafeArea( + child: ColoredBox( + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final actionType = controller.actionType.value; + return DefaultAppBarWidget( + title: actionType.getTitle(appLocalizations), + imagePaths: controller.imagePaths, + responsiveUtils: controller.responsiveUtils, + onBackAction: () => controller.closeView(context), + ); + }), + Expanded( + child: SingleChildScrollView( + controller: controller.scrollController, + physics: const ClampingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible(child: formView), + IdentityCreatorFormBottomView(controller: controller), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/identity_creator/presentation/widgets/identity_creator_form_title_widget.dart b/lib/features/identity_creator/presentation/widgets/identity_creator_form_title_widget.dart new file mode 100644 index 000000000..5e42021f7 --- /dev/null +++ b/lib/features/identity_creator/presentation/widgets/identity_creator_form_title_widget.dart @@ -0,0 +1,29 @@ +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:flutter/material.dart'; + +class IdentityCreatorFormTitleWidget extends StatelessWidget { + final String title; + + const IdentityCreatorFormTitleWidget({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: 32, + end: 32, + top: 24, + bottom: 12, + ), + child: Text( + title, + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: ThemeUtils.textStyleM3HeadlineSmall, + ), + ), + ); + } +} diff --git a/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart index e6265baa4..e69de29bb 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart @@ -1,103 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/presentation/utils/theme_utils.dart'; -import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/extensions/email_address_extension.dart'; - -typedef OnSelectEmailAddressDropListAction = Function(EmailAddress? emailAddress); - -class IdentityDropListFieldBuilder extends StatelessWidget { - - final ImagePaths _imagePaths; - final String _label; - final EmailAddress? _emailAddressSelected; - final List _listEmailAddress; - final OnSelectEmailAddressDropListAction? onSelectItemDropList; - - const IdentityDropListFieldBuilder( - this._imagePaths, - this._label, - this._emailAddressSelected, - this._listEmailAddress, { - super.key, - this.onSelectItemDropList - }); - - @override - Widget build(BuildContext context) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - _label, - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail)), - const SizedBox(height: 8), - DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - hint: Row( - children: [ - Expanded(child: Text( - '', - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black, - ), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - )), - ], - ), - items: _listEmailAddress.map((item) => DropdownMenuItem( - value: item, - child: Text( - item.emailAddress, - style: ThemeUtils.defaultTextStyleInterFont.copyWith(fontSize: 16, fontWeight: FontWeight.normal, color: Colors.black), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - ), - )).toList(), - value: _emailAddressSelected, - onChanged: onSelectItemDropList, - buttonStyleData: ButtonStyleData( - height: 44, - padding: const EdgeInsetsDirectional.only(end: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.colorInputBorderCreateMailbox, width: 0.5), - color: AppColor.colorInputBackgroundCreateMailbox), - ), - dropdownStyleData: DropdownStyleData( - maxHeight: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white), - elevation: 4, - scrollbarTheme: ScrollbarThemeData( - radius: const Radius.circular(40), - thickness: WidgetStateProperty.all(6), - thumbVisibility: WidgetStateProperty.all(true), - ), - ), - iconStyleData: IconStyleData( - icon: SvgPicture.asset(_imagePaths.icDropDown), - iconSize: 14, - ), - menuItemStyleData: const MenuItemStyleData( - height: 44, - padding: EdgeInsets.symmetric(horizontal: 12), - ), - ), - ) - ]); - } -} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart index 93db93058..e69de29bb 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart @@ -1,51 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/presentation/utils/theme_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; - -class IdentityFieldNoEditableBuilder extends StatelessWidget { - - final String _label; - final EmailAddress? _emailAddressSelected; - - const IdentityFieldNoEditableBuilder( - this._label, - this._emailAddressSelected, - {super.key} - ); - - @override - Widget build(BuildContext context) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - _label, - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail)), - const SizedBox(height: 8), - Container( - height: 44, - alignment: Alignment.centerLeft, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.colorInputBorderCreateMailbox, width: 0.5), - color: AppColor.colorInputBackgroundCreateMailbox), - child: Text( - _emailAddressSelected?.email ?? '', - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontSize: 16, - fontWeight: FontWeight.normal, - color: AppColor.colorInputBorderCreateMailbox), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap - ) - ), - ]); - } -} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart index ef4427308..687b9b4b1 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart @@ -32,6 +32,7 @@ class IdentityInputDecorationBuilder extends InputDecorationBuilder { filled: true, fillColor: errorText?.isNotEmpty == true ? AppColor.colorInputBackgroundErrorVerifyName - : AppColor.colorInputBackgroundCreateMailbox); + : Colors.white, + ); } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart index cf582dfe6..e69de29bb 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart @@ -1,66 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/theme_utils.dart'; -import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/direction_utils.dart'; -import 'package:core/utils/platform_info.dart'; -import 'package:flutter/material.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart'; - -typedef OnChangeInputNameAction = Function(String? value); - -class IdentityInputFieldBuilder extends StatelessWidget { - - final String _label; - final String? _error; - final String? requiredIndicator; - final TextEditingController? editingController; - final FocusNode? focusNode; - final TextInputType? inputType; - final bool isMandatory; - final OnChangeInputNameAction? onChangeInputNameAction; - - const IdentityInputFieldBuilder( - this._label, - this._error, - this.requiredIndicator, { - super.key, - this.isMandatory = false, - this.editingController, - this.focusNode, - this.inputType, - this.onChangeInputNameAction - }); - - @override - Widget build(BuildContext context) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - isMandatory - ? '$_label ($requiredIndicator)' - : _label, style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColor.colorContentEmail)), - const SizedBox(height: 8), - TextFieldBuilder( - onTextChange: onChangeInputNameAction, - textInputAction: TextInputAction.next, - autoFocus: true, - maxLines: 1, - textDirection: DirectionUtils.getDirectionByLanguage(context), - controller: editingController, - focusNode: focusNode, - textStyle: ThemeUtils.defaultTextStyleInterFont.copyWith(color: Colors.black, fontSize: 16), - keyboardType: inputType ?? TextInputType.text, - semanticLabel: 'Identity input field', - decoration: (IdentityInputDecorationBuilder() - ..setContentPadding(EdgeInsets.symmetric( - vertical: PlatformInfo.isWeb ? 16 : 12, - horizontal: 12)) - ..setErrorText(_error)) - .build(), - ) - ]); - } -} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart index eb00e7a71..e69de29bb 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart @@ -1,88 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/theme_utils.dart'; -import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; -import 'package:core/utils/platform_info.dart'; -import 'package:flutter/material.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart'; - -typedef OnSelectedSuggestionAction = Function(EmailAddress? emailAddress); -typedef OnSuggestionCallbackAction = Function(String? pattern); -typedef OnChangeInputSuggestionAction = Function(String? pattern); - -class IdentityInputWithDropListFieldBuilder extends StatelessWidget { - - final String _label; - final String? _error; - final TextEditingController? editingController; - final FocusNode? focusNode; - final OnSelectedSuggestionAction? onSelectedSuggestionAction; - final OnSuggestionCallbackAction? onSuggestionCallbackAction; - final OnChangeInputSuggestionAction? onChangeInputSuggestionAction; - - const IdentityInputWithDropListFieldBuilder( - this._label, - this._error, - this.editingController, { - super.key, - this.focusNode, - this.onSelectedSuggestionAction, - this.onSuggestionCallbackAction, - this.onChangeInputSuggestionAction, - }); - - @override - Widget build(BuildContext context) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - _label, - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail)), - const SizedBox(height: 8), - TypeAheadFormFieldBuilder( - focusNode: focusNode, - controller: editingController, - textInputAction: TextInputAction.done, - decoration: (IdentityInputDecorationBuilder() - ..setContentPadding(EdgeInsets.symmetric( - vertical: PlatformInfo.isWeb ? 16 : 12, - horizontal: 12)) - ..setErrorText(_error)) - .build(), - debounceDuration: const Duration(milliseconds: 500), - suggestionsCallback: (pattern) async { - if (onChangeInputSuggestionAction != null) { - onChangeInputSuggestionAction!(pattern); - } - if (onSuggestionCallbackAction != null) { - return onSuggestionCallbackAction!(pattern); - } else { - return []; - } - }, - itemBuilder: (BuildContext context, emailAddress) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Text( - emailAddress.email ?? '', - style: ThemeUtils.defaultTextStyleInterFont.copyWith( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Colors.black))); - }, - onSuggestionSelected: (emailSelected) { - if (onSelectedSuggestionAction != null) { - onSelectedSuggestionAction!(emailSelected); - } - }, - noItemsFoundBuilder: (context) => const SizedBox(), - hideOnEmpty: true, - hideOnError: true, - hideOnLoading: true, - ) - ]); - } -} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_signature_input_field_widget.dart b/lib/features/identity_creator/presentation/widgets/identity_signature_input_field_widget.dart new file mode 100644 index 000000000..008824c01 --- /dev/null +++ b/lib/features/identity_creator/presentation/widgets/identity_signature_input_field_widget.dart @@ -0,0 +1,212 @@ +import 'package:core/presentation/constants/constants_ui.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/html/html_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:html_editor_enhanced/html_editor.dart' as html_editor_browser; +import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/insert_image_loading_indicator.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +class IdentitySignatureInputFieldWidget extends StatelessWidget + with RichTextButtonMixin { + final IdentityCreatorController controller; + + IdentitySignatureInputFieldWidget({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (_, constraints) { + return Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10)), + border: Border.all(color: AppColor.m3Neutral90), + ), + padding: const EdgeInsetsDirectional.only( + start: 8, + end: 8, + top: 4, + bottom: 8, + ), + child: _buildSignatureHtmlTemplate( + context, + constraints.maxWidth, + ), + ), + Obx(() { + final draggableAppState = controller.draggableAppState.value; + if (draggableAppState == DraggableAppState.inActive) { + return const SizedBox.shrink(); + } + return Positioned.fill( + child: PointerInterceptor( + child: LocalFileDropZoneWidget( + imagePaths: controller.imagePaths, + width: constraints.maxWidth, + height: constraints.maxHeight, + margin: EdgeInsets.zero, + onLocalFileDropZoneListener: (details) => + controller.onLocalFileDropZoneListener( + context: context, + details: details, + maxWidth: constraints.maxWidth, + ), + ), + ), + ); + }), + Obx(() { + final isUploading = + controller.publicAssetController?.isUploading.isTrue ?? false; + final isCompressingInlineImage = + controller.isCompressingInlineImage.isTrue; + + return InsertImageLoadingIndicator( + isInserting: isUploading || isCompressingInlineImage, + ); + }), + ], + ); + }, + ); + } + + Widget _buildSignatureHtmlTemplate(BuildContext context, double maxWidth) { + if (PlatformInfo.isWeb) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController!, + padding: const EdgeInsets.only(bottom: 12), + extendedOption: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: buildWrapIconStyleText( + icon: buildIconWithTooltip( + path: controller.imagePaths.icAddPicture, + tooltip: AppLocalizations.of(context).insertImage, + ), + hasDropdown: false, + onTap: () => controller.pickImage(context), + ), + ), + ], + ), + SizedBox( + height: 189, + child: _buildHtmlEditorWeb( + context, + controller.contentHtmlEditor, + maxWidth, + ), + ), + ], + ); + } else { + return _buildHtmlEditor( + context, + initialContent: controller.contentHtmlEditor, + ); + } + } + + Widget _buildHtmlEditorWeb( + BuildContext context, + String initContent, + double maxWidth, + ) { + final richTextWebController = controller.richTextWebController!; + + return html_editor_browser.HtmlEditor( + key: const Key('identity_create_editor_web'), + controller: richTextWebController.editorController, + htmlEditorOptions: html_editor_browser.HtmlEditorOptions( + shouldEnsureVisible: true, + hint: '', + darkMode: false, + cacheHTMLAssetOffline: true, + initialText: initContent.isEmpty ? null : initContent, + disableDragAndDrop: true, + spellCheck: true, + customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor( + direction: AppUtils.getCurrentDirection(context), + ), + ), + htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions( + toolbarType: html_editor_browser.ToolbarType.hide, + defaultToolbarButtons: [], + ), + otherOptions: const html_editor_browser.OtherOptions(height: 200), + callbacks: html_editor_browser.Callbacks( + onBeforeCommand: controller.updateContentHtmlEditor, + onChangeContent: (content) { + controller.updateContentHtmlEditor(content); + if (!controller.isLoadSignatureCompleted) { + controller.onLoadSignatureCompleted(content); + } + }, + onInit: () { + richTextWebController.editorController.setOnDragDropEvent(); + richTextWebController.editorController.setFullScreen(); + controller.updateContentHtmlEditor(initContent); + }, + onFocus: () { + FocusManager.instance.primaryFocus?.unfocus(); + Future.delayed(const Duration(milliseconds: 500), () { + richTextWebController.editorController.setFocus(); + }); + richTextWebController.closeAllMenuPopup(); + }, + onChangeSelection: richTextWebController.onEditorSettingsChange, + onChangeCodeview: controller.updateContentHtmlEditor, + onDragEnter: controller.handleOnDragEnterSignatureEditorWeb, + onDragLeave: (_) {}, + onImageUpload: (listFileUpload) => controller.onPasteImageSuccess( + context, + listFileUpload, + maxWidth: maxWidth, + ), + onImageUploadError: (listFileUpload, base64, uploadError) => + controller.onPasteImageFailure( + context, + listFileUpload, + base64: base64, + uploadError: uploadError, + ), + ), + ); + } + + Widget _buildHtmlEditor(BuildContext context, {String? initialContent}) { + return HtmlEditor( + key: controller.htmlKey, + minHeight: ConstantsUI.htmlContentMinHeight.toInt(), + maxHeight: + PlatformInfo.isIOS ? ConstantsUI.composerHtmlContentMaxHeight : null, + addDefaultSelectionMenuItems: false, + initialContent: initialContent ?? '', + customStyleCss: HtmlUtils.customCssStyleHtmlEditor( + direction: AppUtils.getCurrentDirection(context), + ), + onCreated: (editorApi) => controller.initRichTextForMobile( + context, + editorApi, + ), + ); + } +} diff --git a/lib/features/manage_account/presentation/model/identity_action_type.dart b/lib/features/manage_account/presentation/model/identity_action_type.dart index fcf4bce9a..90d995f88 100644 --- a/lib/features/manage_account/presentation/model/identity_action_type.dart +++ b/lib/features/manage_account/presentation/model/identity_action_type.dart @@ -1,5 +1,25 @@ +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + enum IdentityActionType { create, - edit + edit; + + String getTitle(AppLocalizations appLocalizations) { + switch (this) { + case IdentityActionType.create: + return appLocalizations.createNewIdentity; + case IdentityActionType.edit: + return appLocalizations.edit_identity; + } + } + + String getPositiveButtonTitle(AppLocalizations appLocalizations) { + switch (this) { + case IdentityActionType.create: + return appLocalizations.create; + case IdentityActionType.edit: + return appLocalizations.save; + } + } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 8226a6fe8..28834b55e 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4641,5 +4641,17 @@ "placeholders": { "period": {} } + }, + "enterName": "Enter name", + "@enterName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enterEmailAddress": "Enter email address", + "@enterEmailAddress": { + "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 0ecaa8801..8880febab 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4891,4 +4891,18 @@ class AppLocalizations { args: [period], ); } + + String get enterName { + return Intl.message( + 'Enter name', + name: 'enterName', + ); + } + + String get enterEmailAddress { + return Intl.message( + 'Enter email address', + name: 'enterEmailAddress', + ); + } }