Apply new style for create identity view on desktop

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-07-11 19:36:14 +07:00
committed by Dat H. Pham
parent 4fd0931c9e
commit 58d22cbc0b
21 changed files with 1368 additions and 1066 deletions
@@ -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);
@@ -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;
}
}
}
@@ -63,8 +63,8 @@ abstract class InputDecorationBuilder {
errorTextStyle = newStyle;
}
void setFocusBorder(OutlineInputBorder focusBorder) {
focusBorder = focusBorder;
void setFocusBorder(OutlineInputBorder newFocusBorder) {
focusBorder = newFocusBorder;
}
InputDecoration build() {
@@ -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<T> extends StatefulWidget {
final TextDirection textDirection;
@@ -11,7 +13,7 @@ class TypeAheadFormFieldBuilder<T> extends StatefulWidget {
final SuggestionsCallback<T> suggestionsCallback;
final ItemBuilder<T> itemBuilder;
final SuggestionSelectionCallback<T> onSuggestionSelected;
final Widget? suggestionsBoxDecoration;
final SuggestionBoxDecorationBuilder? suggestionsBoxDecoration;
final WidgetBuilder? noItemsFoundBuilder;
final bool hideOnEmpty;
final bool hideOnError;
@@ -26,6 +28,7 @@ class TypeAheadFormFieldBuilder<T> extends StatefulWidget {
final TextInputType keyboardType;
final InputDecoration decoration;
final Color cursorColor;
final TextStyle? textStyle;
const TypeAheadFormFieldBuilder({
super.key,
@@ -47,6 +50,7 @@ class TypeAheadFormFieldBuilder<T> extends StatefulWidget {
this.cursorColor = AppColor.primaryColor,
this.autofillHints,
this.textInputAction,
this.textStyle,
this.onTextChange,
this.onTextSubmitted,
});
@@ -73,13 +77,14 @@ class _TypeAheadFormFieldBuilderState<T> extends State<TypeAheadFormFieldBuilder
key: widget.key,
controller: widget.controller,
focusNode: widget.focusNode,
builder: (context, controller, focusNode) {
builder: (_, controller, focusNode) {
return TextField(
controller: controller,
focusNode: focusNode,
textInputAction: widget.textInputAction,
autocorrect: widget.autocorrect,
autofillHints: widget.autofillHints,
style: widget.textStyle,
keyboardType: widget.keyboardType,
decoration: widget.decoration,
textDirection: _textDirection,
@@ -103,12 +108,17 @@ class _TypeAheadFormFieldBuilderState<T> extends State<TypeAheadFormFieldBuilder
itemBuilder: widget.itemBuilder,
onSelected: widget.onSuggestionSelected,
decorationBuilder: (context, child) {
return widget.suggestionsBoxDecoration ?? Material(
type: MaterialType.card,
elevation: 4,
borderRadius: BorderRadius.circular(14),
child: child,
);
if (widget.suggestionsBoxDecoration == null) {
return Material(
type: MaterialType.card,
elevation: 4,
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: child,
);
} else {
return widget.suggestionsBoxDecoration!(child);
}
},
emptyBuilder: widget.noItemsFoundBuilder,
hideOnEmpty: widget.hideOnEmpty,
@@ -0,0 +1,151 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.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';
import 'package:pointer_interceptor/pointer_interceptor.dart';
typedef OnEmailAddressSelected = void Function(EmailAddress emailAddress);
class DefaultEmailAddressDropDownButton extends StatelessWidget {
final ImagePaths imagePaths;
final List<EmailAddress> 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<EmailAddress>(
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<double>(6),
thumbVisibility: WidgetStateProperty.all<bool>(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<EmailAddress> _buildItemMenu(EmailAddress emailAddress) {
return DropdownMenuItem<EmailAddress>(
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,
),
],
),
),
);
}
}
@@ -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;
}
}
}
@@ -27,16 +27,16 @@ class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin
final ResponsiveUtils _responsiveUtils = Get.find<ResponsiveUtils>();
final EdgeInsetsGeometry? padding;
final List<Widget>? 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<FontNameType>(
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<FontNameType>(
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,
),
);
});
@@ -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<IdentityCreatorController>
with RichTextButtonMixin {
@@ -44,125 +38,285 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
@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<EmailAddress>(
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<EmailAddress>(
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<ScrollNotification>(
@@ -177,103 +331,16 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
);
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<IdentityCreatorController>
),
),
child: Column(children: [
_buildHeaderView(context),
Expanded(child: bodyCreatorView)
]),
),
@@ -328,38 +394,6 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
),
),
),
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<IdentityCreatorController>
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<IdentityCreatorController>
);
}
}
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));
}
}
));
}
}
@@ -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),
],
),
),
],
),
);
}
}
}
@@ -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),
),
],
),
),
),
),
);
}
}
@@ -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),
],
),
),
),
],
),
),
),
),
);
}
}
@@ -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,
),
),
);
}
}
@@ -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<EmailAddress> _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<EmailAddress>(
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<EmailAddress>(
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<double>(6),
thumbVisibility: WidgetStateProperty.all<bool>(true),
),
),
iconStyleData: IconStyleData(
icon: SvgPicture.asset(_imagePaths.icDropDown),
iconSize: 14,
),
menuItemStyleData: const MenuItemStyleData(
height: 44,
padding: EdgeInsets.symmetric(horizontal: 12),
),
),
)
]);
}
}
@@ -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
)
),
]);
}
}
@@ -32,6 +32,7 @@ class IdentityInputDecorationBuilder extends InputDecorationBuilder {
filled: true,
fillColor: errorText?.isNotEmpty == true
? AppColor.colorInputBackgroundErrorVerifyName
: AppColor.colorInputBackgroundCreateMailbox);
: Colors.white,
);
}
}
@@ -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(),
)
]);
}
}
@@ -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<EmailAddress>(
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,
)
]);
}
}
@@ -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,
),
);
}
}
@@ -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;
}
}
}
+12
View File
@@ -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": {}
}
}
@@ -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',
);
}
}