diff --git a/core/lib/core.dart b/core/lib/core.dart index eb665bf64..e00ec0d6f 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -43,6 +43,8 @@ export 'utils/config/errors.dart'; export 'data/utils/compress_file_utils.dart'; export 'utils/platform_info.dart'; export 'utils/option_param_mixin.dart'; +export 'utils/file_utils.dart'; +export 'utils/print_utils.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/data/model/print_attachment.dart b/core/lib/data/model/print_attachment.dart new file mode 100644 index 000000000..7d0ddf3d3 --- /dev/null +++ b/core/lib/data/model/print_attachment.dart @@ -0,0 +1,21 @@ + +import 'package:equatable/equatable.dart'; + +class PrintAttachment with EquatableMixin { + final String iconSvg; + final String name; + final String size; + + PrintAttachment({ + required this.iconSvg, + required this.name, + required this.size + }); + + @override + List get props => [ + iconSvg, + name, + size + ]; +} \ No newline at end of file diff --git a/core/lib/utils/print_utils.dart b/core/lib/utils/print_utils.dart new file mode 100644 index 000000000..f44a031a0 --- /dev/null +++ b/core/lib/utils/print_utils.dart @@ -0,0 +1,399 @@ + +import 'dart:async'; + +import 'package:core/data/model/print_attachment.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/services.dart'; +import 'package:htmltopdfwidgets/htmltopdfwidgets.dart' as pw; +import 'package:intl/intl.dart'; +import 'package:printing/printing.dart'; + +class PrintUtils { + final pw.HTMLToPdf _htmlToPdf; + final ImagePaths _imagePaths; + + PrintUtils(this._htmlToPdf, this._imagePaths); + + Future _createEmailContentWidget(String htmlDocument) async { + try { + final List bodyWidgets = await _htmlToPdf.convert(htmlDocument); + + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + ...bodyWidgets + ] + ); + } catch (e) { + logError('PrintUtils::_createEmailContentWidget: Exception: $e'); + return pw.Text(htmlDocument); + } + } + + List _createBodyWidgets({ + required pw.Context context, + required String logoAppSvg, + required String appName, + required String userName, + required String title, + required String htmlDocument, + required String senderName, + required String senderEmailAddress, + required String dateTime, + required String toPrefix, + required String ccPrefix, + required String bccPrefix, + required String replyToPrefix, + required String titleAttachment, + required pw.Widget contentWidget, + String? toAddress, + String? ccAddress, + String? bccAddress, + String? replyToAddress, + List? listAttachment, + }) { + return [ + pw.Container( + padding: const pw.EdgeInsets.symmetric(horizontal: 32), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _createUserInformationRow( + context: context, + logoAppSvg: logoAppSvg, + appName: appName, + userName: userName + ), + _createPageDivider(), + if (title.isNotEmpty) + ...[ + _createTitleRow( + context: context, + title: title + ), + _createPageDivider(), + ], + pw.SizedBox(height: 12), + pw.Row( + children: [ + pw.Expanded( + child: _createSenderWidget( + context: context, + name: senderName, + emailAddress: senderEmailAddress + ) + ), + _createDateTimeWidget( + context: context, + dateTime: dateTime + ) + ] + ), + if (replyToAddress?.isNotEmpty == true) + _createRecipientRow( + prefix: replyToPrefix, + emailAddress: replyToAddress! + ), + if (toAddress?.isNotEmpty == true) + _createRecipientRow( + prefix: toPrefix, + emailAddress: toAddress! + ), + if (ccAddress?.isNotEmpty == true) + _createRecipientRow( + prefix: ccPrefix, + emailAddress: ccAddress! + ), + if (bccAddress?.isNotEmpty == true) + _createRecipientRow( + prefix: bccPrefix, + emailAddress: bccAddress! + ), + pw.Padding( + padding: const pw.EdgeInsets.symmetric( + horizontal: 12, + vertical: 24), + child: contentWidget + ), + if (listAttachment?.isNotEmpty == true) + ...[ + _createPageDivider(), + pw.SizedBox(height: 12), + _createAttachmentTitle( + context: context, + title: titleAttachment, + totalAttachments: listAttachment!.length + ), + pw.SizedBox(height: 4), + ...listAttachment.map((attachment) => _createAttachmentRow( + context: context, + attachment: attachment + )) + ] + ] + ) + ) + ]; + } + + pw.Widget _createPageDivider() { + return pw.Padding( + padding: const pw.EdgeInsets.only(top: 12), + child: pw.Divider() + ); + } + + pw.Widget _createPageHeader({ + required pw.Context context, + required String appName, + required String title, + required String locale, + }) { + final currentTime = DateFormat('MM/dd/yyyy, h:mm a', locale).format(DateTime.now()); + return pw.Padding( + padding: const pw.EdgeInsets.all(20), + child: pw.Row( + children: [ + pw.Text( + currentTime, + style: pw.Theme.of(context).defaultTextStyle.copyWith( + fontSize: 8, + color: pw.PdfColor.fromInt(AppColor.colorTextBody.value) + ) + ), + pw.SizedBox(width: 16), + pw.Expanded( + child: pw.Text( + '$appName - $title', + textAlign: pw.TextAlign.center, + maxLines: 1, + style: pw.Theme.of(context).defaultTextStyle.copyWith( + fontSize: 8, + color: pw.PdfColor.fromInt(AppColor.colorTextBody.value) + ) + ) + ), + ] + ) + ); + } + + pw.Widget _createUserInformationRow({ + required pw.Context context, + required String logoAppSvg, + required String appName, + required String userName, + }) { + return pw.Row( + children: [ + pw.SvgImage( + svg: logoAppSvg, + width: 32, + height: 32 + ), + pw.SizedBox(width: 12), + pw.Text( + appName, + style: pw.Theme.of(context).header0 + ), + pw.Spacer(), + pw.SizedBox(width: 12), + pw.Text( + userName, + style: pw.Theme.of(context).header4 + ) + ] + ); + } + + pw.Widget _createTitleRow({ + required pw.Context context, + required String title, + }) { + return pw.Padding( + padding: const pw.EdgeInsets.only(top: 12), + child: pw.Text( + title, + style: pw.Theme.of(context).header3 + ) + ); + } + + pw.Widget _createSenderWidget({ + required pw.Context context, + required String name, + required String emailAddress, + }) { + return pw.Row( + children: [ + if (name.isNotEmpty) + pw.Padding( + padding: const pw.EdgeInsetsDirectional.only(end: 4), + child: pw.Text( + name, + style: pw.Theme.of(context).header4 + ) + ), + pw.Text('<$emailAddress>') + ] + ); + } + + pw.Widget _createDateTimeWidget({ + required pw.Context context, + required String dateTime, + }) { + return pw.Text( + dateTime, + style: pw.Theme.of(context).defaultTextStyle.copyWith( + fontSize: 10, + color: pw.PdfColor.fromInt(AppColor.colorTextBody.value) + ) + ); + } + + pw.Widget _createRecipientRow({ + required String prefix, + required String emailAddress, + }) { + return pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text('$prefix:'), + pw.SizedBox(width: 4), + pw.Expanded(child: pw.Text(emailAddress)) + ] + ); + } + + pw.Widget _createAttachmentTitle({ + required pw.Context context, + required String title, + required int totalAttachments, + }) { + return pw.Text( + '$totalAttachments $title', + style: pw.Theme.of(context).header4 + ); + } + + pw.Widget _createAttachmentRow({ + required pw.Context context, + required PrintAttachment attachment, + }) { + return pw.Padding( + padding: const pw.EdgeInsets.only(top: 8), + child: pw.Row( + children: [ + pw.SvgImage( + svg: attachment.iconSvg, + width: 24, + height: 24 + ), + pw.SizedBox(width: 8), + pw.Expanded( + child: pw.Column( + mainAxisSize: pw.MainAxisSize.min, + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + attachment.name, + style: pw.Theme.of(context).header4 + ), + pw.Text(attachment.size) + ] + ) + ), + ] + ) + ); + } + + Future printEmailToPDF({ + required String appName, + required String userName, + required String title, + required String emailContent, + required String senderName, + required String senderEmailAddress, + required String locale, + required String dateTime, + required String toPrefix, + required String ccPrefix, + required String bccPrefix, + required String replyToPrefix, + required String titleAttachment, + String? toAddress, + String? ccAddress, + String? bccAddress, + String? replyToAddress, + List? listAttachment, + }) async { + return await Printing.layoutPdf(onLayout: (pw.PdfPageFormat format) async { + final googleInterRegularFont = await PdfGoogleFonts.interRegular(); + final googleInterBoldFont = await PdfGoogleFonts.interBold(); + final logoAppSvg = await rootBundle.loadString(_imagePaths.icTMailLogo); + final contentWidget = await _createEmailContentWidget(emailContent); + + final pdfDocument = pw.Document(); + + pdfDocument.addPage( + pw.MultiPage( + pageFormat: pw.PdfPageFormat.standard, + orientation: pw.PageOrientation.portrait, + theme: pw.ThemeData( + defaultTextStyle: pw.TextStyle( + font: googleInterRegularFont, + fontSize: 12, + color: pw.PdfColors.black + ), + header3: pw.TextStyle( + font: googleInterBoldFont, + fontSize: 14, + fontWeight: pw.FontWeight.bold, + color: pw.PdfColors.black + ), + header4: pw.TextStyle( + font: googleInterBoldFont, + fontSize: 12, + fontWeight: pw.FontWeight.bold, + color: pw.PdfColors.black + ), + ), + margin: pw.EdgeInsets.zero, + build: (context) => _createBodyWidgets( + context: context, + contentWidget: contentWidget, + logoAppSvg: logoAppSvg, + appName: appName, + userName: userName, + title: title, + htmlDocument: emailContent, + senderName: senderName, + senderEmailAddress: senderEmailAddress, + dateTime: dateTime, + toPrefix: toPrefix, + ccPrefix: ccPrefix, + bccPrefix: toPrefix, + replyToPrefix: replyToPrefix, + toAddress: toAddress, + ccAddress: ccAddress, + bccAddress: bccAddress, + replyToAddress: replyToAddress, + titleAttachment: titleAttachment, + listAttachment: listAttachment + ), + header: (context) => _createPageHeader( + context: context, + appName: appName, + title: title, + locale: locale, + ), + ) + ); + return await pdfDocument.save(); + }); + } +} diff --git a/core/pubspec.lock b/core/pubspec.lock index 32f33c83d..4bc72cb78 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" args: dependency: transitive description: @@ -17,6 +25,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "91b143666f7bb13636f716b6d4e412e372ab15ff7969799af8c9e30a382e9385" + url: "https://pub.dev" + source: hosted + version: "2.2.6" + bidi: + dependency: transitive + description: + name: bidi + sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + url: "https://pub.dev" + source: hosted + version: "2.0.10" boolean_selector: dependency: transitive description: @@ -65,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" cross_file: dependency: transitive description: @@ -73,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.3+7" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" csslib: dependency: transitive description: @@ -328,6 +368,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.3" + htmltopdfwidgets: + dependency: "direct main" + description: + name: htmltopdfwidgets + sha256: "990e3002fbbfaf307555f1991bed61b6228d7053b7e1945815c7a68f56bbda81" + url: "https://pub.dev" + source: hosted + version: "1.0.3" http: dependency: "direct main" description: @@ -344,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + url: "https://pub.dev" + source: hosted + version: "4.1.7" intl: dependency: "direct main" description: @@ -464,6 +520,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + pdf: + dependency: transitive + description: + name: pdf + sha256: "243f05342fc0bdf140eba5b069398985cdbdd3dbb1d776cf43d5ea29cc570ba6" + url: "https://pub.dev" + source: hosted + version: "3.10.8" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: e9d31fd7782ce28ae346b127ea7d1cd748d799bddee379f31191693610e23749 + url: "https://pub.dev" + source: hosted + version: "1.0.1" petitparser: dependency: transitive description: @@ -496,6 +568,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.1" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" + source: hosted + version: "3.7.4" + printing: + dependency: "direct main" + description: + name: printing + sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc" + url: "https://pub.dev" + source: hosted + version: "5.12.0" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" sky_engine: dependency: transitive description: flutter diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 10c480911..35ea63279 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -75,6 +75,10 @@ dependencies: linkify: 5.0.0 + htmltopdfwidgets: 1.0.3 + + printing: 5.12.0 + dev_dependencies: flutter_test: sdk: flutter