TF-2520 Convert email object to html to perform print on browser
Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
@@ -2,4 +2,5 @@ class Constant {
|
||||
static const acceptHeaderDefault = 'application/json';
|
||||
static const contentTypeHeaderDefault = 'application/json';
|
||||
static const pdfMimeType = 'application/pdf';
|
||||
static const textHtmlMimeType = 'text/html';
|
||||
}
|
||||
@@ -2,19 +2,19 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PrintAttachment with EquatableMixin {
|
||||
final String iconSvg;
|
||||
final String iconBase64Data;
|
||||
final String name;
|
||||
final String size;
|
||||
|
||||
PrintAttachment({
|
||||
required this.iconSvg,
|
||||
required this.iconBase64Data,
|
||||
required this.name,
|
||||
required this.size
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
iconSvg,
|
||||
iconBase64Data,
|
||||
name,
|
||||
size
|
||||
];
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:core/domain/exceptions/download_file_exception.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class FileUtils {
|
||||
@@ -111,4 +114,11 @@ class FileUtils {
|
||||
logError('FileUtils::removeFolder():EXCEPTION: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> convertImageAssetToBase64(String assetImage) async {
|
||||
ByteData bytes = await rootBundle.load(assetImage);
|
||||
final buffer = bytes.buffer;
|
||||
final base64Data = base64Encode(Uint8List.view(buffer));
|
||||
return base64Data;
|
||||
}
|
||||
}
|
||||
@@ -91,4 +91,13 @@ class HtmlInteraction {
|
||||
});
|
||||
</script>
|
||||
''';
|
||||
|
||||
static const String scriptHandleInvokePrinterOnBrowser = '''
|
||||
<script type="text/javascript">
|
||||
document.body.onload= function() {
|
||||
document.body.offsetHeight;
|
||||
window.print();
|
||||
};
|
||||
</script>
|
||||
''';
|
||||
}
|
||||
@@ -21,14 +21,47 @@ class HtmlTemplate {
|
||||
}
|
||||
''';
|
||||
|
||||
static const String sampleHtmlDocument = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
</head>
|
||||
<body</body>
|
||||
</html>
|
||||
static const String printDocumentCssStyle = '''
|
||||
<style>
|
||||
body,td,div,p,a,input {
|
||||
font-family: arial, sans-serif;
|
||||
}
|
||||
|
||||
body, td {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
a:link, a:active {
|
||||
color: #1155CC;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:visited{
|
||||
color: #6611CC;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0px
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-width: 800px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
''';
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
|
||||
import 'package:core/data/constants/constant.dart';
|
||||
import 'package:core/presentation/extensions/html_extension.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
class HtmlUtils {
|
||||
static String customCssStyleHtmlEditor({TextDirection direction = TextDirection.ltr}) {
|
||||
@@ -94,4 +96,31 @@ class HtmlUtils {
|
||||
</html>
|
||||
''';
|
||||
}
|
||||
|
||||
static String createTemplateHtmlDocument({String? title}) {
|
||||
return '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
${title != null ? '<title>$title</title>' : ''}
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
''';
|
||||
}
|
||||
|
||||
static String generateSVGImageData(String base64Data) => 'data:image/svg+xml;base64,$base64Data';
|
||||
|
||||
static void openNewTabHtmlDocument(String htmlDocument) {
|
||||
final blob = html.Blob([htmlDocument], Constant.textHtmlMimeType);
|
||||
|
||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||
|
||||
html.window.open(url, '_blank');
|
||||
|
||||
html.Url.revokeObjectUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
+195
-346
@@ -2,319 +2,166 @@
|
||||
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';
|
||||
import 'package:core/utils/file_utils.dart';
|
||||
import 'package:core/utils/html/html_interaction.dart';
|
||||
import 'package:core/utils/html/html_template.dart';
|
||||
import 'package:core/utils/html/html_utils.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart';
|
||||
|
||||
class PrintUtils {
|
||||
final pw.HTMLToPdf _htmlToPdf;
|
||||
final ImagePaths _imagePaths;
|
||||
final FileUtils _fileUtils;
|
||||
|
||||
PrintUtils(this._htmlToPdf, this._imagePaths);
|
||||
PrintUtils(this._imagePaths, this._fileUtils);
|
||||
|
||||
Future<pw.Widget> _createEmailContentWidget(String htmlDocument) async {
|
||||
try {
|
||||
final List<pw.Widget> 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<pw.Widget> _createBodyWidgets({
|
||||
required pw.Context context,
|
||||
required String logoAppSvg,
|
||||
Future<Element> _createUserInformationElement({
|
||||
required String appName,
|
||||
required String userName,
|
||||
required String title,
|
||||
required String htmlDocument,
|
||||
}) async {
|
||||
final logoBase64Data = await _fileUtils.convertImageAssetToBase64(_imagePaths.icTMailLogo);
|
||||
|
||||
return Element.html('''
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tbody>
|
||||
<tr height="14px">
|
||||
<td width="40"><img src="${HtmlUtils.generateSVGImageData(logoBase64Data)}" width="40" height="40" class="logo" /></td>
|
||||
<td style="padding-left: 10px;font-size: 20px;color: #000;"><b>$appName</b></td>
|
||||
<td align="right" style="color: #777;">
|
||||
<b>$userName</b>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
''');
|
||||
}
|
||||
|
||||
Element get dividerElement => Element.html('<hr />');
|
||||
|
||||
Element _createSubjectElement(String subject) {
|
||||
return Element.html('''
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<font size="+1"><b>$subject</b></font><br />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
''');
|
||||
}
|
||||
|
||||
Element _createSenderElement({
|
||||
required String senderName,
|
||||
required String senderEmailAddress,
|
||||
required String dateTime,
|
||||
}) {
|
||||
return Element.html('''
|
||||
<tr>
|
||||
<td>
|
||||
<font size="-1"><b>$senderName </b><$senderEmailAddress></font>
|
||||
</td>
|
||||
<td align="right"><font size="-1">$dateTime</font></td>
|
||||
</tr>
|
||||
''');
|
||||
}
|
||||
|
||||
String _createRecipientHtmlTag(String prefix, String emailAddress) {
|
||||
Element element = Element.html('<div class="${prefix.toLowerCase()}"></div>');
|
||||
element.text = '$prefix: $emailAddress';
|
||||
return element.outerHtml;
|
||||
}
|
||||
|
||||
Element _createRecipientsElement({
|
||||
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<PrintAttachment>? 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
|
||||
))
|
||||
]
|
||||
]
|
||||
)
|
||||
)
|
||||
];
|
||||
return Element.html('''
|
||||
<tr>
|
||||
<td colspan="2" style="padding-bottom: 4px;" class="recipient">
|
||||
${replyToAddress?.isNotEmpty == true ? _createRecipientHtmlTag(replyToPrefix, replyToAddress!) : ''}
|
||||
${toAddress?.isNotEmpty == true ? _createRecipientHtmlTag(toPrefix, toAddress!) : ''}
|
||||
${ccAddress?.isNotEmpty == true ? _createRecipientHtmlTag(ccPrefix, ccAddress!) : ''}
|
||||
${bccAddress?.isNotEmpty == true ? _createRecipientHtmlTag(bccPrefix, bccAddress!) : ''}
|
||||
</td>
|
||||
</tr>
|
||||
''');
|
||||
}
|
||||
|
||||
pw.Widget _createPageDivider() {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.only(top: 12),
|
||||
child: pw.Divider()
|
||||
);
|
||||
Element _createEmailContentElement(String emailContent) {
|
||||
return Element.html('''
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<table width="100%" cellpadding="12" cellspacing="0" border="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>$emailContent</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
''');
|
||||
}
|
||||
|
||||
pw.Widget _createPageHeader({
|
||||
required pw.Context context,
|
||||
required String appName,
|
||||
required String title,
|
||||
required String locale,
|
||||
String _createTitleAttachmentHtmlTag({
|
||||
required int countAttachments,
|
||||
required String titleAttachment
|
||||
}) {
|
||||
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)
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
return '<tr><td colspan="2"><b style="padding-left:3">$countAttachments $titleAttachment</b></td></tr>';
|
||||
}
|
||||
|
||||
pw.Widget _createUserInformationRow({
|
||||
required pw.Context context,
|
||||
required String logoAppSvg,
|
||||
String _createAttachmentHtmlTag(PrintAttachment printAttachment) {
|
||||
return '''
|
||||
<tr>
|
||||
<td>
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a target="_blank" href="">
|
||||
<img width="16" height="16" src="${HtmlUtils.generateSVGImageData(printAttachment.iconBase64Data)}" />
|
||||
</a>
|
||||
</td>
|
||||
<td width="7"></td>
|
||||
<td>
|
||||
<b>${printAttachment.name}</b><br />
|
||||
${printAttachment.size}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
''';
|
||||
}
|
||||
|
||||
Element _createAttachmentsElement({
|
||||
required String titleAttachment,
|
||||
required List<PrintAttachment> listAttachment
|
||||
}) {
|
||||
return Element.html('''
|
||||
<table class="attachments" cellspacing="0" cellpadding="5" border="0">
|
||||
<tbody>
|
||||
${_createTitleAttachmentHtmlTag(countAttachments: listAttachment.length, titleAttachment: titleAttachment)}
|
||||
${listAttachment.map((printAttachment) => _createAttachmentHtmlTag(printAttachment)).toList().join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> printEmail({
|
||||
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<bool> printEmailToPDF({
|
||||
required String appName,
|
||||
required String userName,
|
||||
required String title,
|
||||
required String subject,
|
||||
required String emailContent,
|
||||
required String senderName,
|
||||
required String senderEmailAddress,
|
||||
@@ -331,69 +178,71 @@ class PrintUtils {
|
||||
String? replyToAddress,
|
||||
List<PrintAttachment>? 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);
|
||||
Document document = parse(HtmlUtils.createTemplateHtmlDocument(title: '$appName - $subject'));
|
||||
|
||||
final pdfDocument = pw.Document();
|
||||
Element bodyContainerElement = Element.html('<div class="body-container"></div>');
|
||||
Element mainContentElement = Element.html('<div class="main-content"></div>');
|
||||
Element messageElement = Element.html('<table width="100%" cellpadding="0" cellspacing="0" border="0" class="message"></table>');
|
||||
Element messageBodyElement = Element.html('<tbody></tbody>');
|
||||
|
||||
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();
|
||||
});
|
||||
Element userInfoElement = await _createUserInformationElement(
|
||||
appName: appName,
|
||||
userName: userName);
|
||||
|
||||
Element subjectElement = _createSubjectElement(subject);
|
||||
|
||||
Element senderElement = _createSenderElement(
|
||||
senderName: senderName,
|
||||
senderEmailAddress: senderEmailAddress,
|
||||
dateTime: dateTime);
|
||||
|
||||
Element recipientsElement = _createRecipientsElement(
|
||||
toPrefix: toPrefix,
|
||||
ccPrefix: ccPrefix,
|
||||
bccPrefix: bccPrefix,
|
||||
replyToPrefix: replyToPrefix,
|
||||
toAddress: toAddress,
|
||||
ccAddress: ccAddress,
|
||||
bccAddress: bccAddress,
|
||||
replyToAddress: replyToAddress,
|
||||
);
|
||||
|
||||
Element emailContentElement = _createEmailContentElement(emailContent);
|
||||
|
||||
bodyContainerElement.append(userInfoElement);
|
||||
bodyContainerElement.append(dividerElement);
|
||||
|
||||
mainContentElement.append(subjectElement);
|
||||
mainContentElement.append(dividerElement);
|
||||
|
||||
messageBodyElement.append(senderElement);
|
||||
messageBodyElement.append(recipientsElement);
|
||||
messageBodyElement.append(emailContentElement);
|
||||
|
||||
messageElement.append(messageBodyElement);
|
||||
|
||||
mainContentElement.append(messageElement);
|
||||
|
||||
if (listAttachment?.isNotEmpty == true) {
|
||||
Element attachmentsElement = _createAttachmentsElement(
|
||||
titleAttachment: titleAttachment,
|
||||
listAttachment: listAttachment!);
|
||||
mainContentElement.append(dividerElement);
|
||||
mainContentElement.append(attachmentsElement);
|
||||
}
|
||||
|
||||
bodyContainerElement.append(mainContentElement);
|
||||
|
||||
document.body?.append(bodyContainerElement);
|
||||
|
||||
Element scriptElement = Element.html(HtmlInteraction.scriptHandleInvokePrinterOnBrowser);
|
||||
document.body?.append(scriptElement);
|
||||
|
||||
Element styleElement = Element.html(HtmlTemplate.printDocumentCssStyle);
|
||||
document.head?.append(styleElement);
|
||||
|
||||
final htmlDocument = document.outerHtml;
|
||||
|
||||
HtmlUtils.openNewTabHtmlDocument(htmlDocument);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
# 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:
|
||||
@@ -25,22 +17,6 @@ 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:
|
||||
@@ -89,14 +65,6 @@ 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:
|
||||
@@ -105,14 +73,6 @@ 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:
|
||||
@@ -368,14 +328,6 @@ 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:
|
||||
@@ -392,14 +344,6 @@ 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:
|
||||
@@ -520,22 +464,6 @@ 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:
|
||||
@@ -568,30 +496,6 @@ 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
|
||||
|
||||
@@ -75,10 +75,6 @@ dependencies:
|
||||
|
||||
linkify: 5.0.0
|
||||
|
||||
htmltopdfwidgets: 1.0.3
|
||||
|
||||
printing: 5.12.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -106,6 +106,7 @@ class ComposerBindings extends BaseBindings {
|
||||
Get.lazyPut(() => PrintFileDataSourceImpl(
|
||||
Get.find<PrintUtils>(),
|
||||
Get.find<ImagePaths>(),
|
||||
Get.find<FileUtils>(),
|
||||
Get.find<CacheExceptionThrower>()
|
||||
));
|
||||
Get.lazyPut(() => EmailHiveCacheDataSourceImpl(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:tmail_ui_user/features/email/domain/model/email_print.dart';
|
||||
|
||||
abstract class PrintFileDataSource {
|
||||
Future<void> printEmailToPDF(EmailPrint emailPrint);
|
||||
Future<void> printEmail(EmailPrint emailPrint);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:core/data/model/print_attachment.dart';
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/file_utils.dart';
|
||||
import 'package:core/utils/print_utils.dart';
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:model/email/attachment.dart';
|
||||
import 'package:model/extensions/utc_date_extension.dart';
|
||||
import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart';
|
||||
@@ -15,16 +14,18 @@ class PrintFileDataSourceImpl extends PrintFileDataSource {
|
||||
|
||||
final PrintUtils _printUtils;
|
||||
final ImagePaths _imagePaths;
|
||||
final FileUtils _fileUtils;
|
||||
final ExceptionThrower _exceptionThrower;
|
||||
|
||||
PrintFileDataSourceImpl(
|
||||
this._printUtils,
|
||||
this._imagePaths,
|
||||
this._fileUtils,
|
||||
this._exceptionThrower
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> printEmailToPDF(EmailPrint emailPrint) {
|
||||
Future<void> printEmail(EmailPrint emailPrint) {
|
||||
return Future.sync(() async {
|
||||
final sender = emailPrint.emailInformation.from?.isNotEmpty == true
|
||||
? emailPrint.emailInformation.from!.first
|
||||
@@ -37,9 +38,9 @@ class PrintFileDataSourceImpl extends PrintFileDataSource {
|
||||
|
||||
if (emailPrint.attachments?.isNotEmpty == true) {
|
||||
await Future.forEach<Attachment>(emailPrint.attachments ?? [], (attachment) async {
|
||||
final iconSvg = await rootBundle.loadString(attachment.getIcon(_imagePaths));
|
||||
final iconBase64Data = await _fileUtils.convertImageAssetToBase64(attachment.getIcon(_imagePaths));
|
||||
final printAttachment = PrintAttachment(
|
||||
iconSvg: iconSvg,
|
||||
iconBase64Data: iconBase64Data,
|
||||
name: attachment.name ?? '',
|
||||
size: filesize(attachment.size?.value)
|
||||
);
|
||||
@@ -47,11 +48,11 @@ class PrintFileDataSourceImpl extends PrintFileDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
final result = await _printUtils.printEmailToPDF(
|
||||
return await _printUtils.printEmail(
|
||||
appName: emailPrint.appName,
|
||||
userName: emailPrint.userName,
|
||||
locale: emailPrint.locale,
|
||||
title: emailPrint.emailInformation.subject ?? '',
|
||||
subject: emailPrint.emailInformation.subject ?? '',
|
||||
emailContent: emailPrint.emailContent,
|
||||
senderName: sender?.name ?? '',
|
||||
senderEmailAddress: sender?.email ?? '',
|
||||
@@ -67,8 +68,6 @@ class PrintFileDataSourceImpl extends PrintFileDataSource {
|
||||
titleAttachment: emailPrint.titleAttachment,
|
||||
listAttachment: listPrintAttachment
|
||||
);
|
||||
log('PrintFileDataSourceImpl::printEmailToPDF: RESULT = $result');
|
||||
return result;
|
||||
}).catchError(_exceptionThrower.throwException);
|
||||
}
|
||||
}
|
||||
@@ -235,6 +235,6 @@ class EmailRepositoryImpl extends EmailRepository {
|
||||
|
||||
@override
|
||||
Future<void> printEmail(EmailPrint emailPrint) {
|
||||
return _printFileDataSource.printEmailToPDF(emailPrint);
|
||||
return _printFileDataSource.printEmail(emailPrint);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ class PrintEmailInteractor {
|
||||
final htmlContentTransformed = await emailRepository.transformHtmlEmailContent(
|
||||
emailContent,
|
||||
TransformConfiguration.forPrintEmail());
|
||||
log('PrintEmailInteractor::_transformHtmlEmailContent: htmlContentTransformed: $htmlContentTransformed');
|
||||
return htmlContentTransformed;
|
||||
} catch (e) {
|
||||
logError('PrintEmailInteractor::_transformHtmlEmailContent: Exception: $e');
|
||||
|
||||
@@ -105,6 +105,7 @@ class EmailBindings extends BaseBindings {
|
||||
Get.lazyPut(() => PrintFileDataSourceImpl(
|
||||
Get.find<PrintUtils>(),
|
||||
Get.find<ImagePaths>(),
|
||||
Get.find<FileUtils>(),
|
||||
Get.find<CacheExceptionThrower>()
|
||||
));
|
||||
Get.lazyPut(() => EmailHiveCacheDataSourceImpl(
|
||||
|
||||
@@ -214,6 +214,7 @@ class MailboxDashBoardBindings extends BaseBindings {
|
||||
Get.lazyPut(() => PrintFileDataSourceImpl(
|
||||
Get.find<PrintUtils>(),
|
||||
Get.find<ImagePaths>(),
|
||||
Get.find<FileUtils>(),
|
||||
Get.find<CacheExceptionThrower>()
|
||||
));
|
||||
Get.lazyPut(() => MailboxDataSourceImpl(
|
||||
|
||||
@@ -70,6 +70,7 @@ class SendEmailInteractorBindings extends InteractorsBindings {
|
||||
Get.lazyPut(() => PrintFileDataSourceImpl(
|
||||
Get.find<PrintUtils>(),
|
||||
Get.find<ImagePaths>(),
|
||||
Get.find<FileUtils>(),
|
||||
Get.find<CacheExceptionThrower>(),
|
||||
));
|
||||
Get.lazyPut(() => EmailHiveCacheDataSourceImpl(
|
||||
|
||||
@@ -114,6 +114,7 @@ class FcmInteractorBindings extends InteractorsBindings {
|
||||
Get.lazyPut(() => PrintFileDataSourceImpl(
|
||||
Get.find<PrintUtils>(),
|
||||
Get.find<ImagePaths>(),
|
||||
Get.find<FileUtils>(),
|
||||
Get.find<CacheExceptionThrower>()
|
||||
));
|
||||
Get.lazyPut(() => EmailHiveCacheDataSourceImpl(
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:core/utils/print_utils.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:htmltopdfwidgets/htmltopdfwidgets.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart';
|
||||
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||
@@ -62,8 +61,7 @@ class CoreBindings extends Bindings {
|
||||
Get.put(CompressFileUtils());
|
||||
Get.put(AppConfigLoader());
|
||||
Get.put(FileUtils());
|
||||
Get.put(HTMLToPdf());
|
||||
Get.put(PrintUtils(Get.find<HTMLToPdf>(), Get.find<ImagePaths>()));
|
||||
Get.put(PrintUtils(Get.find<ImagePaths>(), Get.find<FileUtils>()));
|
||||
}
|
||||
|
||||
void _bindingIsolate() {
|
||||
|
||||
@@ -57,14 +57,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: barcode
|
||||
sha256: "91b143666f7bb13636f716b6d4e412e372ab15ff7969799af8c9e30a382e9385"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.6"
|
||||
better_open_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -73,14 +65,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.4"
|
||||
bidi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bidi
|
||||
sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.10"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1046,14 +1030,6 @@ packages:
|
||||
url: "https://github.com/linagora/html-editor-enhanced.git"
|
||||
source: git
|
||||
version: "2.5.1"
|
||||
htmltopdfwidgets:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: htmltopdfwidgets
|
||||
sha256: "990e3002fbbfaf307555f1991bed61b6228d7053b7e1945815c7a68f56bbda81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1358,22 +1334,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
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"
|
||||
percent_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1470,14 +1430,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
printing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: printing
|
||||
sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1502,14 +1454,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -231,8 +231,6 @@ dependencies:
|
||||
|
||||
mime: 1.0.4
|
||||
|
||||
htmltopdfwidgets: 1.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_i
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/view_attachment_for_web_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart';
|
||||
@@ -69,6 +70,7 @@ const fallbackGenerators = {
|
||||
MockSpec<ImagePaths>(),
|
||||
MockSpec<ResponsiveUtils>(),
|
||||
MockSpec<Uuid>(),
|
||||
MockSpec<PrintEmailInteractor>(),
|
||||
])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -99,6 +101,7 @@ void main() {
|
||||
final imagePaths = MockImagePaths();
|
||||
final responsiveUtils = MockResponsiveUtils();
|
||||
final uuid = MockUuid();
|
||||
final printEmailInteractor = MockPrintEmailInteractor();
|
||||
|
||||
late SingleEmailController singleEmailController = SingleEmailController(
|
||||
getEmailContentInteractor,
|
||||
@@ -112,6 +115,7 @@ void main() {
|
||||
getAllIdentitiesInteractor,
|
||||
storeOpenedEmailInteractor,
|
||||
viewAttachmentForWebInteractor,
|
||||
printEmailInteractor,
|
||||
);
|
||||
|
||||
final testAccountId = AccountId(Id('123'));
|
||||
|
||||
Reference in New Issue
Block a user