TF-3427 Implement print draft

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-01-15 16:46:50 +07:00
committed by Dat H. Pham
parent 78600f09e9
commit 6fa66efce2
14 changed files with 454 additions and 66 deletions
@@ -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)],
@@ -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<ComposerCacheRepository>()));
Get.lazyPut(() => PrintEmailInteractor(
Get.find<EmailRepository>()));
IdentityInteractorsBindings().dependencies();
}
@@ -251,6 +254,7 @@ class ComposerBindings extends BaseBindings {
Get.find<GetAlwaysReadReceiptSettingInteractor>(),
Get.find<CreateNewAndSendEmailInteractor>(),
Get.find<CreateNewAndSaveEmailToDraftsInteractor>(),
Get.find<PrintEmailInteractor>(),
));
}
@@ -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<Identity>();
final listFromIdentities = RxList<Identity>();
final isEmailChanged = Rx<bool>(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<String> _getContentInEditor() async {
Future<String> 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<int> _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<void> _updateSavedEmailDraftHash() async {
_savedEmailDraftHash = await _hashDraftEmail();
}
Future<void> _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();
@@ -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<ComposerController> {
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<ComposerController> {
),
),
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<ComposerController> {
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<ComposerController> {
),
),
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<ComposerController> {
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<ComposerController> {
}
)
),
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(
@@ -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<void> 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<dynamic> _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,
);
}
}
@@ -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<Object?> get props => [
content,
@@ -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<ImagePaths>();
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,
@@ -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<ImagePaths>();
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,
@@ -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<PrintDraftDialogView> createState() => _PrintDraftDialogViewState();
}
class _PrintDraftDialogViewState extends State<PrintDraftDialogView> {
StreamSubscription? _streamSubscription;
final ValueNotifier<dartz.Either<Failure, Success>?> _viewStateNotifier = ValueNotifier(null);
@override
void initState() {
super.initState();
_streamSubscription = widget.printEmailInteractor
.execute(widget.emailPrint)
.listen(
_handleDataStream,
onError: _handleErrorStream
);
}
void _handleDataStream(dartz.Either<Failure, Success> 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();
}
}
@@ -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<PrintAttachment> 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,
@@ -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<Attachment>? 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<Object?> 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,
);
}
}
@@ -12,6 +12,7 @@ class PrintEmailInteractor {
Stream<Either<Failure, Success>> execute(EmailPrint emailPrint) async* {
try {
yield Right(PrintEmailLoading());
await emailRepository.printEmail(emailPrint);
yield Right(PrintEmailSuccess());
} catch (e) {
@@ -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,
)
)
);
@@ -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<GetAlwaysReadReceiptSettingInteractor>(),
MockSpec<CreateNewAndSendEmailInteractor>(),
MockSpec<CreateNewAndSaveEmailToDraftsInteractor>(),
MockSpec<PrintEmailInteractor>(),
// Additional Getx dependencies mock specs
MockSpec<NetworkConnectionController>(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();