diff --git a/assets/images/ic_attachment_file.svg b/assets/images/ic_attachment_file.svg deleted file mode 100644 index 5dbd3c36c..000000000 --- a/assets/images/ic_attachment_file.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/ic_attachments_composer.svg b/assets/images/ic_attachments_composer.svg new file mode 100644 index 000000000..9dacdfabe --- /dev/null +++ b/assets/images/ic_attachments_composer.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_close_composer.svg b/assets/images/ic_close_composer.svg new file mode 100644 index 000000000..ab4826af2 --- /dev/null +++ b/assets/images/ic_close_composer.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_delete_attachment.svg b/assets/images/ic_delete_attachment.svg new file mode 100644 index 000000000..e9d62d6be --- /dev/null +++ b/assets/images/ic_delete_attachment.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_delete_composer.svg b/assets/images/ic_delete_composer.svg new file mode 100644 index 000000000..52f30249f --- /dev/null +++ b/assets/images/ic_delete_composer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/ic_file_attachment.svg b/assets/images/ic_file_attachment.svg new file mode 100644 index 000000000..9524125b7 --- /dev/null +++ b/assets/images/ic_file_attachment.svg @@ -0,0 +1,8 @@ + + + + diff --git a/assets/images/ic_fullscreen_composer.svg b/assets/images/ic_fullscreen_composer.svg new file mode 100644 index 000000000..9f92b83d7 --- /dev/null +++ b/assets/images/ic_fullscreen_composer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_minimize_composer.svg b/assets/images/ic_minimize_composer.svg new file mode 100644 index 000000000..ac0e2f5f4 --- /dev/null +++ b/assets/images/ic_minimize_composer.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_send_composer.svg b/assets/images/ic_send_composer.svg new file mode 100644 index 000000000..4a446e6f8 --- /dev/null +++ b/assets/images/ic_send_composer.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/core.dart b/core/lib/core.dart index 4c2cdfa04..edba7690d 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -32,6 +32,7 @@ export 'presentation/views/text/rich_text_builder.dart'; export 'presentation/views/responsive/responsive_widget.dart'; export 'presentation/views/list/tree_view.dart'; export 'presentation/views/button/button_builder.dart'; +export 'presentation/views/button/icon_button_web.dart'; export 'presentation/views/image/avatar_builder.dart'; export 'presentation/views/list/sliver_grid_delegate_fixed_height.dart'; export 'presentation/views/text/text_form_field_builder.dart'; diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index b2fac64cd..af93e8f7b 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -79,12 +79,12 @@ extension AppColor on Color { static const colorInputBorderCreateMailbox = Color(0x1F000000); static const colorInputBackgroundErrorVerifyName = Color(0xFFFAEBEB); static const colorInputBackgroundCreateMailbox = Color(0xFFF2F3F5); - static const colorHintInputCreateMailbox= Color(0xFFA9B4C2); - static const colorMessageConfirmDialog= Color(0xFF6D7885); - static const colorActionDeleteConfirmDialog= Color(0xFFE64646); - static const colorActionCancelDialog= Color(0xFF007AFF); - static const colorMessageDialog= Color(0xFF222222); - static const colorConfirmActionDialog= Color(0xFFF2F2F2); + static const colorHintInputCreateMailbox = Color(0xFFA9B4C2); + static const colorMessageConfirmDialog = Color(0xFF6D7885); + static const colorActionDeleteConfirmDialog = Color(0xFFE64646); + static const colorActionCancelDialog = Color(0xFF007AFF); + static const colorMessageDialog = Color(0xFF222222); + static const colorConfirmActionDialog = Color(0xFFF2F2F2); static const colorEmailAddress = Color(0xFF333333); static const colorHintEmailAddressInput = Color(0x993C3C43); static const colorDividerComposer = Color(0xFFC6C6C8); @@ -94,6 +94,10 @@ extension AppColor on Color { static const colorEmailAddressPrefix = Color(0xFF9AA7B6); static const colorEmailAddressTag = Color(0x146D7885); static const colorLineLeftEmailView = Color(0x2999A2AD); + static const colorShadowComposer = Color(0x1F000000); + static const colorBottomBarComposer = Color(0x5CEBEDF0); + static const colorShadowComposerFullScreen = Color(0x33000000); + static const colorCancelButton = Color(0xFFF2F2F2); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 6c4cac104..fa43be498 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -10,7 +10,6 @@ class ImagePaths { String get icBack => _getImagePath('ic_back.svg'); String get icEyeDisable => _getImagePath('ic_eye_disable.svg'); String get icShare => _getImagePath('ic_share.svg'); - String get icAttachmentFile => _getImagePath('ic_attachment_file.svg'); String get icMoreReceiver => _getImagePath('ic_more_receiver.svg'); String get icComposerClose => _getImagePath('ic_composer_close.svg'); String get icComposerSend => _getImagePath('ic_composer_send.svg'); @@ -66,6 +65,14 @@ class ImagePaths { String get icDeleteEmail => _getImagePath('ic_delete_email.svg'); String get icNewMessage => _getImagePath('ic_new_message.svg'); String get icUnreadStatus => _getImagePath('ic_unread_status.svg'); + String get icAttachmentsComposer => _getImagePath('ic_attachments_composer.svg'); + String get icCloseComposer => _getImagePath('ic_close_composer.svg'); + String get icFullScreenComposer => _getImagePath('ic_fullscreen_composer.svg'); + String get icMinimizeComposer => _getImagePath('ic_minimize_composer.svg'); + String get icSendComposer => _getImagePath('ic_send_composer.svg'); + String get icDeleteComposer => _getImagePath('ic_delete_composer.svg'); + String get icFileAttachment => _getImagePath('ic_file_attachment.svg'); + String get icDeleteAttachment => _getImagePath('ic_delete_attachment.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/utils/responsive_utils.dart b/core/lib/presentation/utils/responsive_utils.dart index d324e980a..85ecf2770 100644 --- a/core/lib/presentation/utils/responsive_utils.dart +++ b/core/lib/presentation/utils/responsive_utils.dart @@ -15,7 +15,7 @@ class ResponsiveUtils { final double desktopVerticalMargin = 120.0; final double desktopHorizontalMargin = 200.0; - bool isMobileDevice(BuildContext context) => MediaQuery.of(context).size.shortestSide < 600; + bool isMobileDevice(BuildContext context) => MediaQuery.of(context).size.shortestSide < minTabletWidth; double getSizeWidthScreen(BuildContext context) => MediaQuery.of(context).size.width; diff --git a/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart b/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart index 6359c62b6..40dc2a552 100644 --- a/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart +++ b/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart @@ -2,6 +2,7 @@ import 'package:core/core.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; typedef OnConfirmActionClick = void Function(); typedef OnCancelActionClick = void Function(); @@ -15,6 +16,9 @@ class ConfirmationDialogActionSheetBuilder { String? _cancelText; OnConfirmActionClick?_onConfirmActionClick; OnCancelActionClick? _onCancelActionClick; + TextStyle? _styleConfirmButton; + TextStyle? _styleCancelButton; + TextStyle? _styleMessage; ConfirmationDialogActionSheetBuilder(this._context); @@ -32,11 +36,23 @@ class ConfirmationDialogActionSheetBuilder { _messageText = message; } + void styleMessage(TextStyle? style) { + _styleMessage = style; + } + + void styleConfirmButton(TextStyle? style) { + _styleConfirmButton = style; + } + + void styleCancelButton(TextStyle? style) { + _styleCancelButton = style; + } + void show() { showCupertinoModalPopup( context: _context, barrierColor: AppColor.colorDefaultCupertinoActionSheet, - builder: (context) => CupertinoActionSheet( + builder: (context) => PointerInterceptor(child: CupertinoActionSheet( actions: [ Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10), @@ -45,27 +61,27 @@ class ConfirmationDialogActionSheetBuilder { child: Text( _messageText ?? '', textAlign: TextAlign.center, - style: TextStyle(fontSize: 14, color: AppColor.colorMessageConfirmDialog)), + style: _styleMessage ?? TextStyle(fontSize: 14, color: AppColor.colorMessageConfirmDialog)), onPressed: () => {}, ) ), Container( - color: Colors.white, - child: CupertinoActionSheetAction( - child: Text( - _confirmText ?? '', - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20, color: AppColor.colorActionDeleteConfirmDialog)), - onPressed: () => _onConfirmActionClick?.call(), - ) + color: Colors.white, + child: CupertinoActionSheetAction( + child: Text( + _confirmText ?? '', + style: _styleConfirmButton ?? TextStyle(fontWeight: FontWeight.w500, fontSize: 20, color: AppColor.colorActionDeleteConfirmDialog)), + onPressed: () => _onConfirmActionClick?.call(), + ) ), ], cancelButton: CupertinoActionSheetAction( child: Text( _cancelText ?? '', - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20, color: AppColor.colorActionCancelDialog)), + style: _styleCancelButton ?? TextStyle(fontWeight: FontWeight.w500, fontSize: 20, color: AppColor.colorActionCancelDialog)), onPressed: () => _onCancelActionClick?.call(), ), - ) + )) ); } } \ No newline at end of file diff --git a/core/lib/presentation/views/button/icon_button_web.dart b/core/lib/presentation/views/button/icon_button_web.dart new file mode 100644 index 000000000..56db2a026 --- /dev/null +++ b/core/lib/presentation/views/button/icon_button_web.dart @@ -0,0 +1,43 @@ + +import 'package:flutter/material.dart'; + +typedef IconWebCallback = void Function(); +typedef IconWebHasPositionCallback = void Function(RelativeRect); + +Widget buildIconWeb({ + required Widget icon, + String? tooltip, + IconWebCallback? onTap, + EdgeInsets? iconPadding, + double? iconSize, +}) { + return Material( + type: MaterialType.circle, + color: Colors.transparent, + child: IconButton(icon: icon, iconSize: iconSize, padding: iconPadding ?? EdgeInsets.all(8.0), tooltip: tooltip, onPressed: onTap) + ); +} + +Widget buildIconWebHasPosition(BuildContext context, { + required Widget icon, + String? tooltip, + IconWebHasPositionCallback? onTap +}) { + return GestureDetector( + onTapDown: (detail) { + final screenSize = MediaQuery.of(context).size; + final offset = detail.globalPosition; + final position = RelativeRect.fromLTRB( + offset.dx, + offset.dy, + screenSize.width - offset.dx, + screenSize.height - offset.dy, + ); + onTap?.call(position); + }, + child: Tooltip( + message: tooltip, + child: icon, + ) + ); +} \ No newline at end of file diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index 57705110d..ac19397f1 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -16,6 +16,13 @@ class ConfirmDialogBuilder { String _confirmText = ''; String _cancelText = ''; Widget? _iconWidget; + Color? _colorCancelButton; + Color? _colorConfirmButton; + TextStyle? _styleTextCancelButton; + TextStyle? _styleTextConfirmButton; + TextStyle? _styleTitle; + TextStyle? _styleContent; + double? _radiusButton; OnConfirmButtonAction? _onConfirmButtonAction; OnCancelButtonAction? _onCancelButtonAction; @@ -39,6 +46,34 @@ class ConfirmDialogBuilder { _iconWidget = icon; } + void colorCancelButton(Color? color) { + _colorCancelButton = color; + } + + void colorConfirmButton(Color? color) { + _colorConfirmButton = color; + } + + void styleTextCancelButton(TextStyle? style) { + _styleTextCancelButton = style; + } + + void styleTextConfirmButton(TextStyle? style) { + _styleTextConfirmButton = style; + } + + void styleTitle(TextStyle? style) { + _styleTitle = style; + } + + void styleContent(TextStyle? style) { + _styleContent = style; + } + + void radiusButton(double? radius) { + _radiusButton = radius; + } + void onConfirmButtonAction(String confirmText, OnConfirmButtonAction? onConfirmButtonAction) { _confirmText = confirmText; _onConfirmButtonAction = onConfirmButtonAction; @@ -66,52 +101,57 @@ class ConfirmDialogBuilder { color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(16))), child: Wrap(children: [ - Align( - alignment: Alignment.centerRight, - child: IconButton( - padding: EdgeInsets.only(top: 16, right: 16), - onPressed: () => _onCloseButtonAction?.call(), - icon: SvgPicture.asset(_imagePath.icCloseMailbox, width: 30, height: 30, fit: BoxFit.fill))), + if (_onCloseButtonAction != null) + Align( + alignment: Alignment.centerRight, + child: IconButton( + padding: EdgeInsets.only(top: 16, right: 16), + onPressed: () => _onCloseButtonAction?.call(), + icon: SvgPicture.asset(_imagePath.icCloseMailbox, width: 30, height: 30, fit: BoxFit.fill))), if (_iconWidget != null) Container( margin: EdgeInsets.only(top: 24), alignment: Alignment.center, child: _iconWidget, ), - Padding( - padding: EdgeInsets.only(top: 12), - child: Center( - child: Text( - _title, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 20.0, color: AppColor.colorActionDeleteConfirmDialog, fontWeight: FontWeight.w500) + if (_title.isNotEmpty) + Padding( + padding: EdgeInsets.only(top: 12), + child: Center( + child: Text( + _title, + textAlign: TextAlign.center, + style: _styleTitle ?? TextStyle(fontSize: 20.0, color: AppColor.colorActionDeleteConfirmDialog, fontWeight: FontWeight.w500) + ) ) - ) - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24), - child: Center( - child: Text(_content, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 17.0, color: AppColor.colorMessageDialog) + ), + if (_content.isNotEmpty) + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Center( + child: Text(_content, + textAlign: TextAlign.center, + style: _styleContent ?? TextStyle(fontSize: 17.0, color: AppColor.colorMessageDialog) + ), ), ), - ), Padding( padding: EdgeInsets.only(bottom: 16, left: 16, right: 16), child: Row( children: [ - Expanded( - child: _buildButton(name: _cancelText, action: _onCancelButtonAction) - ), + Expanded(child: _buildButton( + name: _cancelText, + bgColor: _colorCancelButton, + radius: _radiusButton, + textStyle: _styleTextCancelButton, + action: _onCancelButtonAction)), SizedBox(width: 16), - Expanded( - child: _buildButton( - name: _confirmText, - bgColor: AppColor.colorConfirmActionDialog, - nameColor: AppColor.colorActionDeleteConfirmDialog, - action: _onConfirmButtonAction) - ) + Expanded(child: _buildButton( + name: _confirmText, + bgColor: _colorConfirmButton, + radius: _radiusButton, + textStyle: _styleTextConfirmButton, + action: _onConfirmButtonAction)) ] )) ]) @@ -120,7 +160,7 @@ class ConfirmDialogBuilder { } Widget _buildButton({ - String? name, Color? nameColor, Color? bgColor, Function? action + String? name, TextStyle? textStyle, Color? bgColor, double? radius, Function? action }) { return SizedBox( width: double.infinity, @@ -133,7 +173,7 @@ class ConfirmDialogBuilder { backgroundColor: MaterialStateProperty.resolveWith( (Set states) => bgColor ?? AppColor.colorTextButton), shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(radius ?? 8), side: BorderSide(width: 0, color: bgColor ?? AppColor.colorTextButton), )), padding: MaterialStateProperty.resolveWith( @@ -141,7 +181,7 @@ class ConfirmDialogBuilder { elevation: MaterialStateProperty.resolveWith((Set states) => 0)), child: Text(name ?? '', textAlign: TextAlign.center, - style: TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: nameColor ?? Colors.white)), + style: textStyle ?? TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)), ) ); } diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart index 893eb1568..22dd8a3d2 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart'; import 'package:flutter/material.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'dart:developer' as developer; import 'package:universal_html/html.dart' as html; import 'package:core/presentation/utils/shims/dart_ui.dart' as ui; @@ -221,10 +222,10 @@ class _HtmlContentViewerOnWebState extends State { future: webInit, builder: (context, snapshot) { if (snapshot.hasData) { - return HtmlElementView( + return PointerInterceptor(child: HtmlElementView( key: ValueKey(htmlData), viewType: createdViewId, - ); + )); } else { return Container(); } diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 4d1cceeff..59d8e669b 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -74,6 +74,8 @@ dependencies: http: 0.13.4 + pointer_interceptor: 0.9.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index ef8d7b908..0ddc7dbce 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -127,6 +127,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 9827841e5..82f179931 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -19,8 +19,10 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/auto_complete_pattern.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; @@ -35,15 +37,16 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_inter import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_mutiple_attachment_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_action.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:uuid/uuid.dart'; class ComposerController extends BaseController { @@ -51,11 +54,14 @@ class ComposerController extends BaseController { final mailboxDashBoardController = Get.find(); final expandMode = ExpandMode.COLLAPSE.obs; + final expandModeAttachments = ExpandMode.EXPAND.obs; final composerArguments = Rxn(); final isEnableEmailSendButton = false.obs; final isInitialRecipient = false.obs; final attachments = [].obs; final emailContents = [].obs; + final subjectEmail = Rxn(); + final screenDisplayMode = ScreenDisplayMode.normal.obs; final SendEmailInteractor _sendEmailInteractor; final SaveEmailAddressesInteractor _saveEmailAddressInteractor; @@ -63,6 +69,7 @@ class ComposerController extends BaseController { final GetAutoCompleteWithDeviceContactInteractor _getAutoCompleteWithDeviceContactInteractor; final AppToast _appToast; final ImagePaths _imagePaths; + final ResponsiveUtils responsiveUtils; final Uuid _uuid; final LocalFilePickerInteractor _localFilePickerInteractor; final UploadMultipleAttachmentInteractor _uploadMultipleAttachmentInteractor; @@ -74,7 +81,6 @@ class ComposerController extends BaseController { List listToEmailAddress = []; List listCcEmailAddress = []; List listBccEmailAddress = []; - String? _subjectEmail; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.localContact; HtmlEditorApi? htmlEditorApi; final HtmlEditorBrowser.HtmlEditorController htmlControllerBrowser = HtmlEditorBrowser.HtmlEditorController(); @@ -85,8 +91,13 @@ class ComposerController extends BaseController { final bccEmailAddressController = TextEditingController(); List initialAttachments = []; + String? _textEditor; - void setSubjectEmail(String subject) => _subjectEmail = subject; + void setTextEditor(String? text) => _textEditor = text; + + String? get textEditor => _textEditor; + + void setSubjectEmail(String subject) => subjectEmail.value = subject; Future _getEmailBodyText({bool onlyText = false}) async { if (kIsWeb) { @@ -107,6 +118,7 @@ class ComposerController extends BaseController { this._getAutoCompleteWithDeviceContactInteractor, this._appToast, this._imagePaths, + this.responsiveUtils, this._uuid, this._deviceInfoPlugin, this._localFilePickerInteractor, @@ -178,7 +190,7 @@ class ComposerController extends BaseController { } void _initEmail() { - final arguments = Get.arguments; + final arguments = kIsWeb ? mailboxDashBoardController.routerArguments : Get.arguments; if (arguments is ComposerArguments) { composerArguments.value = arguments; if (arguments.emailActionType == EmailActionType.edit) { @@ -211,6 +223,14 @@ class ComposerController extends BaseController { return ''; } + String? get initTextEditorComposer { + if (composerArguments.value?.emailActionType == EmailActionType.compose) { + return textEditor; + } else { + return textEditor ?? getContentEmail(); + } + } + Tuple2? _getHeaderEmailQuoted(String locale, ComposerArguments arguments) { if (arguments.presentationEmail != null) { final sentDate = arguments.presentationEmail?.sentAt; @@ -226,8 +246,9 @@ class ComposerController extends BaseController { } void _initToEmailAddress(ComposerArguments arguments) { - if (arguments.presentationEmail != null) { - final userEmailAddress = EmailAddress(null, arguments.userProfile.email); + final userProfile = mailboxDashBoardController.userProfile.value; + if (arguments.presentationEmail != null && userProfile != null) { + final userEmailAddress = EmailAddress(null, userProfile.email); final recipients = arguments.presentationEmail!.generateRecipientsEmailAddressForComposer(arguments.emailActionType, arguments.mailboxRole); @@ -302,12 +323,12 @@ class ComposerController extends BaseController { return emailQuotedHtml; } - Future _generateEmail(ComposerArguments arguments, {bool asDrafts = false}) async { + Future _generateEmail(Map mapDefaultMailboxId, UserProfile userProfile, {bool asDrafts = false}) async { final generateEmailId = EmailId(Id(_uuid.v1())); - final outboxMailboxId = arguments.mapMailboxId[PresentationMailbox.roleOutbox]; - final draftMailboxId = arguments.mapMailboxId[PresentationMailbox.roleDrafts]; + final outboxMailboxId = mapDefaultMailboxId[PresentationMailbox.roleOutbox]; + final draftMailboxId = mapDefaultMailboxId[PresentationMailbox.roleDrafts]; final listFromEmailAddress = { - EmailAddress(null, arguments.userProfile.email) + EmailAddress(null, userProfile.email) }; final generatePartId = PartId(_uuid.v1()); final generateBlobId = Id(_uuid.v1()); @@ -323,7 +344,7 @@ class ComposerController extends BaseController { cc: listCcEmailAddress.toSet(), bcc: listBccEmailAddress.toSet(), keywords: asDrafts ? {KeyWordIdentifier.emailDraft : true} : null, - subject: _subjectEmail, + subject: subjectEmail.value, htmlBody: { EmailBodyPart( partId: generatePartId, @@ -363,30 +384,44 @@ class ComposerController extends BaseController { clearFocusEditor(context); if (isEnableEmailSendButton.value) { - final arguments = composerArguments.value; - if (arguments != null) { - _saveEmailAddress(); - - final email = await _generateEmail(arguments); - final accountId = arguments.session.accounts.keys.first; - final sentMailboxId = arguments.mapMailboxId[PresentationMailbox.roleSent]; - final submissionCreateId = Id(_uuid.v1()); - - mailboxDashBoardController.consumeState(_sendEmailInteractor.execute( - accountId, - EmailRequest( - email, - submissionCreateId, - mailboxIdSaved: sentMailboxId, - emailIdDestroyed: arguments.emailActionType == EmailActionType.edit ? arguments.presentationEmail?.id : null))); + if (subjectEmail.value?.isNotEmpty == true) { + _handleSendMessages(context); + } else { + _showConfirmDialogSendEmail(context); } - - popBack(); } else { _appToast.showErrorToast(AppLocalizations.of(context).your_email_should_have_at_least_one_recipient); } } + void _handleSendMessages(BuildContext context) async { + final arguments = composerArguments.value; + final session = mailboxDashBoardController.sessionCurrent; + final mapDefaultMailboxId = mailboxDashBoardController.mapDefaultMailboxId; + final userProfile = mailboxDashBoardController.userProfile.value; + if (arguments != null && session != null && mapDefaultMailboxId.isNotEmpty && userProfile != null) { + _saveEmailAddress(); + + final email = await _generateEmail(mapDefaultMailboxId, userProfile); + final accountId = session.accounts.keys.first; + final sentMailboxId = mapDefaultMailboxId[PresentationMailbox.roleSent]; + final submissionCreateId = Id(_uuid.v1()); + + mailboxDashBoardController.consumeState(_sendEmailInteractor.execute( + accountId, + EmailRequest(email, submissionCreateId, mailboxIdSaved: sentMailboxId, + emailIdDestroyed: arguments.emailActionType == EmailActionType.edit + ? arguments.presentationEmail?.id + : null))); + } + + if (kIsWeb) { + closeComposerWeb(); + } else { + popBack(); + } + } + void _saveEmailAddress() async { final listEmailAddressCanSave = Set(); listEmailAddressCanSave.addAll(listToEmailAddress + listCcEmailAddress + listBccEmailAddress); @@ -460,10 +495,10 @@ class ComposerController extends BaseController { } void _uploadAttachmentsAction(List pickedFiles) async { - if (composerArguments.value != null) { - final accountId = composerArguments.value!.session.accounts.keys.first; - final uploadUrl = composerArguments.value!.session.getUploadUrl(accountId); - + final session = mailboxDashBoardController.sessionCurrent; + if (session != null) { + final accountId = session.accounts.keys.first; + final uploadUrl = session.getUploadUrl(accountId); consumeState(_uploadMultipleAttachmentInteractor.execute(pickedFiles, accountId, uploadUrl)); } } @@ -508,9 +543,9 @@ class ComposerController extends BaseController { final oldEmailBody = kIsWeb ? getContentEmail() : '\n${getContentEmail()}\n'; final isEmailBodyChanged = !oldEmailBody.isSame(newEmailBody); - final newEmailSubject = _subjectEmail; - final subjectEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? ''; - final oldEmailSubject = arguments.emailActionType.getSubjectComposer(Get.context!, subjectEmail); + final newEmailSubject = subjectEmail.value; + final titleEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? ''; + final oldEmailSubject = arguments.emailActionType.getSubjectComposer(Get.context!, titleEmail); final isEmailSubjectChanged = !oldEmailSubject.isSame(newEmailSubject); final recipients = arguments.presentationEmail @@ -544,13 +579,18 @@ class ComposerController extends BaseController { clearFocusEditor(context); final arguments = composerArguments.value; - if (arguments != null && Get.context != null) { + final mapDefaultMailboxId = mailboxDashBoardController.mapDefaultMailboxId; + final userProfile = mailboxDashBoardController.userProfile.value; + final session = mailboxDashBoardController.sessionCurrent; + + if (arguments != null && Get.context != null && mapDefaultMailboxId.isNotEmpty + && userProfile != null && session != null) { final isChanged = await _isEmailChanged(arguments); if (isChanged) { _saveEmailAddress(); - final newEmail = await _generateEmail(arguments, asDrafts: true); - final accountId = arguments.session.accounts.keys.first; + final newEmail = await _generateEmail(mapDefaultMailboxId, userProfile, asDrafts: true); + final accountId = session.accounts.keys.first; final oldEmail = arguments.presentationEmail; if (arguments.emailActionType == EmailActionType.edit && oldEmail != null) { @@ -563,10 +603,10 @@ class ComposerController extends BaseController { } void _getEmailContentAction(ComposerArguments arguments) async { - final baseDownloadUrl = arguments.session.getDownloadUrl(); - final accountId = arguments.session.accounts.keys.first; + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(); + final accountId = mailboxDashBoardController.sessionCurrent?.accounts.keys.first; final emailId = arguments.presentationEmail?.id; - if (emailId != null) { + if (emailId != null && baseDownloadUrl != null && accountId != null) { consumeState(_getEmailContentInteractor.execute(accountId, emailId, baseDownloadUrl)); } } @@ -594,7 +634,7 @@ class ComposerController extends BaseController { if (arguments.emailActionType == EmailActionType.edit) { return arguments.presentationEmail?.from?.first.emailAddress ?? ''; } else { - return arguments.userProfile.email; + return mailboxDashBoardController.userProfile.value?.email ?? ''; } } return ''; @@ -603,6 +643,69 @@ class ComposerController extends BaseController { void clearFocusEditor(BuildContext context) { if (!kIsWeb) { htmlEditorApi?.unfocus(context); + htmlControllerBrowser.clearFocus(); + } + } + + void closeComposerWeb() { + mailboxDashBoardController.dispatchDashBoardAction(DashBoardAction.none); + } + + void minimizeComposer() { + _updateTextForEditor(); + final newDisplayMode = screenDisplayMode.value == ScreenDisplayMode.minimize ? ScreenDisplayMode.normal : ScreenDisplayMode.minimize; + screenDisplayMode.value = newDisplayMode; + } + + void setFullScreenComposer() { + _updateTextForEditor(); + final newDisplayMode = screenDisplayMode.value == ScreenDisplayMode.fullScreen ? ScreenDisplayMode.normal : ScreenDisplayMode.fullScreen; + screenDisplayMode.value = newDisplayMode; + } + + void _updateTextForEditor() async { + final textCurrent = await htmlControllerBrowser.getText(); + log('_updateTextForEditor() | textCurrent: $textCurrent'); + htmlControllerBrowser.insertHtml(textCurrent); + } + + void deleteComposer() { + mailboxDashBoardController.dispatchDashBoardAction(DashBoardAction.none); + } + + void toggleDisplayAttachments() { + final newExpandMode = expandModeAttachments.value == ExpandMode.COLLAPSE ? ExpandMode.EXPAND : ExpandMode.COLLAPSE; + expandModeAttachments.value = newExpandMode; + } + + void _showConfirmDialogSendEmail(BuildContext context) { + if (responsiveUtils.isMobileDevice(context)) { + (ConfirmationDialogActionSheetBuilder(context) + ..messageText(AppLocalizations.of(context).message_confirm_send_email) + ..styleConfirmButton(TextStyle(fontSize: 20, fontWeight: FontWeight.normal, color: Colors.black)) + ..onCancelAction(AppLocalizations.of(context).cancel, () => popBack()) + ..onConfirmAction(AppLocalizations.of(context).send, () { + popBack(); + _handleSendMessages(context); + })).show(); + } else { + showDialog( + context: context, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + builder: (BuildContext context) => PointerInterceptor(child: (ConfirmDialogBuilder(_imagePaths) + ..key(Key('confirm_dialog_send_message')) + ..content(AppLocalizations.of(context).message_confirm_send_email) + ..colorConfirmButton(AppColor.colorTextButton) + ..colorCancelButton(AppColor.colorCancelButton) + ..styleContent(TextStyle(fontSize: 15, fontWeight: FontWeight.normal, color: AppColor.colorMessageDialog)) + ..styleTextCancelButton(TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) + ..styleTextConfirmButton(TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) + ..onConfirmButtonAction(AppLocalizations.of(context).send, () { + popBack(); + _handleSendMessages(context); + }) + ..onCancelButtonAction(AppLocalizations.of(context).cancel, () => popBack())) + .build())); } } diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart new file mode 100644 index 000000000..6638d92ad --- /dev/null +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -0,0 +1,529 @@ +import 'package:core/core.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:html_editor_enhanced/html_editor.dart' as HtmlEditorBrowser; +import 'package:model/model.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_file_composer_builder.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_input_builder.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class ComposerView extends GetWidget { + + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); + final appToast = Get.find(); + final keyboardUtils = Get.find(); + + @override + Widget build(BuildContext context) { + return ResponsiveWidget( + responsiveUtils: responsiveUtils, + mobile: Scaffold( + backgroundColor: Colors.white, + body: Align(alignment: Alignment.center, child: Card( + color: Colors.transparent, + child: Container( + color: Colors.white, + child: PointerInterceptor(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() => _buildAppBarForMobile(context, controller.isEnableEmailSendButton.value)), + SizedBox(height: 16), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildEmailAddress(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildSubjectEmail(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Expanded(child: Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: _buildEditorAndAttachments(context))), + ] + )) + ) + )) + ), + desktop: Obx(() { + return Stack(children: [ + if (controller.screenDisplayMode == ScreenDisplayMode.normal) + Positioned(right: 10, bottom: 10, child: Card( + elevation: 20, + color: Colors.transparent, + child: Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + width: responsiveUtils.getSizeWidthScreen(context) * 0.5, + height: responsiveUtils.getSizeHeightScreen(context) * 0.65, + child: PointerInterceptor(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 5), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildAppBar(context)), + SizedBox(height: 10), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildTitleComposer(context)), + SizedBox(height: 16), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildEmailAddress(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildSubjectEmail(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Expanded(child: Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: _buildEditorAndAttachments(context))), + Obx(() => _buildBottomBar(context, controller.isEnableEmailSendButton.value)), + ] + ) + ) + ) + )), + if (controller.screenDisplayMode == ScreenDisplayMode.minimize) + Positioned(right: 10, bottom: 10, child: Card( + elevation: 20, + color: Colors.transparent, + child: Container( + decoration: BoxDecoration(color: AppColor.colorTextButton, borderRadius: BorderRadius.all(Radius.circular(20))), + width: 500, + height: 50, + child: PointerInterceptor(child: Row( + children: [ + Expanded(child: Padding( + padding: EdgeInsets.only(left: 20, right: 16), + child: _buildTitleComposer(context), + )), + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icMinimizeComposer, color: Colors.white, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).minimize, + onTap: () => controller.minimizeComposer()), + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icFullScreenComposer, color: Colors.white, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).fullscreen, + onTap: () => controller.setFullScreenComposer()), + Padding( + padding: EdgeInsets.only(right: 8), + child: buildIconWeb( + icon: SvgPicture.asset(imagePaths.icCloseComposer, color: Colors.white, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).close, + onTap: () => controller.closeComposerWeb())), + ] + )) + ) + )), + if (controller.screenDisplayMode == ScreenDisplayMode.fullScreen) + Scaffold( + backgroundColor: Colors.black38, + body: Align(alignment: Alignment.center, child: Card( + color: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), + child: Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + width: responsiveUtils.getSizeWidthScreen(context) * 0.8, + height: responsiveUtils.getSizeHeightScreen(context) * 0.85, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(20)), + child: PointerInterceptor(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 5), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildAppBar(context)), + SizedBox(height: 10), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildTitleComposer(context)), + SizedBox(height: 16), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildEmailAddress(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildSubjectEmail(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Expanded(child: Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: _buildEditorAndAttachments(context))), + Obx(() => _buildBottomBar(context, controller.isEnableEmailSendButton.value)), + ] + )) + ) + ) + ) + ) + ) + ]); + }), + tablet: Scaffold( + backgroundColor: Colors.black38, + body: Align(alignment: Alignment.center, child: Card( + color: Colors.transparent, + child: Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(20)), + child: PointerInterceptor(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 5), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildAppBar(context)), + SizedBox(height: 10), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildTitleComposer(context)), + SizedBox(height: 16), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildEmailAddress(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: _buildSubjectEmail(context)), + Padding(padding: EdgeInsets.symmetric(horizontal: 20), child: Divider(color: AppColor.colorDividerComposer, height: 1)), + Expanded(child: Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: _buildEditorAndAttachments(context))), + Obx(() => _buildBottomBar(context, controller.isEnableEmailSendButton.value)), + ] + )) + ) + ) + )) + ), + ); + } + + Widget _buildAppBar(BuildContext context) { + return Transform( + transform: Matrix4.translationValues(15.0, 0.0, 0.0), + child: Row( + children: [ + Spacer(), + if (responsiveUtils.isDesktop(context)) + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icMinimizeComposer, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).minimize, + onTap: () => controller.minimizeComposer()), + if (responsiveUtils.isDesktop(context)) + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icFullScreenComposer, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).fullscreen, + onTap: () => controller.setFullScreenComposer()), + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icCloseComposer, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).close, + onTap: () => controller.closeComposerWeb()), + ] + ), + ); + } + + Widget _buildAppBarForMobile(BuildContext context, bool isEnableSendButton) { + return Container( + padding: EdgeInsets.only(left: 10, right: 10, top: 10), + color: Colors.white, + child: Row( + children: [ + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icCloseComposer, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).close, + onTap: () => controller.closeComposerWeb()), + Spacer(), + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icAttachmentsComposer, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).pick_attachments, + onTap: () => controller.openFilePickerByType(context, FileType.any)), + buildIconWeb( + icon: SvgPicture.asset( + imagePaths.icSendComposer, + color: isEnableSendButton ? AppColor.colorTextButton : AppColor.colorDisableMailboxCreateButton, + fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).send, + onTap: () => controller.sendEmailAction(context)), + ] + ), + ); + } + + Widget _buildBottomBar(BuildContext context, bool isEnableSendButton) { + return Container( + padding: EdgeInsets.only(left: 10, right: 20, top: 18, bottom: 18), + color: AppColor.colorBottomBarComposer, + child: Row( + children: [ + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icDeleteComposer, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).delete, + onTap: () => controller.deleteComposer()), + Spacer(), + buildIconWeb( + icon: SvgPicture.asset(imagePaths.icAttachmentsComposer, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).pick_attachments, + onTap: () => controller.openFilePickerByType(context, FileType.any)), + SizedBox(width: 16), + SizedBox( + width: 102, + height: 36, + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith( + (states) => isEnableSendButton ? AppColor.colorTextButton : AppColor.colorDisableMailboxCreateButton), + padding: MaterialStateProperty.resolveWith((states) => EdgeInsets.symmetric(horizontal: 16, vertical: 10)), + elevation: MaterialStateProperty.resolveWith((states) => 0), + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(width: 0, color: isEnableSendButton ? AppColor.colorTextButton : AppColor.colorDisableMailboxCreateButton)))), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text(AppLocalizations.of(context).send.toUpperCase(), style: TextStyle(fontSize: 17, color: Colors.white, fontWeight: FontWeight.w500)), + SizedBox(width: 5), + SvgPicture.asset(imagePaths.icSendComposer, width: 18, height: 18, fit: BoxFit.fill), + ]), + onPressed: () => controller.sendEmailAction(context) + ), + ) + ] + ), + ); + } + + Widget _buildTitleComposer(BuildContext context) { + return Obx(() => Text( + controller.subjectEmail.isNotEmpty == true + ? controller.subjectEmail.value ?? '' + : AppLocalizations.of(context).new_message.capitalizeFirstEach, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500, + color: controller.screenDisplayMode == ScreenDisplayMode.minimize ? Colors.white : Colors.black), + )); + } + + Widget _buildEmailAddress(BuildContext context) { + return Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(right: 8), + child: Text( + '${AppLocalizations.of(context).from_email_address_prefix}:', + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle(fontSize: 15, color: AppColor.colorHintEmailAddressInput))), + Expanded( + child: Obx(() => controller.composerArguments.value != null + ? Text( + '<${controller.getEmailAddressSender()}>', + style: TextStyle(fontSize: 16, color: AppColor.colorContentEmail, fontWeight: FontWeight.normal)) + : SizedBox.shrink() + ) + ) + ] + ) + ), + Divider(color: AppColor.colorDividerComposer, height: 1), + Obx(() => (EmailAddressInputBuilder( + context, + imagePaths, + appToast, + PrefixEmailAddress.to, + controller.listToEmailAddress, + controller: controller.toEmailAddressController, + isInitial: controller.isInitialRecipient.value, + expandMode: controller.expandMode.value) + ..addExpandAddressActionClick(() => controller.expandEmailAddressAction()) + ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) + ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) + .build() + ), + Obx(() => controller.expandMode.value == ExpandMode.EXPAND + ? Divider(color: AppColor.colorDividerComposer, height: 1) + : SizedBox.shrink()), + Obx(() => controller.expandMode.value == ExpandMode.EXPAND + ? (EmailAddressInputBuilder( + context, + imagePaths, + appToast, + PrefixEmailAddress.cc, + controller.listCcEmailAddress, + controller: controller.ccEmailAddressController, + isInitial: controller.isInitialRecipient.value,) + ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) + ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) + .build() + : SizedBox.shrink() + ), + Obx(() => controller.expandMode.value == ExpandMode.EXPAND + ? Divider(color: AppColor.colorDividerComposer, height: 1) + : SizedBox.shrink()), + Obx(() => controller.expandMode.value == ExpandMode.EXPAND + ? (EmailAddressInputBuilder( + context, + imagePaths, + appToast, + PrefixEmailAddress.bcc, + controller.listBccEmailAddress, + controller: controller.bccEmailAddressController, + isInitial: controller.isInitialRecipient.value,) + ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) + ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) + .build() + : SizedBox.shrink() + ), + ], + ); + } + + Widget _buildSubjectEmail(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(right: 8, top: 16), + child: Text( + '${AppLocalizations.of(context).subject_email}:', + style: TextStyle(fontSize: 15, color: AppColor.colorHintEmailAddressInput))), + Expanded(child: (TextFieldBuilder() + ..key(Key('subject_email_input')) + ..cursorColor(AppColor.colorTextButton) + ..onChange((value) => controller.setSubjectEmail(value)) + ..textStyle(TextStyle(color: AppColor.colorEmailAddress, fontSize: 15)) + ..textDecoration(InputDecoration(contentPadding: EdgeInsets.zero, border: InputBorder.none)) + ..addController(controller.subjectEmailInputController)) + .build() + ) + ] + ); + } + + Widget _buildEditorAndAttachments(BuildContext context) { + return SingleChildScrollView( + physics: ClampingScrollPhysics(), + child: Column( + children: [ + Obx(() => controller.attachments.isNotEmpty + ? Padding( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 10), + child: _buildAttachmentsTitle(context, controller.attachments, controller.expandModeAttachments.value)) + : SizedBox.shrink()), + Obx(() => controller.attachments.isEmpty + ? _buildAttachmentsLoadingView() + : SizedBox.shrink()), + Obx(() => controller.attachments.isNotEmpty + ? Padding( + padding: EdgeInsets.only(bottom: 8, left: 10, right: 10), + child: _buildAttachmentsList(context, controller.attachments, controller.expandModeAttachments.value)) + : SizedBox.shrink()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: _buildComposerEditor(context)), + ] + ) + ); + } + + Widget _buildComposerEditor(BuildContext context) { + return Obx(() => HtmlEditorBrowser.HtmlEditor( + key: Key('composer_editor_web'), + controller: controller.htmlControllerBrowser, + htmlEditorOptions: HtmlEditorBrowser.HtmlEditorOptions( + hint: '', + initialText: controller.initTextEditorComposer, + autoAdjustHeight: true, + darkMode: false, + disableScrollbarEditor: true, + ), + htmlToolbarOptions: HtmlEditorBrowser.HtmlToolbarOptions( + toolbarPosition: HtmlEditorBrowser.ToolbarPosition.custom + ), + otherOptions: HtmlEditorBrowser.OtherOptions(height: 550), + callbacks: HtmlEditorBrowser.Callbacks(onBeforeCommand: (String? currentHtml) { + controller.setTextEditor(currentHtml); + }, onChangeContent: (String? changed) { + controller.setTextEditor(changed); + }), + )); + } + + Widget _buildAttachmentsLoadingView({EdgeInsets? padding, double? size}) { + return Obx(() => controller.viewState.value.fold( + (failure) => SizedBox.shrink(), + (success) => success is UploadingAttachmentState + ? Center(child: Padding( + padding: padding ?? EdgeInsets.all(10), + child: SizedBox( + width: size ?? 20, + height: size ?? 20, + child: CupertinoActivityIndicator(color: AppColor.colorTextButton)))) + : SizedBox.shrink())); + } + + Widget _buildAttachmentsTitle(BuildContext context, List attachments, ExpandMode expandModeAttachment) { + return Row( + children: [ + Text( + '${AppLocalizations.of(context).attachments} (${filesize(attachments.totalSize(), 0)}):', + style: TextStyle(fontSize: 12, color: AppColor.colorHintEmailAddressInput, fontWeight: FontWeight.normal)), + _buildAttachmentsLoadingView(padding: EdgeInsets.only(left: 16), size: 16), + Spacer(), + Material( + type: MaterialType.circle, + color: Colors.transparent, + child: TextButton( + child: Text( + expandModeAttachment == ExpandMode.EXPAND + ? AppLocalizations.of(context).hide + : '${AppLocalizations.of(context).show_all} (${attachments.length})', + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, color: AppColor.colorTextButton)), + onPressed: () => controller.toggleDisplayAttachments() + ) + ) + ], + ); + } + + Widget _buildAttachmentsList(BuildContext context, List attachments, ExpandMode expandMode) { + if (expandMode == ExpandMode.EXPAND) { + return LayoutBuilder(builder: (context, constraints) { + return GridView.builder( + key: Key('list_attachment_full'), + primary: false, + shrinkWrap: true, + itemCount: attachments.length, + gridDelegate: SliverGridDelegateFixedHeight( + height: 60, + crossAxisCount: _getMaxItemRowListAttachment(context, constraints), + crossAxisSpacing: 8.0, + mainAxisSpacing: 8.0), + itemBuilder: (context, index) => + (AttachmentFileComposerBuilder(context, imagePaths, attachments[index]) + ..addOnDeleteAttachmentAction((attachment) => controller.removeAttachmentAction(attachment))) + .build() + ); + }); + } else { + return LayoutBuilder(builder: (context, constraints) { + return Align( + alignment: Alignment.centerLeft, + child: SizedBox( + height: 60, + child: ListView.builder( + key: Key('list_attachment_minimize'), + shrinkWrap: true, + physics: ClampingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: attachments.length, + itemBuilder: (context, index) => + (AttachmentFileComposerBuilder(context, imagePaths, attachments[index], + itemMargin: EdgeInsets.only(right: 8), + maxWidth: _getMaxWidthItemListAttachment(context, constraints), + maxHeight: 60) + ..addOnDeleteAttachmentAction((attachment) => controller.removeAttachmentAction(attachment))) + .build() + ) + ) + ); + }); + } + } + + int _getMaxItemRowListAttachment(BuildContext context, BoxConstraints constraints) { + if (constraints.maxWidth < ResponsiveUtils.minTabletWidth) { + return 2; + } else if (constraints.maxWidth < ResponsiveUtils.minTabletLargeWidth) { + return 3; + } else { + return 4; + } + } + + double _getMaxWidthItemListAttachment(BuildContext context, BoxConstraints constraints) { + final currentWidth = constraints.maxWidth - 40; + return currentWidth / _getMaxItemRowListAttachment(context, constraints); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/screen_display_mode.dart b/lib/features/composer/presentation/model/screen_display_mode.dart new file mode 100644 index 000000000..074587b32 --- /dev/null +++ b/lib/features/composer/presentation/model/screen_display_mode.dart @@ -0,0 +1,6 @@ + +enum ScreenDisplayMode { + fullScreen, + minimize, + normal +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/attachment_file_composer_builder.dart b/lib/features/composer/presentation/widgets/attachment_file_composer_builder.dart new file mode 100644 index 000000000..671cacc2f --- /dev/null +++ b/lib/features/composer/presentation/widgets/attachment_file_composer_builder.dart @@ -0,0 +1,87 @@ + +import 'package:core/core.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnDeleteAttachmentAction = void Function(Attachment attachment); + +class AttachmentFileComposerBuilder { + + final BuildContext context; + final ImagePaths imagePaths; + final Attachment attachment; + final double? maxWidth; + final double? maxHeight; + final EdgeInsets? itemMargin; + + OnDeleteAttachmentAction? _onDeleteAttachmentAction; + + Widget? buttonAction; + + AttachmentFileComposerBuilder( + this.context, + this.imagePaths, + this.attachment, + { + this.maxWidth, + this.maxHeight, + this.itemMargin, + } + ); + + void addOnDeleteAttachmentAction(OnDeleteAttachmentAction onDeleteAttachmentAction) { + _onDeleteAttachmentAction = onDeleteAttachmentAction; + } + + Widget build() { + return Theme( + data: ThemeData( + splashColor: Colors.transparent , + highlightColor: Colors.transparent), + child: Container( + margin: itemMargin ?? EdgeInsets.zero, + padding: EdgeInsets.zero, + alignment: Alignment.center, + width: maxWidth, + height: maxHeight, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColor.colorInputBorderCreateMailbox), + color: Colors.white), + child: Stack(children: [ + ListTile( + contentPadding: EdgeInsets.only(right: 16), + onTap: () {}, + leading: Transform( + transform: Matrix4.translationValues(8.0, -3.0, 0.0), + child: SvgPicture.asset(imagePaths.icFileAttachment, fit: BoxFit.fill)), + title: Transform( + transform: Matrix4.translationValues(-4.0, -6.0, 0.0), + child: Text( + attachment.name ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 12, color: AppColor.colorNameEmail, fontWeight: FontWeight.w500), + )), + subtitle: attachment.size != null && attachment.size?.value != 0 + ? Transform( + transform: Matrix4.translationValues(-4.0, -5.0, 0.0), + child: Text( + filesize(attachment.size?.value, 0), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 10, color: AppColor.colorContentEmail, fontWeight: FontWeight.normal))) + : null, + ), + Positioned(right: -5, top: -5, child: buildIconWeb( + icon: SvgPicture.asset(imagePaths.icDeleteAttachment, fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).delete, + onTap: () => _onDeleteAttachmentAction?.call(attachment))) + ]), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/email_address_input_builder.dart b/lib/features/composer/presentation/widgets/email_address_input_builder.dart index b05fb4dba..20df7b44f 100644 --- a/lib/features/composer/presentation/widgets/email_address_input_builder.dart +++ b/lib/features/composer/presentation/widgets/email_address_input_builder.dart @@ -26,6 +26,7 @@ class EmailAddressInputBuilder { final List _listEmailAddress; final TextEditingController? controller; final bool? isInitial; + final bool hasAvatar; OnOpenExpandAddressActionClick? _onOpenExpandAddressActionClick; OnUpdateListEmailAddressAction? _onUpdateListEmailAddressAction; @@ -51,6 +52,7 @@ class EmailAddressInputBuilder { this._listEmailAddress, { this.isInitial, + this.hasAvatar = false, this.controller, this.expandMode = ExpandMode.COLLAPSE, } @@ -69,7 +71,7 @@ class EmailAddressInputBuilder { Expanded(child: _buildTagEditor()), if (_prefixEmailAddress == PrefixEmailAddress.to) Padding( - padding: EdgeInsets.only(top: 4), + padding: EdgeInsets.only(top: 8), child: _buildButtonExpandAddress()) ] ); @@ -79,12 +81,10 @@ class EmailAddressInputBuilder { return Material( type: MaterialType.circle, color: Colors.transparent, - child: IconButton( - icon: SvgPicture.asset( - expandMode == ExpandMode.EXPAND ? _imagePaths.icExpandAddress : _imagePaths.icMoreReceiver, - width: 20, - height: 20, - fit: BoxFit.fill), + child: TextButton( + child: Text( + expandMode == ExpandMode.EXPAND ? AppLocalizations.of(_context).hide : AppLocalizations.of(_context).details, + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 16, color: AppColor.colorTextButton)), onPressed: () => _onOpenExpandAddressActionClick?.call() ) ); @@ -98,6 +98,7 @@ class EmailAddressInputBuilder { keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.done, hasAddButton: false, + tagSpacing: 8, resetTextOnSubmitted: true, textStyle: TextStyle(color: AppColor.colorEmailAddress, fontSize: 14, fontWeight: FontWeight.w500), onSubmitted: (value) { @@ -117,16 +118,22 @@ class EmailAddressInputBuilder { tagBuilder: (context, index) => Padding( padding: EdgeInsets.only(top: kIsWeb ? 8 : 0), child: Chip( - labelPadding: EdgeInsets.only(left: 8, right: 8, bottom: 2), + labelPadding: EdgeInsets.only(left: 12, right: 12, bottom: 2), label: Text(_listEmail[index], maxLines: 1, overflow: TextOverflow.ellipsis), deleteIcon: SvgPicture.asset(_imagePaths.icDeleteEmailAddress, fit: BoxFit.fill), labelStyle: TextStyle(color: AppColor.colorEmailAddress, fontSize: 14, fontWeight: FontWeight.w500), backgroundColor: AppColor.emailAddressChipColor, - avatar: CircleAvatar( - backgroundColor: AppColor.colorTextButton, - child: Text( - _listEmail[index].isNotEmpty ? _listEmail[index][0].toUpperCase() : '', - style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500))), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(width: 0, color: AppColor.emailAddressChipColor), + ), + avatar: hasAvatar == true + ? CircleAvatar( + backgroundColor: AppColor.colorTextButton, + child: Text( + _listEmail[index].isNotEmpty ? _listEmail[index][0].toUpperCase() : '', + style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500))) + : null, onDeleted: () { setState(() => _listEmailAddress.removeAt(index)); _onUpdateListEmailAddressAction?.call(_prefixEmailAddress, _listEmailAddress); diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 34a4462bc..8fbeaf19e 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -2,6 +2,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -33,6 +34,7 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_ import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_action.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -480,16 +482,16 @@ class EmailController extends BaseController { void composeEmailFromEmailAddress(EmailAddress emailAddress) { popBack(); - if (canComposeEmail()) { - push( - AppRoutes.COMPOSER, - arguments: ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role, - session: mailboxDashBoardController.sessionCurrent!, - userProfile: mailboxDashBoardController.userProfile.value!, - mapMailboxId: mailboxDashBoardController.mapDefaultMailboxId)); + final arguments = ComposerArguments( + emailActionType: EmailActionType.composeFromEmailAddress, + emailAddress: emailAddress, + mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); + if (kIsWeb) { + if (mailboxDashBoardController.dashBoardAction != DashBoardAction.compose) { + mailboxDashBoardController.dispatchDashBoardAction(DashBoardAction.compose, arguments: arguments); + } + } else { + push(AppRoutes.COMPOSER, arguments: arguments); } } @@ -497,11 +499,6 @@ class EmailController extends BaseController { popBack(); } - bool canComposeEmail() => mailboxDashBoardController.sessionCurrent != null - && mailboxDashBoardController.userProfile.value != null - && mailboxDashBoardController.mapDefaultMailboxId.isNotEmpty - && mailboxDashBoardController.selectedEmail.value != null; - void backToThreadView() { attachmentsExpandMode.value = ExpandMode.COLLAPSE; emailAddressExpandMode.value = ExpandMode.COLLAPSE; @@ -513,30 +510,36 @@ class EmailController extends BaseController { if (emailActionType == EmailActionType.compose) { composeEmailAction(); } else { - if (canComposeEmail()) { - push( - AppRoutes.COMPOSER, - arguments: ComposerArguments( - emailActionType: emailActionType, - presentationEmail: mailboxDashBoardController.selectedEmail.value!, - emailContents: emailContents, - attachments: attachments, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role, - session: mailboxDashBoardController.sessionCurrent!, - userProfile: mailboxDashBoardController.userProfile.value!, - mapMailboxId: mailboxDashBoardController.mapDefaultMailboxId)); + final arguments = ComposerArguments( + emailActionType: emailActionType, + presentationEmail: mailboxDashBoardController.selectedEmail.value!, + emailContents: emailContents, + attachments: attachments, + mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); + + if (kIsWeb) { + if (mailboxDashBoardController.dashBoardAction != DashBoardAction.compose) { + mailboxDashBoardController.dispatchDashBoardAction(DashBoardAction.compose, arguments: arguments); + } + if (Get.currentRoute == AppRoutes.EMAIL) { + popBack(); + } + } else { + push(AppRoutes.COMPOSER, arguments: arguments); } } } void composeEmailAction() { - if (canComposeEmail()) { - push( - AppRoutes.COMPOSER, - arguments: ComposerArguments( - session: mailboxDashBoardController.sessionCurrent!, - userProfile: mailboxDashBoardController.userProfile.value!, - mapMailboxId: mailboxDashBoardController.mapDefaultMailboxId)); + if (kIsWeb) { + if (mailboxDashBoardController.dashBoardAction != DashBoardAction.compose) { + mailboxDashBoardController.dispatchDashBoardAction(DashBoardAction.compose, arguments: ComposerArguments()); + } + if (Get.currentRoute == AppRoutes.EMAIL) { + popBack(); + } + } else { + push(AppRoutes.COMPOSER, arguments: ComposerArguments()); } } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 0338c471a..91931833d 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -216,7 +216,7 @@ class EmailView extends GetView { mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: EdgeInsets.only(top: 20), + padding: EdgeInsets.only(top: kIsWeb ? 10: 20), child: Text( '${prefixEmailAddress.asName(context)}:', style: TextStyle(fontSize: 14, color: AppColor.colorEmailAddressPrefix))), diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 8f323f726..0335abb80 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -1,19 +1,15 @@ -import 'package:equatable/equatable.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; -class ComposerArguments with EquatableMixin { +class ComposerArguments extends RouterArguments { final EmailActionType emailActionType; final PresentationEmail? presentationEmail; final List? emailContents; final EmailAddress? emailAddress; final List? attachments; - final Session session; - final UserProfile userProfile; - final Map mapMailboxId; final Role? mailboxRole; ComposerArguments({ @@ -23,9 +19,6 @@ class ComposerArguments with EquatableMixin { this.attachments, this.mailboxRole, this.emailAddress, - required this.session, - required this.userProfile, - required this.mapMailboxId, }); @override @@ -35,8 +28,5 @@ class ComposerArguments with EquatableMixin { attachments, mailboxRole, emailAddress, - session, - userProfile, - mapMailboxId, ]; } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart b/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart index 5675ab258..3177bb14e 100644 --- a/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart +++ b/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart @@ -2,7 +2,9 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; typedef OnBackActionClick = void Function(); typedef OnEmailActionClick = void Function(PresentationEmail, EmailActionType); @@ -51,9 +53,9 @@ class AppBarMailWidgetBuilder { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ - if ((!_responsiveUtils.isDesktop(_context) && !_responsiveUtils.isTabletLarge(_context))) + if (_conditionShowBackButton()) _buildBackButton(), - if ((!_responsiveUtils.isDesktop(_context) && !_responsiveUtils.isTabletLarge(_context))) + if (_conditionShowBackButton()) Expanded(child: _buildMailboxName()), if (_presentationEmail != null) _buildListOptionButton(), ] @@ -62,6 +64,16 @@ class AppBarMailWidgetBuilder { ); } + bool _conditionShowBackButton() { + if (Get.currentRoute == AppRoutes.MAILBOX_DASHBOARD) { + return !_responsiveUtils.isDesktop(_context) && !_responsiveUtils.isTabletLarge(_context); + } else if (Get.currentRoute == AppRoutes.EMAIL) { + return _responsiveUtils.isMobileDevice(_context) || _responsiveUtils.isTablet(_context); + } else { + return false; + } + } + Widget _buildBackButton() { return IconButton( icon: SvgPicture.asset(_imagePaths.icBack, width: 20, height: 20, color: AppColor.colorTextButton, fit: BoxFit.fill), diff --git a/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart b/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart index d4a1b2a2d..3500b8027 100644 --- a/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart +++ b/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart @@ -78,7 +78,7 @@ class AttachmentFileTileBuilder { }}, leading: Transform( transform: Matrix4.translationValues(14.0, 2.0, 0.0), - child: SvgPicture.asset(_imagePaths.icAttachmentFile, width: 24, height: 24, fit: BoxFit.fill)), + child: SvgPicture.asset(_imagePaths.icFileAttachment, width: 24, height: 24, fit: BoxFit.fill)), title: Transform( transform: Matrix4.translationValues(-8.0, -8.0, 0.0), child: Text( diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 19b9a4853..46eeb415d 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -453,6 +453,8 @@ class MailboxController extends BaseMailboxController { ..title(AppLocalizations.of(context).delete_mailboxes) ..content(AppLocalizations.of(context).message_confirmation_dialog_delete_mailbox(presentationMailbox.name?.name ?? '')) ..addIcon(SvgPicture.asset(_imagePaths.icRemoveDialog, fit: BoxFit.fill)) + ..colorConfirmButton(AppColor.colorConfirmActionDialog) + ..styleTextConfirmButton(TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorActionDeleteConfirmDialog)) ..onCloseButtonAction(() => popBack()) ..onConfirmButtonAction(AppLocalizations.of(context).delete, () => _deleteMailboxAction(presentationMailbox)) ..onCancelButtonAction(AppLocalizations.of(context).cancel, () => popBack())) diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart index b67befe67..9d539c209 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart @@ -11,16 +11,20 @@ import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/email_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_user_profile_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_user_profile_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_action.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; class MailboxDashBoardController extends BaseController { @@ -36,6 +40,7 @@ class MailboxDashBoardController extends BaseController { final userProfile = Rxn(); final searchState = SearchState.initial().obs; final suggestionSearch = [].obs; + final dashBoardAction = DashBoardAction.none.obs; SearchQuery? searchQuery; Session? sessionCurrent; @@ -43,6 +48,7 @@ class MailboxDashBoardController extends BaseController { Map mapMailbox = Map(); TextEditingController searchInputController = TextEditingController(); FocusNode searchFocus = FocusNode(); + RouterArguments? routerArguments; MailboxDashBoardController( this._getUserProfileInteractor, @@ -225,6 +231,20 @@ class MailboxDashBoardController extends BaseController { } } + void dispatchDashBoardAction(DashBoardAction action, {RouterArguments? arguments}) { + switch(action) { + case DashBoardAction.none: + routerArguments = null; + Get.delete(); + break; + case DashBoardAction.compose: + routerArguments = arguments; + ComposerBindings().dependencies(); + break; + } + dashBoardAction.value = action; + } + @override void onClose() { searchInputController.dispose(); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart index 4721f6a65..9e83e3c28 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart @@ -1,9 +1,12 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart' + if (dart.library.html) 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_action.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart'; class MailboxDashBoardView extends GetWidget { @@ -30,35 +33,40 @@ class MailboxDashBoardView extends GetWidget { desktop: SizedBox.shrink() ), drawerEnableOpenDragGesture: !responsiveUtils.isDesktop(context), - body: ResponsiveWidget( - responsiveUtils: responsiveUtils, - desktop: Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(flex: 1, child: MailboxView()), - Expanded(flex: 1, child: ThreadView()), - Expanded(flex: 2, child: EmailView()), - ], - ), - ), - tabletLarge: Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - width: responsiveUtils.getSizeWidthScreen(context) * 0.35, - padding: EdgeInsets.symmetric(horizontal: 16), - child: ThreadView() + body: Stack(children: [ + ResponsiveWidget( + responsiveUtils: responsiveUtils, + desktop: Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(flex: 1, child: MailboxView()), + Expanded(flex: 1, child: ThreadView()), + Expanded(flex: 2, child: EmailView()), + ], ), - Expanded(child: EmailView()), - ], - ), - ), - tablet: ThreadView(), - mobile: ThreadView()), + ), + tabletLarge: Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: responsiveUtils.getSizeWidthScreen(context) * 0.35, + padding: EdgeInsets.symmetric(horizontal: 16), + child: ThreadView() + ), + Expanded(child: EmailView()), + ], + ), + ), + tablet: ThreadView(), + mobile: ThreadView()), + Obx(() => controller.dashBoardAction == DashBoardAction.compose + ? ComposerView() + : SizedBox.shrink()), + ]), ); } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/model/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/model/dashboard_action.dart new file mode 100644 index 000000000..7af9da24b --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/model/dashboard_action.dart @@ -0,0 +1,5 @@ + +enum DashBoardAction { + none, + compose +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 4fd9ad5c3..e6b6846b8 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1,7 +1,6 @@ -import 'dart:math'; - import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -28,6 +27,7 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_action.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; @@ -675,10 +675,6 @@ class ThreadController extends BaseController { } } - bool canComposeEmail() => mailboxDashBoardController.sessionCurrent != null - && mailboxDashBoardController.userProfile.value != null - && mailboxDashBoardController.mapDefaultMailboxId.isNotEmpty; - bool isSelectionEnabled() => currentSelectMode.value == SelectMode.ACTIVE; void pressEmailSelectionAction(BuildContext context, EmailActionType actionType, List selectionEmail) { @@ -709,27 +705,25 @@ class ThreadController extends BaseController { } void editEmail(PresentationEmail presentationEmail) { - if (canComposeEmail()) { - push( - AppRoutes.COMPOSER, - arguments: ComposerArguments( - emailActionType: EmailActionType.edit, - presentationEmail: presentationEmail, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role, - session: mailboxDashBoardController.sessionCurrent!, - userProfile: mailboxDashBoardController.userProfile.value!, - mapMailboxId: mailboxDashBoardController.mapDefaultMailboxId)); + final arguments = ComposerArguments( + emailActionType: EmailActionType.edit, + presentationEmail: presentationEmail, + mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); + + if (kIsWeb) { + mailboxDashBoardController.dispatchDashBoardAction(DashBoardAction.compose, arguments: arguments); + } else { + push(AppRoutes.COMPOSER, arguments: arguments); } } void composeEmailAction() { - if (canComposeEmail()) { - push( - AppRoutes.COMPOSER, - arguments: ComposerArguments( - session: mailboxDashBoardController.sessionCurrent!, - userProfile: mailboxDashBoardController.userProfile.value!, - mapMailboxId: mailboxDashBoardController.mapDefaultMailboxId)); + if (kIsWeb) { + if (mailboxDashBoardController.dashBoardAction != DashBoardAction.compose) { + mailboxDashBoardController.dispatchDashBoardAction(DashBoardAction.compose, arguments: ComposerArguments()); + } + } else { + push(AppRoutes.COMPOSER, arguments: ComposerArguments()); } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index c256b2937..acade40bd 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -773,4 +773,47 @@ class AppLocalizations { 'Email address copied to clipboard', name: 'email_address_copied_to_clipboard'); } + + String get minimize { + return Intl.message( + 'Minimize', + name: 'minimize'); + } + + String get fullscreen { + return Intl.message( + 'Fullscreen', + name: 'fullscreen'); + } + + String get close { + return Intl.message( + 'Close', + name: 'close'); + } + + String get send { + return Intl.message( + 'Send', + name: 'send'); + } + + String get attachments { + return Intl.message( + 'Attachments', + name: 'attachments'); + } + + String get show_all { + return Intl.message( + 'Show all', + name: 'show_all'); + } + + String get message_confirm_send_email { + return Intl.message( + 'Are you sure to send messages without a subject?', + name: 'message_confirm_send_email'); + } + } \ No newline at end of file diff --git a/lib/main/pages/app_pages.dart b/lib/main/pages/app_pages.dart index 07e837d25..3704ea6e7 100644 --- a/lib/main/pages/app_pages.dart +++ b/lib/main/pages/app_pages.dart @@ -1,6 +1,7 @@ import 'package:get/get_navigation/src/routes/get_route.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; -import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart' + if (dart.library.html) 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_bindings.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_view.dart'; import 'package:tmail_ui_user/features/home/presentation/home_bindings.dart'; diff --git a/lib/main/routes/router_arguments.dart b/lib/main/routes/router_arguments.dart new file mode 100644 index 000000000..03e7ef5d9 --- /dev/null +++ b/lib/main/routes/router_arguments.dart @@ -0,0 +1,3 @@ +import 'package:equatable/equatable.dart'; + +abstract class RouterArguments with EquatableMixin {} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 0eb28f40a..c2340e9fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -125,11 +125,16 @@ dependencies: url: https://github.com/Enough-Software/enough_html_editor.git # html_editor_enhanced: Compose email for Web Browser - html_editor_enhanced: 2.4.0+1 + html_editor_enhanced: + git: + url: https://github.com/dab246/html-editor-enhanced.git + ref: html_editor_for_web # fk_user_agent fk_user_agent: 2.1.0 + pointer_interceptor: 0.9.1 + dev_dependencies: flutter_test: sdk: flutter