diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 1ca116c7f..ca5e80620 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -234,6 +234,8 @@ extension AppColor on Color { static const colorStarredSearchFilterIcon = Color(0xFFFFCC00); static const colorMobileSearchFilterButton = Color(0xFFEBEDF0); static const colorContactViewClearFilterButton = Color(0x001C3D0D); + static const steelGrayA540 = Color(0xFF55687D); + static const blue700 = Color(0xFF208BFF); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 9c1c97009..516816322 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -35,6 +35,7 @@ import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; @@ -226,6 +227,8 @@ class ComposerBindings extends BaseBindings { )); Get.lazyPut(() => RestoreEmailInlineImagesInteractor( Get.find())); + Get.lazyPut(() => PrintEmailInteractor( + Get.find())); IdentityInteractorsBindings().dependencies(); } @@ -251,6 +254,7 @@ class ComposerBindings extends BaseBindings { 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 bad5c7ecf..9f1903e01 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -72,6 +72,7 @@ import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions. import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -127,6 +128,7 @@ class ComposerController extends BaseController final bccRecipientState = PrefixRecipientState.disabled.obs; final identitySelected = Rxn(); final listFromIdentities = RxList(); + final isEmailChanged = Rx(false); final LocalFilePickerInteractor _localFilePickerInteractor; final LocalImagePickerInteractor _localImagePickerInteractor; @@ -140,6 +142,7 @@ class ComposerController extends BaseController final GetAlwaysReadReceiptSettingInteractor _getAlwaysReadReceiptSettingInteractor; final CreateNewAndSendEmailInteractor _createNewAndSendEmailInteractor; final CreateNewAndSaveEmailToDraftsInteractor _createNewAndSaveEmailToDraftsInteractor; + final PrintEmailInteractor printEmailInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -196,6 +199,7 @@ class ComposerController extends BaseController ButtonState _closeComposerButtonState = ButtonState.enabled; ButtonState _saveToDraftButtonState = ButtonState.enabled; ButtonState _sendButtonState = ButtonState.enabled; + ButtonState printDraftButtonState = ButtonState.enabled; SignatureStatus _identityContentOnOpenPolicy = SignatureStatus.editedAvailable; int? _savedEmailDraftHash; bool _restoringSignatureButton = false; @@ -224,6 +228,7 @@ class ComposerController extends BaseController this._getAlwaysReadReceiptSettingInteractor, this._createNewAndSendEmailInteractor, this._createNewAndSaveEmailToDraftsInteractor, + this.printEmailInteractor, ); @override @@ -452,7 +457,7 @@ class ComposerController extends BaseController return null; } - final emailContent = await _getContentInEditor(); + final emailContent = await getContentInEditor(); return CreateEmailRequest( session: mailboxDashBoardController.sessionCurrent!, @@ -968,7 +973,7 @@ class ComposerController extends BaseController _handleSendMessages(context); } - Future _getContentInEditor() async { + Future getContentInEditor() async { try { final htmlTextEditor = PlatformInfo.isWeb ? _textEditorWeb @@ -977,7 +982,7 @@ class ComposerController extends BaseController ? htmlTextEditor!.removeEditorStartTag() : ''; } catch (e) { - logError('ComposerController::_getContentInEditor:Exception = $e'); + logError('ComposerController::getContentInEditor:Exception = $e'); return ''; } } @@ -997,7 +1002,7 @@ class ComposerController extends BaseController popBack(); } - final emailContent = await _getContentInEditor(); + final emailContent = await getContentInEditor(); final cancelToken = CancelToken(); final resultState = await _showSendingMessageDialog( emailContent: emailContent, @@ -1239,7 +1244,7 @@ class ComposerController extends BaseController } Future _hashDraftEmail() async { - final emailContent = await _getContentInEditor(); + final emailContent = await getContentInEditor(); final savedEmailDraft = SavedEmailDraft( subject: subjectEmail.value ?? '', @@ -1255,18 +1260,21 @@ class ComposerController extends BaseController return savedEmailDraft.hashCode; } + int get emptyDraftEmailHash => SavedEmailDraft.empty().hashCode; + Future _updateSavedEmailDraftHash() async { _savedEmailDraftHash = await _hashDraftEmail(); } Future _initEmailDraftHash() async { - if (composerArguments.value?.emailActionType != EmailActionType.compose - && composerArguments.value?.emailActionType != EmailActionType.editDraft - ) { - return; - } + final draftEmailHash = await _hashDraftEmail(); - _savedEmailDraftHash = await _hashDraftEmail(); + isEmailChanged.value = draftEmailHash != emptyDraftEmailHash; + + if (composerArguments.value?.emailActionType == EmailActionType.compose || + composerArguments.value?.emailActionType == EmailActionType.editDraft) { + _savedEmailDraftHash = await _hashDraftEmail(); + } } void handleClickSaveAsDraftsButton(BuildContext context) async { @@ -1287,7 +1295,7 @@ class ComposerController extends BaseController return; } - final emailContent = await _getContentInEditor(); + final emailContent = await getContentInEditor(); final cancelToken = CancelToken(); final resultState = await _showSavingMessageToDraftsDialog( emailContent: emailContent, @@ -2268,7 +2276,7 @@ class ComposerController extends BaseController popBack(); - final emailContent = await _getContentInEditor(); + final emailContent = await getContentInEditor(); final draftEmailId = _getDraftEmailId(); log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); final cancelToken = CancelToken(); diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index 51e9399f7..0535d5cb8 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/responsive/responsive_widget.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -6,6 +7,7 @@ import 'package:model/email/prefix_email_address.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/composer_print_draft_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/view/web/desktop_responsive_container_view.dart'; @@ -43,6 +45,7 @@ class ComposerView extends GetWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Obx(() => MobileResponsiveAppBarComposerWidget( + imagePaths: controller.imagePaths, isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions, @@ -509,6 +512,7 @@ class ComposerView extends GetWidget { ), ), Obx(() => BottomBarComposerWidget( + imagePaths: controller.imagePaths, isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, hasReadReceipt: controller.hasRequestReadReceipt.value, @@ -520,6 +524,8 @@ class ComposerView extends GetWidget { saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), + isPrintDraftEnabled: controller.isEmailChanged.isTrue, + onPrintDraftAction: () => controller.printDraft(context), )), ], ), @@ -788,6 +794,7 @@ class ComposerView extends GetWidget { ), ), Obx(() => BottomBarComposerWidget( + imagePaths: controller.imagePaths, isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled, isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled, hasReadReceipt: controller.hasRequestReadReceipt.value, @@ -799,6 +806,8 @@ class ComposerView extends GetWidget { saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context), sendMessageAction: () => controller.handleClickSendButton(context), requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context), + isPrintDraftEnabled: controller.isEmailChanged.isTrue, + onPrintDraftAction: () => controller.printDraft(context), )), ], ), @@ -893,6 +902,21 @@ class ComposerView extends GetWidget { } ) ), + if (controller.isEmailChanged.isTrue) + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + controller.imagePaths.icPrinter, + AppLocalizations.of(context).print, + colorIcon: AppColor.steelGrayA540, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + onCallbackAction: () { + popBack(); + controller.printDraft(context); + }, + ), + ), PopupMenuItem( padding: EdgeInsets.zero, child: PopupItemWidget( diff --git a/lib/features/composer/presentation/extensions/composer_print_draft_extension.dart b/lib/features/composer/presentation/extensions/composer_print_draft_extension.dart new file mode 100644 index 000000000..e560df517 --- /dev/null +++ b/lib/features/composer/presentation/extensions/composer_print_draft_extension.dart @@ -0,0 +1,102 @@ + +import 'package:core/domain/extensions/datetime_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/state/button_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/print_draft_dialog_view.dart'; +import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; +import 'package:tmail_ui_user/features/email/domain/state/print_email_state.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +extension ComposerPrintDraftExtension on ComposerController { + + Future printDraft(BuildContext context) async { + if (printDraftButtonState == ButtonState.disabled) { + log('ComposerPrintDraftExtension::printDraft: Printing draft'); + return; + } + printDraftButtonState = ButtonState.disabled; + + if (composerArguments.value == null || + mailboxDashBoardController.sessionCurrent == null || + mailboxDashBoardController.accountId.value == null) { + log('ComposerPrintDraftExtension::printDraft: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); + printDraftButtonState = ButtonState.enabled; + return; + } + + final locale = Localizations.localeOf(context); + final appLocalizations = AppLocalizations.of(context); + + final emailContent = await getContentInEditor(); + + final resultState = await _showPrintDraftsDialog( + appLocalizations: appLocalizations, + locale: locale, + emailContent: emailContent, + ); + + if (resultState is PrintEmailFailure) { + printDraftButtonState = ButtonState.enabled; + + if (context.mounted) { + appToast.showToastErrorMessage(context, appLocalizations.printingFailed); + } + } else if (resultState is PrintEmailSuccess) { + printDraftButtonState = ButtonState.enabled; + } + } + + Future _showPrintDraftsDialog({ + required AppLocalizations appLocalizations, + required Locale locale, + required String emailContent, + }) { + final presentationEmail = composerArguments.value!.presentationEmail; + + final childWidget = PointerInterceptor( + child: PrintDraftDialogView( + emailPrint: EmailPrint( + appName: appLocalizations.app_name, + userName: mailboxDashBoardController.userEmail, + attachments: uploadController.attachmentsUploaded, + emailContent: emailContent, + locale: locale.toLanguageTag(), + fromPrefix: appLocalizations.from_email_address_prefix, + toPrefix: appLocalizations.to_email_address_prefix, + ccPrefix: appLocalizations.cc_email_address_prefix, + bccPrefix: appLocalizations.bcc_email_address_prefix, + replyToPrefix: appLocalizations.replyToEmailAddressPrefix, + titleAttachment: appLocalizations.attachments.toLowerCase(), + toAddress: listToEmailAddress.toSet().toEscapeHtmlStringUseCommaSeparator(), + ccAddress: listCcEmailAddress.toSet().toEscapeHtmlStringUseCommaSeparator(), + bccAddress: listBccEmailAddress.toSet().toEscapeHtmlStringUseCommaSeparator(), + sender: composerArguments.value!.presentationEmail?.from?.isNotEmpty == true + ? composerArguments.value!.presentationEmail?.from!.first + : null, + receiveTime: presentationEmail?.getReceivedAt( + locale.toLanguageTag(), + pattern: presentationEmail.receivedAt + ?.value + .toLocal() + .toPatternForPrinting(locale.toLanguageTag()), + ) ?? '', + subject: presentationEmail?.subject, + ), + printEmailInteractor: printEmailInteractor, + ), + ); + + return Get.dialog( + childWidget, + barrierDismissible: false, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/saved_email_draft.dart b/lib/features/composer/presentation/model/saved_email_draft.dart index a39809032..47bd1ca4e 100644 --- a/lib/features/composer/presentation/model/saved_email_draft.dart +++ b/lib/features/composer/presentation/model/saved_email_draft.dart @@ -23,7 +23,20 @@ class SavedEmailDraft with EquatableMixin { required this.identity, required this.hasReadReceipt, }); - + + factory SavedEmailDraft.empty() { + return SavedEmailDraft( + subject: '', + content: '', + toRecipients: {}, + ccRecipients: {}, + bccRecipients: {}, + attachments: [], + identity: null, + hasReadReceipt: false, + ); + } + @override List get props => [ content, diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart index 6687d4392..7de92e121 100644 --- a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -1,13 +1,15 @@ +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class BottomBarComposerWidget extends StatelessWidget { + final ImagePaths imagePaths; final bool isCodeViewEnabled; + final bool isPrintDraftEnabled; final bool isFormattingOptionsEnabled; final bool hasReadReceipt; final VoidCallback openRichToolbarAction; @@ -18,12 +20,13 @@ class BottomBarComposerWidget extends StatelessWidget { final VoidCallback saveToDraftAction; final VoidCallback sendMessageAction; final VoidCallback requestReadReceiptAction; + final VoidCallback onPrintDraftAction; - final _imagePaths = Get.find(); - - BottomBarComposerWidget({ + const BottomBarComposerWidget({ super.key, + required this.imagePaths, required this.isCodeViewEnabled, + required this.isPrintDraftEnabled, required this.isFormattingOptionsEnabled, required this.hasReadReceipt, required this.openRichToolbarAction, @@ -34,6 +37,7 @@ class BottomBarComposerWidget extends StatelessWidget { required this.saveToDraftAction, required this.sendMessageAction, required this.requestReadReceiptAction, + required this.onPrintDraftAction, }); @override @@ -45,7 +49,7 @@ class BottomBarComposerWidget extends StatelessWidget { child: Row( children: [ TMailButtonWidget.fromIcon( - icon: _imagePaths.icRichToolbar, + icon: imagePaths.icRichToolbar, borderRadius: BottomBarComposerWidgetStyle.iconRadius, padding: BottomBarComposerWidgetStyle.richTextIconPadding, backgroundColor: isFormattingOptionsEnabled @@ -60,7 +64,7 @@ class BottomBarComposerWidget extends StatelessWidget { ), const SizedBox(width: BottomBarComposerWidgetStyle.space), TMailButtonWidget.fromIcon( - icon: _imagePaths.icAttachFile, + icon: imagePaths.icAttachFile, iconColor: BottomBarComposerWidgetStyle.iconColor, borderRadius: BottomBarComposerWidgetStyle.iconRadius, backgroundColor: Colors.transparent, @@ -73,7 +77,7 @@ class BottomBarComposerWidget extends StatelessWidget { AbsorbPointer( absorbing: isCodeViewEnabled, child: TMailButtonWidget.fromIcon( - icon: _imagePaths.icInsertImage, + icon: imagePaths.icInsertImage, iconColor: BottomBarComposerWidgetStyle.iconColor, borderRadius: BottomBarComposerWidgetStyle.iconRadius, backgroundColor: Colors.transparent, @@ -85,7 +89,7 @@ class BottomBarComposerWidget extends StatelessWidget { ), const SizedBox(width: BottomBarComposerWidgetStyle.space), TMailButtonWidget.fromIcon( - icon: _imagePaths.icStyleCodeView, + icon: imagePaths.icStyleCodeView, iconColor: isCodeViewEnabled ? BottomBarComposerWidgetStyle.selectedIconColor : BottomBarComposerWidgetStyle.iconColor, @@ -100,7 +104,7 @@ class BottomBarComposerWidget extends StatelessWidget { ), const Spacer(), TMailButtonWidget.fromIcon( - icon: _imagePaths.icDeleteMailbox, + icon: imagePaths.icDeleteMailbox, borderRadius: BottomBarComposerWidgetStyle.iconRadius, padding: BottomBarComposerWidgetStyle.iconPadding, iconSize: BottomBarComposerWidgetStyle.iconSize, @@ -109,7 +113,7 @@ class BottomBarComposerWidget extends StatelessWidget { ), const SizedBox(width: BottomBarComposerWidgetStyle.space), TMailButtonWidget.fromIcon( - icon: _imagePaths.icReadReceipt, + icon: imagePaths.icReadReceipt, borderRadius: BottomBarComposerWidgetStyle.iconRadius, padding: BottomBarComposerWidgetStyle.iconPadding, iconSize: BottomBarComposerWidgetStyle.iconSize, @@ -122,8 +126,21 @@ class BottomBarComposerWidget extends StatelessWidget { onTapActionCallback: requestReadReceiptAction, ), const SizedBox(width: BottomBarComposerWidgetStyle.space), + if (isPrintDraftEnabled) + TMailButtonWidget.fromIcon( + icon: imagePaths.icPrinter, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + iconColor: AppColor.steelGrayA540, + margin: const EdgeInsetsDirectional.only( + end: BottomBarComposerWidgetStyle.space, + ), + tooltipMessage: AppLocalizations.of(context).print, + onTapActionCallback: onPrintDraftAction, + ), TMailButtonWidget.fromIcon( - icon: _imagePaths.icSaveToDraft, + icon: imagePaths.icSaveToDraft, borderRadius: BottomBarComposerWidgetStyle.iconRadius, padding: BottomBarComposerWidgetStyle.iconPadding, iconSize: BottomBarComposerWidgetStyle.iconSize, @@ -133,7 +150,7 @@ class BottomBarComposerWidget extends StatelessWidget { const SizedBox(width: BottomBarComposerWidgetStyle.sendButtonSpace), TMailButtonWidget( text: AppLocalizations.of(context).send, - icon: _imagePaths.icSend, + icon: imagePaths.icSend, iconAlignment: TextDirection.rtl, padding: BottomBarComposerWidgetStyle.sendButtonPadding, iconSize: BottomBarComposerWidgetStyle.iconSize, diff --git a/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart index 5c7b0790e..90813f89d 100644 --- a/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -9,6 +8,7 @@ typedef OnOpenContextMenuAction = Function(RelativeRect position); class MobileResponsiveAppBarComposerWidget extends StatelessWidget { + final ImagePaths imagePaths; final bool isCodeViewEnabled; final bool isSendButtonEnabled; final bool isFormattingOptionsEnabled; @@ -19,10 +19,9 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { final OnOpenContextMenuAction openContextMenuAction; final VoidCallback openRichToolbarAction; - final _imagePaths = Get.find(); - - MobileResponsiveAppBarComposerWidget({ + const MobileResponsiveAppBarComposerWidget({ super.key, + required this.imagePaths, required this.isCodeViewEnabled, required this.isFormattingOptionsEnabled, required this.isSendButtonEnabled, @@ -43,7 +42,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { child: Row( children: [ TMailButtonWidget.fromIcon( - icon: _imagePaths.icCancel, + icon: imagePaths.icCancel, backgroundColor: Colors.transparent, tooltipMessage: AppLocalizations.of(context).saveAndClose, iconSize: MobileAppBarComposerWidgetStyle.iconSize, @@ -52,7 +51,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { ), const Spacer(), TMailButtonWidget.fromIcon( - icon: _imagePaths.icRichToolbar, + icon: imagePaths.icRichToolbar, padding: MobileAppBarComposerWidgetStyle.richTextIconPadding, backgroundColor: isFormattingOptionsEnabled ? MobileAppBarComposerWidgetStyle.selectedBackgroundColor @@ -66,7 +65,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { ), const SizedBox(width: MobileAppBarComposerWidgetStyle.space), TMailButtonWidget.fromIcon( - icon: _imagePaths.icAttachFile, + icon: imagePaths.icAttachFile, iconColor: MobileAppBarComposerWidgetStyle.iconColor, backgroundColor: Colors.transparent, iconSize: MobileAppBarComposerWidgetStyle.iconSize, @@ -76,7 +75,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { const SizedBox(width: MobileAppBarComposerWidgetStyle.space), if (!isCodeViewEnabled) TMailButtonWidget.fromIcon( - icon: _imagePaths.icInsertImage, + icon: imagePaths.icInsertImage, iconColor: MobileAppBarComposerWidgetStyle.iconColor, backgroundColor: Colors.transparent, iconSize: MobileAppBarComposerWidgetStyle.iconSize, @@ -86,8 +85,8 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { const SizedBox(width: MobileAppBarComposerWidgetStyle.space), TMailButtonWidget.fromIcon( icon: isSendButtonEnabled - ? _imagePaths.icSendMobile - : _imagePaths.icSendDisable, + ? imagePaths.icSendMobile + : imagePaths.icSendDisable, backgroundColor: Colors.transparent, padding: MobileAppBarComposerWidgetStyle.iconPadding, iconSize: MobileAppBarComposerWidgetStyle.sendButtonIconSize, @@ -96,7 +95,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget { ), const SizedBox(width: MobileAppBarComposerWidgetStyle.space), TMailButtonWidget.fromIcon( - icon: _imagePaths.icMore, + icon: imagePaths.icMore, iconColor: MobileAppBarComposerWidgetStyle.iconColor, backgroundColor: Colors.transparent, iconSize: MobileAppBarComposerWidgetStyle.iconSize, diff --git a/lib/features/composer/presentation/widgets/web/print_draft_dialog_view.dart b/lib/features/composer/presentation/widgets/web/print_draft_dialog_view.dart new file mode 100644 index 000000000..bf40b5c37 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/print_draft_dialog_view.dart @@ -0,0 +1,202 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; +import 'package:tmail_ui_user/features/email/domain/state/print_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class PrintDraftDialogView extends StatefulWidget { + + final EmailPrint emailPrint; + final PrintEmailInteractor printEmailInteractor; + + const PrintDraftDialogView({ + super.key, + required this.emailPrint, + required this.printEmailInteractor, + }); + + @override + State createState() => _PrintDraftDialogViewState(); +} + +class _PrintDraftDialogViewState extends State { + + StreamSubscription? _streamSubscription; + final ValueNotifier?> _viewStateNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _streamSubscription = widget.printEmailInteractor + .execute(widget.emailPrint) + .listen( + _handleDataStream, + onError: _handleErrorStream + ); + } + + void _handleDataStream(dartz.Either newState) { + _viewStateNotifier.value = newState; + + newState.fold( + (failure) { + if (failure is PrintEmailFailure) { + popBack(result: failure); + } + }, + (success) { + if (success is PrintEmailSuccess) { + popBack(result: success); + } + } + ); + } + + void _handleErrorStream(Object error, StackTrace stackTrace) { + logError('_PrintDraftDialogViewState::_handleErrorStream: Exception = $error'); + popBack(result: PrintEmailFailure(exception: error)); + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12))), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 16.0 + ), + alignment: Alignment.center, + child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: Colors.white, + ), + width: min(context.width, 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorItemSelected, + ), + alignment: Alignment.center, + child: Text( + AppLocalizations.of(context).print, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ), + ), + ), + const Divider(), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).status}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: ValueListenableBuilder( + valueListenable: _viewStateNotifier, + builder: (context, value, child) { + if (value == null) { + return child!; + } + + return value.fold( + (failure) => child!, + (success) { + return Text( + '${_getStatusMessage(success)}...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ); + } + ); + }, + child: Text( + '...', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: AppColor.labelColor, + fontSize: 14 + ), + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 16), + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).progress}:', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 14 + ), + ), + const SizedBox(width: 8), + Expanded( + child: LinearProgressIndicator( + color: Colors.white.withOpacity(0.6), + backgroundColor: AppColor.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ) + ], + ), + ), + ], + ) + ], + ), + ), + ); + } + + String _getStatusMessage(Success success) { + if (success is PrintEmailLoading) { + return AppLocalizations.of(context).printingInProgress; + } else { + return AppLocalizations.of(context).printingFailed; + } + } + + @override + void dispose() { + _streamSubscription?.cancel(); + _viewStateNotifier.dispose(); + super.dispose(); + } +} diff --git a/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart b/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart index 7aed25348..a85ffdfb6 100644 --- a/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart @@ -1,5 +1,4 @@ import 'package:core/data/model/print_attachment.dart'; -import 'package:core/domain/extensions/datetime_extension.dart'; import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; @@ -8,7 +7,6 @@ import 'package:core/utils/file_utils.dart'; import 'package:core/utils/print_utils.dart'; import 'package:filesize/filesize.dart'; import 'package:model/email/attachment.dart'; -import 'package:model/extensions/email_extension.dart'; import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; @@ -37,14 +35,6 @@ class PrintFileDataSourceImpl extends PrintFileDataSource { final emailContentEscaped = await _transformHtmlEmailContent( emailPrint.emailContent); - final sender = emailPrint.emailInformation.from?.isNotEmpty == true - ? emailPrint.emailInformation.from!.first - : null; - final receiveTime = emailPrint.emailInformation.getReceivedAt( - newLocale: emailPrint.locale, - pattern: emailPrint.emailInformation.receivedAt?.value.toLocal().toPatternForPrinting(emailPrint.locale) - ); - final List listPrintAttachment = []; if (emailPrint.attachments?.isNotEmpty == true) { @@ -62,11 +52,11 @@ class PrintFileDataSourceImpl extends PrintFileDataSource { return await _printUtils.printEmail( appName: emailPrint.appName, userName: emailPrint.userName, - subject: emailPrint.emailInformation.subject?.escapeLtGtHtmlString() ?? '', + subject: emailPrint.subject?.escapeLtGtHtmlString() ?? '', emailContent: emailContentEscaped, - senderName: sender?.name.escapeLtGtHtmlString() ?? '', - senderEmailAddress: sender?.email ?? '', - dateTime: receiveTime, + senderName: emailPrint.sender?.name.escapeLtGtHtmlString() ?? '', + senderEmailAddress: emailPrint.sender?.email ?? '', + dateTime: emailPrint.receiveTime, fromPrefix: emailPrint.fromPrefix, toPrefix: emailPrint.toPrefix, ccPrefix: emailPrint.ccPrefix, diff --git a/lib/features/email/domain/model/email_print.dart b/lib/features/email/domain/model/email_print.dart index 3c1f33063..caca107a8 100644 --- a/lib/features/email/domain/model/email_print.dart +++ b/lib/features/email/domain/model/email_print.dart @@ -1,11 +1,10 @@ import 'package:equatable/equatable.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/attachment.dart'; class EmailPrint with EquatableMixin { final String appName; final String userName; - final Email emailInformation; final String emailContent; final String locale; final String toPrefix; @@ -14,16 +13,18 @@ class EmailPrint with EquatableMixin { final String bccPrefix; final String replyToPrefix; final String titleAttachment; + final String receiveTime; final List? attachments; final String? toAddress; final String? ccAddress; final String? bccAddress; final String? replyToAddress; + final EmailAddress? sender; + final String? subject; EmailPrint({ required this.appName, required this.userName, - required this.emailInformation, required this.emailContent, required this.locale, required this.fromPrefix, @@ -32,18 +33,20 @@ class EmailPrint with EquatableMixin { required this.bccPrefix, required this.replyToPrefix, required this.titleAttachment, + required this.receiveTime, this.attachments, this.toAddress, this.ccAddress, this.bccAddress, this.replyToAddress, + this.sender, + this.subject, }); @override List get props => [ appName, userName, - emailInformation, emailContent, locale, fromPrefix, @@ -52,11 +55,14 @@ class EmailPrint with EquatableMixin { bccPrefix, replyToPrefix, titleAttachment, + receiveTime, attachments, toAddress, ccAddress, bccAddress, replyToAddress, + sender, + subject, ]; } @@ -65,9 +71,9 @@ extension EmailPrintExtension on EmailPrint { return EmailPrint( appName: appName, userName: userName, - emailInformation: emailInformation, emailContent: newEmailContent, titleAttachment: titleAttachment, + receiveTime: receiveTime, locale: locale, fromPrefix: fromPrefix, toPrefix: toPrefix, @@ -79,6 +85,8 @@ extension EmailPrintExtension on EmailPrint { ccAddress: ccAddress, bccAddress: bccAddress, replyToAddress: replyToAddress, + sender: sender, + subject: subject, ); } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/print_email_interactor.dart b/lib/features/email/domain/usecases/print_email_interactor.dart index 2cdaa4a40..6efe6e0a1 100644 --- a/lib/features/email/domain/usecases/print_email_interactor.dart +++ b/lib/features/email/domain/usecases/print_email_interactor.dart @@ -12,6 +12,7 @@ class PrintEmailInteractor { Stream> execute(EmailPrint emailPrint) async* { try { + yield Right(PrintEmailLoading()); await emailRepository.printEmail(emailPrint); yield Right(PrintEmailSuccess()); } catch (e) { diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 321711fef..818b7e496 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1742,25 +1742,38 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _printEmailButtonState = ButtonState.disabled; + final locale = Localizations.localeOf(context); + final appLocalizations = AppLocalizations.of(context); + consumeState( _printEmailInteractor.execute( EmailPrint( - appName: AppLocalizations.of(context).app_name, + appName: appLocalizations.app_name, userName: mailboxDashBoardController.userEmail, - emailInformation: email.toEmail(), attachments: currentEmailLoaded.value!.attachments, emailContent: currentEmailLoaded.value!.htmlContent, - locale: Localizations.localeOf(context).toLanguageTag(), - fromPrefix: AppLocalizations.of(context).from_email_address_prefix, - toPrefix: AppLocalizations.of(context).to_email_address_prefix, - ccPrefix: AppLocalizations.of(context).cc_email_address_prefix, - bccPrefix: AppLocalizations.of(context).bcc_email_address_prefix, - replyToPrefix: AppLocalizations.of(context).replyToEmailAddressPrefix, - titleAttachment: AppLocalizations.of(context).attachments.toLowerCase(), + locale: locale.toLanguageTag(), + fromPrefix: appLocalizations.from_email_address_prefix, + toPrefix: appLocalizations.to_email_address_prefix, + ccPrefix: appLocalizations.cc_email_address_prefix, + bccPrefix: appLocalizations.bcc_email_address_prefix, + replyToPrefix: appLocalizations.replyToEmailAddressPrefix, + titleAttachment: appLocalizations.attachments.toLowerCase(), toAddress: email.to?.listEmailAddressToString(isFullEmailAddress: true), ccAddress: email.cc?.listEmailAddressToString(isFullEmailAddress: true), bccAddress: email.bcc?.listEmailAddressToString(isFullEmailAddress: true), replyToAddress: email.replyTo?.listEmailAddressToString(isFullEmailAddress: true), + sender: email.from?.isNotEmpty == true + ? email.from!.first + : null, + receiveTime: email.getReceivedAt( + locale.toLanguageTag(), + pattern: email.receivedAt + ?.value + .toLocal() + .toPatternForPrinting(locale.toLanguageTag()), + ), + subject: email.subject, ) ) ); diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index 6315f219e..6baca38a0 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -44,6 +44,7 @@ import 'package:tmail_ui_user/features/composer/presentation/model/formatting_op import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.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/domain/usecases/print_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; @@ -156,6 +157,7 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon MockSpec(), MockSpec(), MockSpec(), + MockSpec(), // Additional Getx dependencies mock specs MockSpec(fallbackGenerators: fallbackGenerators), @@ -197,6 +199,7 @@ void main() { late MockGetAlwaysReadReceiptSettingInteractor mockGetAlwaysReadReceiptSettingInteractor; late MockCreateNewAndSendEmailInteractor mockCreateNewAndSendEmailInteractor; late MockCreateNewAndSaveEmailToDraftsInteractor mockCreateNewAndSaveEmailToDraftsInteractor; + late MockPrintEmailInteractor mockPrintEmailInteractor; // Declaration Getx dependencies final mockMailboxDashBoardController = MockMailboxDashBoardController(); @@ -262,6 +265,7 @@ void main() { mockGetAlwaysReadReceiptSettingInteractor = MockGetAlwaysReadReceiptSettingInteractor(); mockCreateNewAndSendEmailInteractor = MockCreateNewAndSendEmailInteractor(); mockCreateNewAndSaveEmailToDraftsInteractor = MockCreateNewAndSaveEmailToDraftsInteractor(); + mockPrintEmailInteractor = MockPrintEmailInteractor(); composerController = ComposerController( mockLocalFilePickerInteractor, @@ -275,7 +279,8 @@ void main() { mockTransformHtmlEmailContentInteractor, mockGetAlwaysReadReceiptSettingInteractor, mockCreateNewAndSendEmailInteractor, - mockCreateNewAndSaveEmailToDraftsInteractor + mockCreateNewAndSaveEmailToDraftsInteractor, + mockPrintEmailInteractor, ); mockHtmlEditorApi = MockHtmlEditorApi();