TF-3416 Implement preview EML attachment

This commit is contained in:
dab246
2025-01-13 10:02:50 +07:00
committed by Dat H. Pham
parent 7cbdfddb6d
commit d304083b2e
54 changed files with 1489 additions and 148 deletions
+2 -1
View File
@@ -14,9 +14,9 @@ export 'presentation/extensions/compare_string_extension.dart';
export 'presentation/extensions/compare_list_extensions.dart';
export 'presentation/extensions/string_extension.dart';
export 'presentation/extensions/tap_down_details_extension.dart';
export 'domain/extensions/media_type_extension.dart';
export 'presentation/extensions/map_extensions.dart';
export 'presentation/extensions/either_view_state_extension.dart';
export 'presentation/extensions/media_type_extension.dart';
// Exceptions
export 'domain/exceptions/download_file_exception.dart';
@@ -51,6 +51,7 @@ export 'utils/broadcast_channel/broadcast_channel.dart';
export 'utils/list_utils.dart';
export 'utils/mail/domain.dart';
export 'utils/mail/mail_address.dart';
export 'utils/preview_eml_file_utils.dart';
// Views
export 'presentation/views/text/slogan_builder.dart';
+1
View File
@@ -9,4 +9,5 @@ class Constant {
static const imageType = 'image';
static const textVCardMimeType = 'text/x-vcard';
static const textPlainMimeType = 'text/plain';
static const emlMimeType = 'message/rfc822';
}
@@ -1,23 +0,0 @@
import 'package:core/domain/preview/document_uti.dart';
import 'package:core/domain/preview/supported_preview_file_types.dart';
import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType {
bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType);
bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType);
bool isImageFile() => SupportedPreviewFileTypes.imageMimeTypes.contains(mimeType);
bool isDocFile() => SupportedPreviewFileTypes.docMimeTypes.contains(mimeType);
bool isPowerPointFile() => SupportedPreviewFileTypes.pptMimeTypes.contains(mimeType);
bool isExcelFile() => SupportedPreviewFileTypes.xlsMimeTypes.contains(mimeType);
bool isZipFile() => SupportedPreviewFileTypes.zipMimeTypes.contains(mimeType);
bool isPdfFile() => SupportedPreviewFileTypes.pdfMimeTypes.contains(mimeType);
DocumentUti getDocumentUti() => DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]);
}
@@ -14,9 +14,7 @@ class SupportedPreviewFileTypes {
'application/msword',
'application/vnd.ms-works'];
static const pdfMimeTypes = [
'application/pdf',
'application/rtf'];
static const rtfMimeTypes = ['application/rtf'];
static const xlsMimeTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -0,0 +1,50 @@
import 'package:core/data/constants/constant.dart';
import 'package:core/domain/preview/document_uti.dart';
import 'package:core/domain/preview/supported_preview_file_types.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType {
bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType);
bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType);
bool isImageFile() => SupportedPreviewFileTypes.imageMimeTypes.contains(mimeType);
bool isDocFile() => SupportedPreviewFileTypes.docMimeTypes.contains(mimeType);
bool isPowerPointFile() => SupportedPreviewFileTypes.pptMimeTypes.contains(mimeType);
bool isExcelFile() => SupportedPreviewFileTypes.xlsMimeTypes.contains(mimeType);
bool isZipFile() => SupportedPreviewFileTypes.zipMimeTypes.contains(mimeType);
bool isRtfFile() => SupportedPreviewFileTypes.rtfMimeTypes.contains(mimeType);
DocumentUti getDocumentUti() => DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]);
String getIcon(ImagePaths imagePaths, {String? fileName}) {
if (isPDFFile(fileName: fileName) == true || isRtfFile()) {
return imagePaths.icFilePdf;
} else if (isDocFile()) {
return imagePaths.icFileDocx;
} else if (isExcelFile()) {
return imagePaths.icFileXlsx;
} else if (isPowerPointFile()) {
return imagePaths.icFilePptx;
} else if (isZipFile()) {
return imagePaths.icFileZip;
} else if (isImageFile()) {
return imagePaths.icFilePng;
} else {
return imagePaths.icFileEPup;
}
}
bool isPDFFile({required String? fileName}) =>
mimeType == Constant.pdfMimeType ||
(mimeType == Constant.octetStreamMimeType &&
fileName?.endsWith(Constant.pdfExtension) == true);
bool get isEMLFile => mimeType == Constant.emlMimeType;
}
+151
View File
@@ -1,5 +1,6 @@
class HtmlTemplate {
static const String fontLink = '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">';
static const String nameClassToolTip = 'tmail-tooltip';
static const String tooltipLinkCss = '''
.$nameClassToolTip .tooltiptext {
@@ -64,4 +65,154 @@ class HtmlTemplate {
}
</style>
''';
static const String previewEMLFileCssStyle = '''
<style>
body {
font-family: Arial, 'Inter', sans-serif;
color: #333;
margin: 0;
padding: 0;
}
.email-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px;
}
.email-subject {
font-weight: bold;
font-size: 16px;
color: #333;
margin-bottom: 10px;
}
.email-header {
display: flex;
align-items: flex-start;
gap: 10px;
}
.placeholder-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f2f2f2;
border-radius: 50%;
color: #d9534f;
font-size: 18px;
font-weight: bold;
flex-shrink: 0;
}
.email-info {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.sender {
font-weight: bold;
color: #333;
}
.sender-email {
font-size: 12px;
color: #666;
}
.recipients {
font-size: 12px;
color: #666;
margin-top: 5px;
word-wrap: break-word;
line-height: 1.5;
}
.email-meta {
display: flex;
align-items: center;
gap: 5px;
}
.attachment-icon {
width: 16px;
height: 16px;
}
.email-date {
font-size: 12px;
color: #666;
white-space: nowrap;
}
.email-body {
font-size: 14px;
color: #333;
margin-top: 10px;
margin-left: 40px;
}
.attachments {
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #ddd;
margin-left: 40px;
}
.attachment-title {
font-weight: bold;
color: #333;
margin-bottom: 10px;
font-size: 14px;
}
.attachment-item {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 12px;
color: #666;
margin-bottom: 10px;
cursor: pointer;
padding: 5px;
transition: background-color 0.3s, box-shadow 0.3s;
}
.attachment-item:hover {
background-color: #f5f5f5;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.attachment-item .icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f2f2f2;
border-radius: 50%;
color: #007bff;
}
.attachment-item .file-details {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.attachment-item .file-name {
font-weight: bold;
}
.attachment-item .file-size {
font-size: 12px;
color: #666;
}
</style>
''';
}
+24
View File
@@ -421,4 +421,28 @@ class HtmlUtils {
const fileSizeSpan = document.getElementById('file-size');
fileSizeSpan.textContent = formatFileSize(bytesJs.length);''';
}
static void openNewWindowByUrl(
String url,
{
int width = 800,
int height = 600,
}
) async {
try {
final screenWidth = html.window.screen?.width ?? width;
final screenHeight = html.window.screen?.height ?? height;
final left = (screenWidth - width) ~/ 2;
final top = (screenHeight - height) ~/ 2;
final options = 'width=$width,height=$height,top=$top,left=$left';
html.window.open(url, '_blank', options);
html.Url.revokeObjectUrl(url);
} catch (e) {
logError('AppUtils::openNewWindowByUrl:Exception = $e');
}
}
}
+179
View File
@@ -0,0 +1,179 @@
import 'package:core/data/model/print_attachment.dart';
import 'package:core/utils/app_logger.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 PreviewEmlFileUtils {
Element? _createEmailElement({
required String subjectPrefix,
required String fromPrefix,
required String toPrefix,
required String ccPrefix,
required String bccPrefix,
required String replyToPrefix,
required String titleAttachment,
String? subject,
String? senderName,
String? senderEmailAddress,
String? toAddress,
String? ccAddress,
String? bccAddress,
String? replyToAddress,
String? dateTime,
String? attachmentIcon,
String? emailContent,
List<PrintAttachment>? listAttachment,
}) {
try {
return Element.html('''
<div class="email-container">
<!-- Email Subject -->
<div class="email-subject">$subjectPrefix: $subject</div>
<!-- Email Header -->
<div class="email-header">
<!-- Placeholder icon -->
<div class="placeholder-icon">?</div>
<!-- Email information -->
<div class="email-info">
<div class="sender">
${senderName ?? ''} <span class="sender-email">&lt;${senderEmailAddress ?? ''}&gt;</span>
</div>
${replyToAddress?.isNotEmpty == true ? _createRecipientHtmlTag(replyToPrefix, replyToAddress!) : ''}
${toAddress?.isNotEmpty == true ? _createRecipientHtmlTag(toPrefix, toAddress!) : ''}
${ccAddress?.isNotEmpty == true ? _createRecipientHtmlTag(ccPrefix, ccAddress!) : ''}
${bccAddress?.isNotEmpty == true ? _createRecipientHtmlTag(bccPrefix, bccAddress!) : ''}
</div>
<!-- Email metadata -->
<div class="email-meta">
${attachmentIcon?.isNotEmpty == true ? '<img width="16" height="16" src="${HtmlUtils.generateSVGImageData(attachmentIcon!)}" alt="Attachment Icon" class="attachment-icon">' : ''}
${dateTime?.isNotEmpty == true ? '<div class="email-date">$dateTime</div>' : ''}
</div>
</div>
<!-- Email Body -->
<div class="email-body">
<p>$emailContent</p>
</div>
${listAttachment?.isNotEmpty == true ? _createAttachmentsElement(listAttachment: listAttachment ?? [], titleAttachment: titleAttachment) : ''}
''');
} catch (e) {
logError('PreviewEmlFileUtils::_createEmailElement: Exception = $e');
return null;
}
}
String _createRecipientHtmlTag(String prefix, String emailAddress) {
try {
return '''
<div class="recipients">
$prefix: $emailAddress
</div>
''';
} catch (e) {
logError('PreviewEmlFileUtils::_createRecipientHtmlTag: Exception = $e');
return '';
}
}
String _createAttachmentHtmlTag(PrintAttachment printAttachment) {
return '''
<div class="attachment-item">
<div class="icon">
<img width="16" height="16" src="${HtmlUtils.generateSVGImageData(printAttachment.iconBase64Data)}" alt="attachment-icon"/>
</div>
<div class="file-details">
<div class="file-name">${printAttachment.name}</div>
<div class="file-size">${printAttachment.size}</div>
</div>
</div>
''';
}
String _createAttachmentsElement({
required String titleAttachment,
required List<PrintAttachment> listAttachment
}) {
try {
return '''
<div class="attachments">
<!-- Attachment Title -->
<div class="attachment-title">${listAttachment.length} $titleAttachment</div>
<!-- Attachment Items -->
${listAttachment.map((attachment) => _createAttachmentHtmlTag(attachment)).toList().join('\n')}
</div>
''';
} catch (e) {
logError('PreviewEmlFileUtils::_createAttachmentsElement: Exception = $e');
return '';
}
}
String generatePreviewEml({
required String appName,
required String userName,
required String subjectPrefix,
required String subject,
required String emailContent,
required String senderName,
required String senderEmailAddress,
required String dateTime,
required String fromPrefix,
required String toPrefix,
required String ccPrefix,
required String bccPrefix,
required String replyToPrefix,
required String titleAttachment,
required String attachmentIcon,
String? toAddress,
String? ccAddress,
String? bccAddress,
String? replyToAddress,
List<PrintAttachment>? listAttachment,
}) {
Document document = parse(HtmlUtils.createTemplateHtmlDocument(title: '$subject - $userName'));
Element? emailElement = _createEmailElement(
subjectPrefix: subjectPrefix,
fromPrefix: fromPrefix,
toPrefix: toPrefix,
ccPrefix: ccPrefix,
bccPrefix: bccPrefix,
replyToPrefix: replyToPrefix,
subject: subject,
toAddress: toAddress,
ccAddress: ccAddress,
bccAddress: bccAddress,
replyToAddress: replyToAddress,
senderName: senderName,
senderEmailAddress: senderEmailAddress,
dateTime: dateTime,
emailContent: emailContent,
attachmentIcon: attachmentIcon,
titleAttachment: titleAttachment,
listAttachment: listAttachment,
);
Element linkFontElement = Element.html(HtmlTemplate.fontLink);
document.head?.append(linkFontElement);
Element styleElement = Element.html(HtmlTemplate.previewEMLFileCssStyle);
document.head?.append(styleElement);
if (emailElement != null) {
document.body?.append(emailElement);
}
final htmlDocument = document.outerHtml;
return htmlDocument;
}
}