TF-3267 Implement HTML attachment preview

This commit is contained in:
DatDang
2024-12-06 10:07:53 +07:00
committed by Dat H. Pham
parent 3ddb81a8c4
commit ac91cae0db
22 changed files with 558 additions and 18 deletions
+1
View File
@@ -13,4 +13,5 @@ class Constant {
static const mailtoScheme = 'mailto';
static const attachmentScheme = 'attachment';
static const emlPreviewerScheme = 'eml-previewer';
static const htmlExtension = '.html';
}
@@ -0,0 +1,7 @@
class UnsupportedCharsetException implements Exception {
const UnsupportedCharsetException();
}
class NullCharsetException implements Exception {
const NullCharsetException();
}
@@ -47,4 +47,9 @@ extension MediaTypeExtension on MediaType {
fileName?.endsWith(Constant.pdfExtension) == true);
bool get isEMLFile => mimeType == Constant.emlMimeType;
bool isHTMLFile({required String? fileName}) =>
mimeType == Constant.textHtmlMimeType ||
(mimeType == Constant.octetStreamMimeType &&
fileName?.endsWith(Constant.htmlExtension) == true);
}
@@ -20,6 +20,9 @@ class StandardizeHtmlSanitizingTransformers extends TextTransformer {
'style',
'body',
'section',
'nav',
'main',
'footer',
];
const StandardizeHtmlSanitizingTransformers();
@@ -12,6 +12,7 @@ import 'package:flutter/cupertino.dart';
import 'package:universal_html/html.dart' as html;
typedef OnClickHyperLinkAction = Function(Uri?);
typedef OnMailtoClicked = void Function(Uri? uri);
class HtmlContentViewerOnWeb extends StatefulWidget {
@@ -21,13 +22,15 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
final TextDirection? direction;
/// Handler for mailto: links
final Function(Uri?)? mailtoDelegate;
final OnMailtoClicked? mailtoDelegate;
final OnClickHyperLinkAction? onClickHyperLinkAction;
// if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent
// if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent
final bool allowResizeToDocumentSize;
final bool keepWidthWhileLoading;
const HtmlContentViewerOnWeb({
Key? key,
required this.contentHtml,
@@ -37,6 +40,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
this.mailtoDelegate,
this.direction,
this.onClickHyperLinkAction,
this.keepWidthWhileLoading = false,
}) : super(key: key);
@override
@@ -99,7 +103,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
}
}
if (data['type'] != null && data['type'].contains('toDart: htmlWidth')) {
if (data['type'] != null && data['type'].contains('toDart: htmlWidth') && !widget.keepWidthWhileLoading) {
final docWidth = data['width'] ?? _actualWidth;
if (docWidth != null && mounted) {
if (docWidth > _minWidth && widget.allowResizeToDocumentSize) {
+23
View File
@@ -1,7 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
import 'app_logger.dart';
import 'package:core/domain/exceptions/string_exception.dart';
class StringConvert {
static const String separatorPattern = r'[ ,;]+';
@@ -38,4 +41,24 @@ class StringConvert {
return [];
}
}
static String decodeFromBytes(
Uint8List bytes, {
required String? charset,
bool isHtml = false,
}) {
if (isHtml) {
return utf8.decode(bytes);
} else if (charset == null) {
throw const NullCharsetException();
} else if (charset.toLowerCase().contains('utf-8')) {
return utf8.decode(bytes);
} else if (charset.toLowerCase().contains('latin')) {
return latin1.decode(bytes);
} else if (charset.toLowerCase().contains('ascii')) {
return ascii.decode(bytes);
} else {
throw const UnsupportedCharsetException();
}
}
}
@@ -143,5 +143,35 @@ void main() {
expect(result, equals('<img src="cid:email123">'));
});
test(
'SHOULD persist nav tag and remove href attribute of A tag '
'WHEN href is invalid',
() {
const inputHtml = '<nav href="javascript:alert(1)"></nav>';
final result = transformer.process(inputHtml, htmlEscape);
expect(result, equals('<nav></nav>'));
});
test(
'SHOULD persist main tag and remove href attribute of A tag '
'WHEN href is invalid',
() {
const inputHtml = '<main href="javascript:alert(1)"></main>';
final result = transformer.process(inputHtml, htmlEscape);
expect(result, equals('<main></main>'));
});
test(
'SHOULD persist footer tag and remove href attribute of A tag '
'WHEN href is invalid',
() {
const inputHtml = '<footer href="javascript:alert(1)"></footer>';
final result = transformer.process(inputHtml, htmlEscape);
expect(result, equals('<footer></footer>'));
});
});
}
+88
View File
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:core/domain/exceptions/string_exception.dart';
import 'package:core/utils/string_convert.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -159,4 +160,91 @@ void main() {
});
});
});
group('string convert test:', () {
const testText = 'Hello';
test(
'should use utf8 decoder '
'when isHtml is true',
() {
// arrange
final bytes = utf8.encode(testText);
// act
final result = StringConvert.decodeFromBytes(bytes, charset: null, isHtml: true);
// assert
expect(result, testText);
});
test(
'should use utf8 decoder '
'when charset contains utf-8',
() {
// arrange
final bytes = utf8.encode(testText);
// act
final result = StringConvert.decodeFromBytes(bytes, charset: 'utf-8');
// assert
expect(result, testText);
});
test(
'should use latin1 decoder '
'when charset contains latin-1',
() {
// arrange
final bytes = latin1.encode(testText);
// act
final result = StringConvert.decodeFromBytes(bytes, charset: 'latin-1');
// assert
expect(result, testText);
});
test(
'should use ascii decoder '
'when charset contains ascii',
() {
// arrange
final bytes = ascii.encode(testText);
// act
final result = StringConvert.decodeFromBytes(bytes, charset: 'ascii');
// assert
expect(result, testText);
});
test(
'should throw NullCharsetException '
'when charset is null',
() {
// arrange
final bytes = utf8.encode(testText);
// assert
expect(
() => StringConvert.decodeFromBytes(bytes, charset: null),
throwsA(isA<NullCharsetException>()),
);
});
test(
'should throw UnsupportedCharsetException '
'when charset is unsupported',
() {
// arrange
final bytes = utf8.encode(testText);
// assert
expect(
() => StringConvert.decodeFromBytes(bytes, charset: 'unsupported'),
throwsA(isA<UnsupportedCharsetException>()),
);
});
});
}
@@ -520,4 +520,14 @@ class EmailRepositoryImpl extends EmailRepository {
Future<EMLPreviewer> getPreviewEMLContentInMemory(String keyStored) {
return emailDataSource[DataSourceType.session]!.getPreviewEMLContentInMemory(keyStored);
}
@override
Future<String> sanitizeHtmlContent(
String htmlContent,
TransformConfiguration configuration
) {
return _htmlDataSource.transformHtmlEmailContent(
htmlContent,
configuration);
}
}
@@ -195,4 +195,8 @@ abstract class EmailRepository {
Future<void> storePreviewEMLContentToSessionStorage(EMLPreviewer emlPreviewer);
Future<EMLPreviewer> getPreviewEMLContentInMemory(String keyStored);
Future<String> sanitizeHtmlContent(
String htmlContent,
TransformConfiguration configuration);
}
@@ -0,0 +1,40 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:model/email/attachment.dart';
class GettingHtmlContentFromAttachment extends LoadingState {
GettingHtmlContentFromAttachment({required this.attachment});
final Attachment attachment;
@override
List<Object?> get props => [attachment];
}
class GetHtmlContentFromAttachmentSuccess extends UIState {
GetHtmlContentFromAttachmentSuccess({
required this.sanitizedHtmlContent,
required this.htmlAttachmentTitle,
required this.attachment,
});
final String sanitizedHtmlContent;
final String htmlAttachmentTitle;
final Attachment attachment;
@override
List<Object?> get props => [
sanitizedHtmlContent,
htmlAttachmentTitle,
attachment,
];
}
class GetHtmlContentFromAttachmentFailure extends FeatureFailure {
GetHtmlContentFromAttachmentFailure({super.exception, required this.attachment});
final Attachment attachment;
@override
List<Object?> get props => [attachment];
}
@@ -0,0 +1,109 @@
import 'dart:async';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/string_convert.dart';
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:model/download/download_task_id.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_html_content_from_attachment_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart';
class GetHtmlContentFromAttachmentInteractor {
GetHtmlContentFromAttachmentInteractor(this._downloadAttachmentForWebInteractor);
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor;
Stream<Either<Failure, Success>> execute(
AccountId accountId,
Attachment attachment,
DownloadTaskId taskId,
String baseDownloadUrl,
TransformConfiguration transformConfiguration,
) async* {
final onReceiveController = StreamController<Either<Failure, Success>>();
try {
yield Right(GettingHtmlContentFromAttachment(attachment: attachment));
final downloadState = await _downloadAttachmentForWebInteractor.execute(
taskId,
attachment,
accountId,
baseDownloadUrl,
onReceiveController,
).last;
Either<Failure, Success>? sanitizeState;
await downloadState.fold(
(failure) {
sanitizeState = Left(GetHtmlContentFromAttachmentFailure(
exception: failure is FeatureFailure ? failure.exception : null,
attachment: attachment,
));
},
(success) async {
if (success is! DownloadAttachmentForWebSuccess) {
sanitizeState = Right(GettingHtmlContentFromAttachment(attachment: attachment));
} else {
final htmlContent = StringConvert.decodeFromBytes(
success.bytes,
charset: success.attachment.charset,
isHtml: true,
);
sanitizeState = await _sanitizeHtmlContent(
htmlContent,
transformConfiguration,
attachment,
);
}
},
);
onReceiveController.close();
if (sanitizeState != null) {
yield sanitizeState!;
} else {
yield Left(GetHtmlContentFromAttachmentFailure(
exception: null,
attachment: attachment,
));
}
} catch (e) {
logError('GetHtmlContentFromAttachmentInteractor:exception: $e');
onReceiveController.close();
yield Left(GetHtmlContentFromAttachmentFailure(
exception: e,
attachment: attachment,
));
}
}
Future<Either<Failure, Success>?> _sanitizeHtmlContent(
String htmlContent,
TransformConfiguration transformConfiguration,
Attachment attachment,
) async {
try {
final sanitizedHtmlContent = await _downloadAttachmentForWebInteractor
.emailRepository
.sanitizeHtmlContent(
htmlContent,
transformConfiguration,
);
return Right(GetHtmlContentFromAttachmentSuccess(
sanitizedHtmlContent: sanitizedHtmlContent,
htmlAttachmentTitle: attachment.generateFileName(),
attachment: attachment,
));
} catch (e) {
return Left(GetHtmlContentFromAttachmentFailure(
exception: e,
attachment: attachment,
));
}
}
}
@@ -27,6 +27,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_int
import 'package:tmail_ui_user/features/email/domain/usecases/parse_email_by_blob_id_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/preview_email_from_eml_file_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart';
@@ -78,6 +79,7 @@ class EmailBindings extends BaseBindings {
Get.find<StoreEventAttendanceStatusInteractor>(),
Get.find<ParseEmailByBlobIdInteractor>(),
Get.find<PreviewEmailFromEmlFileInteractor>(),
Get.find<GetHtmlContentFromAttachmentInteractor>(),
));
}
@@ -165,6 +167,7 @@ class EmailBindings extends BaseBindings {
Get.lazyPut(() => ParseEmailByBlobIdInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => PreviewEmailFromEmlFileInteractor(Get.find<EmailRepository>()));
IdentityInteractorsBindings().dependencies();
Get.lazyPut(() => GetHtmlContentFromAttachmentInteractor(Get.find<DownloadAttachmentForWebInteractor>()));
}
@override
@@ -8,6 +8,7 @@ import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_
import 'package:core/presentation/utils/html_transformer/text/new_line_transformer.dart';
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
import 'package:core/utils/html/html_utils.dart';
import 'package:core/presentation/utils/html_transformer/dom/sanitize_hyper_link_tag_in_html_transformers.dart';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
@@ -58,6 +59,7 @@ import 'package:tmail_ui_user/features/email/domain/state/download_attachment_fo
import 'package:tmail_ui_user/features/email/domain/state/download_attachments_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/export_attachment_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_html_content_from_attachment_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart';
@@ -82,6 +84,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_even
import 'package:tmail_ui_user/features/email/domain/usecases/parse_email_by_blob_id_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/preview_email_from_eml_file_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/send_receipt_to_sender_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
@@ -99,6 +102,7 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_lis
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/html_attachment_previewer.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart';
import 'package:tmail_ui_user/features/email_previewer/email_previewer_dialog_view.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
@@ -149,6 +153,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
final StoreEventAttendanceStatusInteractor _storeEventAttendanceStatusInteractor;
final ParseEmailByBlobIdInteractor _parseEmailByBlobIdInteractor;
final PreviewEmailFromEmlFileInteractor _previewEmailFromEmlFileInteractor;
final GetHtmlContentFromAttachmentInteractor _getHtmlContentFromAttachmentInteractor;
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor;
@@ -204,6 +209,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
this._storeEventAttendanceStatusInteractor,
this._parseEmailByBlobIdInteractor,
this._previewEmailFromEmlFileInteractor,
this._getHtmlContentFromAttachmentInteractor,
);
@override
@@ -263,6 +269,19 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_handleParseEmailByBlobIdSuccess(success);
} else if (success is PreviewEmailFromEmlFileSuccess) {
_handlePreviewEmailFromEMLFileSuccess(success);
} else if (success is GetHtmlContentFromAttachmentSuccess) {
_updateAttachmentsViewState(success.attachment.blobId, Right(success));
Get.dialog(HtmlAttachmentPreviewer(
title: success.htmlAttachmentTitle,
htmlContent: success.sanitizedHtmlContent,
mailToClicked: openMailToLink,
downloadAttachmentClicked: () {
downloadAttachmentForWeb(success.attachment);
},
responsiveUtils: responsiveUtils,
));
} else if (success is GettingHtmlContentFromAttachment) {
_updateAttachmentsViewState(success.attachment.blobId, Right(success));
}
}
@@ -290,6 +309,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_handleParseEmailByBlobIdFailure(failure);
} else if (failure is PreviewEmailFromEmlFileFailure) {
_handlePreviewEmailFromEMLFileFailure(failure);
} else if (failure is GetHtmlContentFromAttachmentFailure) {
_handleGetHtmlContentFromAttachmentFailure(failure);
}
}
@@ -1155,7 +1176,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
KeyWordIdentifier.emailFlagged: success.markStarAction == MarkStarAction.markStar,
});
mailboxDashBoardController.setSelectedEmail(newEmail);
final emailId = newEmail?.id;
if (emailId == null) return;
mailboxDashBoardController.updateEmailFlagByEmailIds(
@@ -1932,11 +1953,61 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
previewPDFFileAction(context, attachment);
} else if (attachment.isEMLFile) {
previewEMLFileAction(attachment.blobId, AppLocalizations.of(context));
} else {
} else if (attachment.isHTMLFile) {
final attachmentEvaluation = evaluateAttachment(attachment);
if (!attachmentEvaluation.canDownloadAttachment) {
consumeState(Stream.value(Left(GetHtmlContentFromAttachmentFailure(
exception: null,
attachment: attachment,
))));
return;
}
consumeState(_getHtmlContentFromAttachmentInteractor.execute(
attachmentEvaluation.accountId!,
attachment,
DownloadTaskId(attachmentEvaluation.blobId!.value),
attachmentEvaluation.downloadUrl!,
TransformConfiguration.create(
customDomTransformers: [SanitizeHyperLinkTagInHtmlTransformer()],
customTextTransformers: [const StandardizeHtmlSanitizingTransformers()],
),
));
}else {
handleDownloadAttachmentAction(context, attachment);
}
}
({
bool canDownloadAttachment,
AccountId? accountId,
String? downloadUrl,
Id? blobId,
}) evaluateAttachment(
Attachment attachment,
) {
final accountId = mailboxDashBoardController.accountId.value;
final downloadUrl = mailboxDashBoardController.sessionCurrent
?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl);
final blobId = attachment.blobId;
if (accountId == null || downloadUrl == null || blobId == null) {
return (
canDownloadAttachment: false,
accountId: accountId,
downloadUrl: downloadUrl,
blobId: blobId,
);
}
return (
canDownloadAttachment: true,
accountId: accountId,
downloadUrl: downloadUrl,
blobId: blobId,
);
}
Future<void> previewPDFFileAction(BuildContext context, Attachment attachment) async {
if (accountId == null || session == null) {
appToast.showToastErrorMessage(
@@ -2121,4 +2192,13 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
ComposerArguments.fromMailtoUri(listEmailAddress: listEmailAddressMailTo)
);
}
void _handleGetHtmlContentFromAttachmentFailure(GetHtmlContentFromAttachmentFailure failure) {
_updateAttachmentsViewState(failure.attachment.blobId, Left(failure));
if (currentOverlayContext != null && currentContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).thisHtmlAttachmentCannotBePreviewed);
}
}
}
@@ -38,4 +38,6 @@ extension AttachmentExtension on Attachment {
}
DownloadTaskId get downloadTaskId => DownloadTaskId(blobId!.value);
bool get isHTMLFile => type?.isPDFFile(fileName: name) ?? false;
}
@@ -15,6 +15,7 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_html_content_from_attachment_state.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
@@ -64,10 +65,12 @@ class EmailUtils {
static bool checkingIfAttachmentActionIsEnabled(Either<Failure, Success>? state) {
return state?.fold(
(failure) {
return failure is DownloadAttachmentForWebFailure;
return failure is DownloadAttachmentForWebFailure
|| failure is GetHtmlContentFromAttachmentFailure;
},
(success) {
return success is DownloadAttachmentForWebSuccess
|| success is GetHtmlContentFromAttachmentSuccess
|| success is IdleDownloadAttachmentForWeb;
}) ?? false;
}
@@ -0,0 +1,99 @@
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
import 'package:core/presentation/views/responsive/responsive_widget.dart';
import 'package:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/top_bar_attachment_viewer.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
class HtmlAttachmentPreviewer extends StatelessWidget {
const HtmlAttachmentPreviewer({
super.key,
required this.htmlContent,
required this.title,
required this.mailToClicked,
required this.downloadAttachmentClicked,
required this.responsiveUtils,
});
final String title;
final String htmlContent;
final OnMailtoClicked mailToClicked;
final VoidCallback downloadAttachmentClicked;
final ResponsiveUtils responsiveUtils;
static const double _verticalMargin = 16;
@override
Widget build(BuildContext context) {
return Column(
children: [
TopBarAttachmentViewer(
title: title,
closeAction: popBack,
downloadAction: downloadAttachmentClicked,
),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: GestureDetector(
onTap: popBack,
child: PointerInterceptor(
child: ColoredBox(
color: Colors.transparent,
child: Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: _verticalMargin),
color: Colors.white,
child: ResponsiveWidget(
responsiveUtils: responsiveUtils,
desktop: _buildHtmlViewerWith(
context,
width: constraints.maxWidth * 0.4,
height: constraints.maxHeight - _verticalMargin * 2
),
tablet: _buildHtmlViewerWith(
context,
width: constraints.maxWidth * 0.8,
height: constraints.maxHeight - _verticalMargin * 2
),
mobile: _buildHtmlViewerWith(
context,
width: constraints.maxWidth,
height: constraints.maxHeight - _verticalMargin * 2
),
),
),
),
),
),
),
);
}
),
),
],
);
}
HtmlContentViewerOnWeb _buildHtmlViewerWith(
BuildContext context, {
required double width,
required double height,
}) {
return HtmlContentViewerOnWeb(
contentHtml: htmlContent,
widthContent: width,
heightContent: height,
direction: AppUtils.getCurrentDirection(context),
mailtoDelegate: (uri) {
popBack();
mailToClicked(uri);
},
keepWidthWhileLoading: true,
);
}
}
@@ -21,7 +21,7 @@ import 'package:tmail_ui_user/features/email/domain/exceptions/download_attachme
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/pagination_pdf_viewer.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/top_bar_pdf_viewer.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/top_bar_attachment_viewer.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -221,8 +221,8 @@ class _PDFViewerState extends State<PDFViewer> {
if (deviceInfo.hasData && deviceInfo.data is WebBrowserInfo) {
browserName = (deviceInfo.data as WebBrowserInfo).browserName;
}
return TopBarPDFViewer(
attachment: widget.attachment,
return TopBarAttachmentViewer(
title: widget.attachment.generateFileName(),
downloadAction: () => widget.downloadAction?.call(
viewState.bytes,
widget.attachment.generateFileName()
@@ -235,8 +235,8 @@ class _PDFViewerState extends State<PDFViewer> {
}
return child ?? const SizedBox.shrink();
},
child: TopBarPDFViewer(
attachment: widget.attachment,
child: TopBarAttachmentViewer(
title: widget.attachment.generateFileName(),
closeAction: () {
_downloadAttachmentCancelToken?.cancel();
Navigator.maybeOf(context)?.pop();
@@ -1,16 +1,15 @@
import 'package:flutter/material.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class TopBarPDFViewer extends StatelessWidget {
final Attachment attachment;
class TopBarAttachmentViewer extends StatelessWidget {
final String title;
final VoidCallback? downloadAction;
final VoidCallback? printAction;
final VoidCallback? closeAction;
const TopBarPDFViewer({
const TopBarAttachmentViewer({
super.key,
required this.attachment,
required this.title,
this.downloadAction,
this.printAction,
this.closeAction,
@@ -40,7 +39,7 @@ class TopBarPDFViewer extends StatelessWidget {
const SizedBox(width: 8),
Expanded(
child: Text(
attachment.generateFileName(),
title,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Colors.white,
fontSize: 17,
+12
View File
@@ -4119,5 +4119,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"saveAndRefresh": "Save & refresh",
"@saveAndRefresh": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"thisHtmlAttachmentCannotBePreviewed": "This html attachment cannot be previewed",
"@thisHtmlAttachmentCannotBePreviewed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -4327,4 +4327,18 @@ class AppLocalizations {
name: 'editAsNewEmail',
);
}
String get saveAndRefresh {
return Intl.message(
'Save & refresh',
name: 'saveAndRefresh',
);
}
String get thisHtmlAttachmentCannotBePreviewed {
return Intl.message(
'This html attachment cannot be previewed',
name: 'thisHtmlAttachmentCannotBePreviewed',
);
}
}
@@ -29,6 +29,7 @@ import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/state/calendar_event_accept_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_accept_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_event_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart';
@@ -107,6 +108,7 @@ const fallbackGenerators = {
MockSpec<ToastManager>(),
MockSpec<CalendarEventDataSource>(),
MockSpec<DioClient>(),
MockSpec<GetHtmlContentFromAttachmentInteractor>(),
])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@@ -143,6 +145,7 @@ void main() {
final printUtils = MockPrintUtils();
final applicationManager = MockApplicationManager();
final mockToastManager = MockToastManager();
final getHtmlContentFromAttachmentInteractor = MockGetHtmlContentFromAttachmentInteractor();
late SingleEmailController singleEmailController;
@@ -199,6 +202,7 @@ void main() {
storeEventAttendanceStatusInteractor,
parseEmailByBlobIdInteractor,
previewEmailFromEmlFileInteractor,
getHtmlContentFromAttachmentInteractor,
);
});