From 7524a97bc830587de0df2e5af6c940ac80cc933d Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 17 May 2024 02:19:00 +0700 Subject: [PATCH] TF-2764 Add PDFViewer to preview PDF attachment --- contact/pubspec.lock | 64 +++++ core/analysis_options.yaml | 3 +- core/lib/data/constants/constant.dart | 2 + .../extensions/media_type_extension.dart | 2 - .../presentation/resources/image_paths.dart | 4 - .../views/text/text_form_field_builder.dart | 4 +- core/lib/utils/print_utils.dart | 10 +- core/pubspec.lock | 72 +++++ core/pubspec.yaml | 2 + .../base/widget/circle_loading_widget.dart | 16 +- .../presentation/composer_controller.dart | 2 +- .../download_attachment_exceptions.dart | 1 + .../presentation/bindings/email_bindings.dart | 4 - .../controller/single_email_controller.dart | 143 +++++----- .../extensions/attachment_extension.dart | 10 +- .../email/presentation/utils/email_utils.dart | 9 +- .../pdf_viewer/pagination_pdf_viewer.dart | 226 +++++++++++++++ .../widgets/pdf_viewer/pdf_viewer.dart | 261 ++++++++++++++++++ .../pdf_viewer/top_bar_pdf_viewer.dart | 88 ++++++ lib/l10n/intl_messages.arb | 32 ++- lib/main/localizations/app_localizations.dart | 35 +++ model/pubspec.lock | 64 +++++ pubspec.lock | 57 ++++ pubspec.yaml | 5 + web/index.html | 13 + 25 files changed, 1018 insertions(+), 111 deletions(-) create mode 100644 lib/features/email/domain/exceptions/download_attachment_exceptions.dart create mode 100644 lib/features/email/presentation/widgets/pdf_viewer/pagination_pdf_viewer.dart create mode 100644 lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart create mode 100644 lib/features/email/presentation/widgets/pdf_viewer/top_bar_pdf_viewer.dart diff --git a/contact/pubspec.lock b/contact/pubspec.lock index c34e62e7f..cbf82dd47 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + archive: + dependency: transitive + description: + name: archive + sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + url: "https://pub.dev" + source: hosted + version: "3.5.1" args: dependency: transitive description: @@ -33,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" + bidi: + dependency: transitive + description: + name: bidi + sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + url: "https://pub.dev" + source: hosted + version: "2.0.10" boolean_selector: dependency: transitive description: @@ -511,6 +535,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: transitive description: @@ -703,6 +735,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: @@ -743,6 +791,14 @@ 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" pub_semver: dependency: transitive description: @@ -759,6 +815,14 @@ 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: diff --git a/core/analysis_options.yaml b/core/analysis_options.yaml index 0f32754d3..86e04bf28 100644 --- a/core/analysis_options.yaml +++ b/core/analysis_options.yaml @@ -13,4 +13,5 @@ linter: rules: constant_identifier_names: false non_constant_identifier_names: false - unnecessary_string_escapes: false \ No newline at end of file + unnecessary_string_escapes: false + avoid_web_libraries_in_flutter: false \ No newline at end of file diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index e6a50f300..adf3581cb 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -4,4 +4,6 @@ class Constant { static const pdfMimeType = 'application/pdf'; static const textHtmlMimeType = 'text/html'; static const octetStreamMimeType = 'application/octet-stream'; + static const pdfExtension = '.pdf'; + static const imageType = 'image'; } \ No newline at end of file diff --git a/core/lib/domain/extensions/media_type_extension.dart b/core/lib/domain/extensions/media_type_extension.dart index b291d1d3d..79e719af4 100644 --- a/core/lib/domain/extensions/media_type_extension.dart +++ b/core/lib/domain/extensions/media_type_extension.dart @@ -3,8 +3,6 @@ import 'package:core/domain/preview/supported_preview_file_types.dart'; import 'package:http_parser/http_parser.dart'; extension MediaTypeExtension on MediaType { - static const String imageType = 'image'; - bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType); bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType); diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 92f963500..322b6e6fd 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -216,10 +216,6 @@ class ImagePaths { return AssetsPaths.images + imageName; } - String _getIconPath(String iconName) { - return AssetsPaths.icons + iconName; - } - String getConfigurationImagePath(String imageName) { return AssetsPaths.configurationImages + imageName; } diff --git a/core/lib/presentation/views/text/text_form_field_builder.dart b/core/lib/presentation/views/text/text_form_field_builder.dart index cc21b8070..a1a21c6ce 100644 --- a/core/lib/presentation/views/text/text_form_field_builder.dart +++ b/core/lib/presentation/views/text/text_form_field_builder.dart @@ -75,7 +75,7 @@ class _TextFieldFormBuilderState extends State { @override Widget build(BuildContext context) { - return TextFormField( + return TextField( key: widget.key, controller: _controller, cursorColor: widget.cursorColor, @@ -105,7 +105,7 @@ class _TextFieldFormBuilderState extends State { } } }, - onFieldSubmitted: widget.onTextSubmitted, + onSubmitted: widget.onTextSubmitted, onTap: widget.onTap, ); } diff --git a/core/lib/utils/print_utils.dart b/core/lib/utils/print_utils.dart index 530407c76..768119a42 100644 --- a/core/lib/utils/print_utils.dart +++ b/core/lib/utils/print_utils.dart @@ -1,5 +1,5 @@ - import 'dart:async'; +import 'dart:typed_data'; import 'package:core/data/model/print_attachment.dart'; import 'package:core/utils/app_logger.dart'; @@ -8,8 +8,16 @@ 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'; +import 'package:printing/printing.dart'; class PrintUtils { + + Future printPDFFile(Uint8List bytes, String fileName) async { + await Printing.layoutPdf( + name: fileName, + onLayout: (_) => bytes); + } + Element? _createUserInformationElement({ required String appName, required String userName, diff --git a/core/pubspec.lock b/core/pubspec.lock index 007f16e16..994ccee13 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: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + url: "https://pub.dev" + source: hosted + version: "3.5.1" 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: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" + bidi: + dependency: transitive + description: + name: bidi + sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + url: "https://pub.dev" + source: hosted + version: "2.0.10" boolean_selector: dependency: transitive description: @@ -73,6 +97,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: @@ -344,6 +376,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 +504,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 +552,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.1" + 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 7509b80be..8c8531d11 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -73,6 +73,8 @@ dependencies: linkify: 5.0.0 + printing: 5.12.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/features/base/widget/circle_loading_widget.dart b/lib/features/base/widget/circle_loading_widget.dart index 5b635d184..9261b70bd 100644 --- a/lib/features/base/widget/circle_loading_widget.dart +++ b/lib/features/base/widget/circle_loading_widget.dart @@ -4,9 +4,15 @@ import 'package:tmail_ui_user/features/base/styles/circle_loading_widget_styles. class CircleLoadingWidget extends StatelessWidget { final double? size; + final double? strokeWidth; final EdgeInsetsGeometry? padding; - const CircleLoadingWidget({super.key, this.size, this.padding}); + const CircleLoadingWidget({ + super.key, + this.size, + this.padding, + this.strokeWidth + }); @override Widget build(BuildContext context) { @@ -16,9 +22,9 @@ class CircleLoadingWidget extends StatelessWidget { child: SizedBox( width: size ?? CircleLoadingWidgetStyles.size, height: size ?? CircleLoadingWidgetStyles.size, - child: const CircularProgressIndicator( + child: CircularProgressIndicator( color: CircleLoadingWidgetStyles.progressColor, - strokeWidth: CircleLoadingWidgetStyles.width, + strokeWidth: strokeWidth ?? CircleLoadingWidgetStyles.width, ) ), ); @@ -26,9 +32,9 @@ class CircleLoadingWidget extends StatelessWidget { return SizedBox( width: size ?? CircleLoadingWidgetStyles.size, height: size ?? CircleLoadingWidgetStyles.size, - child: const CircularProgressIndicator( + child: CircularProgressIndicator( color: CircleLoadingWidgetStyles.progressColor, - strokeWidth: CircleLoadingWidgetStyles.width, + strokeWidth: strokeWidth ?? CircleLoadingWidgetStyles.width, ) ); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index e5e54397f..53770fffd 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1948,7 +1948,7 @@ class ComposerController extends BaseController { return; } - if (fileUpload.type?.startsWith(MediaTypeExtension.imageType) == true) { + if (fileUpload.type?.startsWith(Constant.imageType) == true) { final fileInfo = await fileUpload.toFileInfo(); if (fileInfo != null) { _uploadInlineAttachmentsAction(fileInfo); diff --git a/lib/features/email/domain/exceptions/download_attachment_exceptions.dart b/lib/features/email/domain/exceptions/download_attachment_exceptions.dart new file mode 100644 index 000000000..390c68f96 --- /dev/null +++ b/lib/features/email/domain/exceptions/download_attachment_exceptions.dart @@ -0,0 +1 @@ +class DownloadAttachmentInteractorIsNull implements Exception {} \ No newline at end of file diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index 0cab2afcc..849339bd0 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -23,7 +23,6 @@ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_int import 'package:tmail_ui_user/features/email/domain/usecases/print_email_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/domain/usecases/view_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; @@ -69,7 +68,6 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find(), Get.find(), Get.find(), )); @@ -150,8 +148,6 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find())); Get.lazyPut(() => GetStoredEmailStateInteractor(Get.find())); - Get.lazyPut(() => ViewAttachmentForWebInteractor( - Get.find())); Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find())); Get.lazyPut(() => PrintEmailInteractor(Get.find())); Get.lazyPut(() => StoreEventAttendanceStatusInteractor(Get.find())); diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 645ad5b1d..5c65219be 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:better_open_file/better_open_file.dart' as open_file; import 'package:core/core.dart'; @@ -19,7 +20,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; import 'package:jmap_dart_client/jmap/mdn/mdn.dart'; -import 'package:mime/mime.dart'; import 'package:model/email/eml_attachment.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -54,7 +54,6 @@ import 'package:tmail_ui_user/features/email/domain/state/print_email_state.dart import 'package:tmail_ui_user/features/email/domain/state/send_receipt_to_sender_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/store_event_attendance_status_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/view_attachment_for_web_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/maybe_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; @@ -70,11 +69,11 @@ import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interac 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'; -import 'package:tmail_ui_user/features/email/domain/usecases/view_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/calendar_event_interactor_bindings.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; @@ -83,6 +82,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/pdf_viewer/pdf_viewer.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -113,6 +113,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final mailboxDashBoardController = Get.find(); final emailSupervisorController = Get.find(); final _downloadManager = Get.find(); + final _printUtils = Get.find(); final _attachmentListScrollController = ScrollController(); final emailContentScrollController = ScrollController(); @@ -126,7 +127,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final StoreOpenedEmailInteractor _storeOpenedEmailInteractor; - final ViewAttachmentForWebInteractor _viewAttachmentForWebInteractor; final PrintEmailInteractor _printEmailInteractor; final StoreEventAttendanceStatusInteractor _storeEventAttendanceStatusInteractor; @@ -175,7 +175,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._downloadAttachmentForWebInteractor, this._getAllIdentitiesInteractor, this._storeOpenedEmailInteractor, - this._viewAttachmentForWebInteractor, this._printEmailInteractor, this._storeEventAttendanceStatusInteractor, ); @@ -210,12 +209,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _moveToMailboxSuccess(success); } else if (success is MarkAsStarEmailSuccess) { _markAsEmailStarSuccess(success); - } else if (success is ViewAttachmentForWebSuccess) { - _viewAttachmentForWebSuccessAction(success); - } else if (success is StartViewAttachmentForWeb) { - _updateAttachmentsViewState(success.attachment.blobId, Right(success)); - } else if (success is ViewingAttachmentForWeb) { - _updateAttachmentsViewState(success.attachment.blobId, Right(success)); } else if (success is DownloadAttachmentForWebSuccess) { _downloadAttachmentForWebSuccessAction(success); } else if (success is StartDownloadAttachmentForWeb) { @@ -252,8 +245,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _downloadAttachmentsFailure(failure); } else if (failure is ExportAttachmentFailure) { _exportAttachmentFailureAction(failure); - } else if (failure is ViewAttachmentForWebFailure) { - _viewAttachmentForWebFailureAction(failure); } else if (failure is DownloadAttachmentForWebFailure) { _downloadAttachmentForWebFailureAction(failure); } else if (failure is ParseCalendarEventFailure) { @@ -826,27 +817,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void viewAttachmentForWeb(Attachment attachment) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (accountId != null && session != null) { - final baseDownloadUrl = session.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); - final generateTaskId = DownloadTaskId(uuid.v4()); - consumeState(_viewAttachmentForWebInteractor.execute( - generateTaskId, - attachment, - accountId, - baseDownloadUrl, - _downloadProgressStateController)); - } else { - consumeState(Stream.value( - Left(ViewAttachmentForWebFailure( - attachment: attachment, - exception: NotFoundSessionException())) - )); - } - } - void _downloadAttachmentForWebSuccessAction(DownloadAttachmentForWebSuccess success) { log('SingleEmailController::_downloadAttachmentForWebSuccessAction():'); @@ -859,25 +829,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { success.attachment.generateFileName()); } - void _viewAttachmentForWebSuccessAction( - ViewAttachmentForWebSuccess success, - ) { - log('SingleEmailController::_viewAttachmentForWebSuccessAction():'); - final mimeType = success.attachment.type?.mimeType ?? - lookupMimeType('', headerBytes: success.bytes); - if (mimeType != Constant.pdfMimeType) { - _downloadAttachmentForWebSuccessAction(success); - return; - } + void _downloadPDFFile(Uint8List bytes, String fileName) { + _downloadManager.createAnchorElementDownloadFileWeb(bytes, fileName); + } - _updateAttachmentsViewState(success.attachment.blobId, Right(success)); - - mailboxDashBoardController.deleteDownloadTask(success.taskId); - - _downloadManager.openDownloadedFileWeb( - success.bytes, - success.attachment.type?.mimeType, - success.attachment.name); + void _printPDFFile(Uint8List bytes, String fileName) async { + await _printUtils.printPDFFile(bytes, fileName); } void _downloadAttachmentForWebFailureAction(DownloadAttachmentForWebFailure failure) { @@ -899,11 +856,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _viewAttachmentForWebFailureAction(ViewAttachmentForWebFailure failure) { - log('SingleEmailController::_viewAttachmentForWebFailureAction(): $failure'); - _downloadAttachmentForWebFailureAction(failure); - } - void _updateAttachmentsViewState( Id? attachmentBlobId, Either viewState) { @@ -1533,13 +1485,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { if (responsiveUtils.isMobile(context)) { (AttachmentListBottomSheetBuilder(context, attachments, imagePaths, _attachmentListScrollController) ..onCloseButtonAction(() => popBack()) - ..onDownloadAttachmentFileAction((attachment) { - if (PlatformInfo.isWeb) { - downloadAttachmentForWeb(attachment); - } else { - exportAttachment(context, attachment); - } - }) + ..onDownloadAttachmentFileAction((attachment) => handleDownloadAttachmentAction(context, attachment)) + ..onViewAttachmentFileAction((attachment) => handleViewAttachmentAction(context, attachment)) ).show(); } else { Get.dialog( @@ -1551,20 +1498,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { scrollController: _attachmentListScrollController, backgroundColor: Colors.black.withAlpha(24), onCloseButtonAction: () => popBack(), - onDownloadAttachmentFileAction: (attachment) { - if (PlatformInfo.isWeb) { - downloadAttachmentForWeb(attachment); - } else { - exportAttachment(context, attachment); - } - }, - // onViewAttachmentFileAction: (attachment) { - // if (PlatformInfo.isWeb) { - // viewAttachmentForWeb(attachment); - // } else { - // exportAttachment(context, attachment); - // } - // }, + onDownloadAttachmentFileAction: (attachment) => handleDownloadAttachmentAction(context, attachment), + onViewAttachmentFileAction: (attachment) => handleViewAttachmentAction(context, attachment), ) ), barrierColor: AppColor.colorDefaultCupertinoActionSheet, @@ -1839,4 +1774,56 @@ class SingleEmailController extends BaseController with AppLoaderMixin { downloadAttachmentForWeb(emlAttachment); } + + void handleDownloadAttachmentAction(BuildContext context, Attachment attachment) { + if (PlatformInfo.isWeb) { + downloadAttachmentForWeb(attachment); + } else if (PlatformInfo.isMobile) { + exportAttachment(context, attachment); + } else { + log('EmailView::handleDownloadAttachmentAction: THE PLATFORM IS SUPPORTED'); + } + } + + void handleViewAttachmentAction(BuildContext context, Attachment attachment) { + if (PlatformInfo.isWeb) { + if (PlatformInfo.isCanvasKit && attachment.isDisplayedPDFIcon) { + previewPDFFileAction(context, attachment); + } else { + downloadAttachmentForWeb(attachment); + } + } else if (PlatformInfo.isMobile) { + exportAttachment(context, attachment); + } else { + log('EmailView::_handleViewAttachmentAction: THE PLATFORM IS SUPPORTED'); + } + } + + Future previewPDFFileAction(BuildContext context, Attachment attachment) async { + final accountId = mailboxDashBoardController.accountId.value; + final downloadUrl = mailboxDashBoardController.sessionCurrent + ?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); + + if (accountId == null || downloadUrl == null) { + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).noPreviewAvailable); + return; + } + + await Get.generalDialog( + barrierColor: Colors.black.withOpacity(0.8), + pageBuilder: (_, __, ___) { + return PointerInterceptor( + child: PDFViewer( + attachment: attachment, + accountId: accountId, + downloadUrl: downloadUrl, + downloadAction: _downloadPDFFile, + printAction: _printPDFFile, + ) + ); + }, + ); + } } \ No newline at end of file diff --git a/lib/features/email/presentation/extensions/attachment_extension.dart b/lib/features/email/presentation/extensions/attachment_extension.dart index 5da78344d..c1d1db8ae 100644 --- a/lib/features/email/presentation/extensions/attachment_extension.dart +++ b/lib/features/email/presentation/extensions/attachment_extension.dart @@ -1,7 +1,6 @@ - +import 'package:core/data/constants/constant.dart'; import 'package:core/domain/extensions/media_type_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:http_parser/http_parser.dart'; import 'package:model/email/attachment.dart'; @@ -9,7 +8,7 @@ extension AttachmentExtension on Attachment { String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) { final mediaType = type ?? fileMediaType; - log('AttachmentExtension::getIcon(): mediaType: $mediaType'); + if (isDisplayedPDFIcon) { return imagePaths.icFilePdf; } @@ -33,6 +32,7 @@ extension AttachmentExtension on Attachment { return imagePaths.icFileEPup; } - bool get isDisplayedPDFIcon => type?.mimeType == 'application/pdf' || - (type?.mimeType == 'application/octet-stream' && name?.endsWith('.pdf') == true); + bool get isDisplayedPDFIcon => type?.mimeType == Constant.pdfMimeType + || (type?.mimeType == Constant.octetStreamMimeType + && name?.endsWith(Constant.pdfExtension) == true); } \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 79b3a66bb..6b8bca98c 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -1,4 +1,3 @@ - import 'package:collection/collection.dart'; import 'package:get/get_utils/src/get_utils/get_utils.dart'; import 'package:core/core.dart'; @@ -8,7 +7,6 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.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/view_attachment_for_web_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'; @@ -57,14 +55,11 @@ class EmailUtils { static bool checkingIfAttachmentActionIsEnabled(Either? state) { return state?.fold( (failure) { - return failure is DownloadAttachmentForWebFailure - || failure is ViewAttachmentForWebFailure; + return failure is DownloadAttachmentForWebFailure; }, (success) { return success is DownloadAttachmentForWebSuccess - || success is ViewAttachmentForWebSuccess - || success is IdleDownloadAttachmentForWeb - || success is IdleViewAttachmentForWeb; + || success is IdleDownloadAttachmentForWeb; }) ?? false; } diff --git a/lib/features/email/presentation/widgets/pdf_viewer/pagination_pdf_viewer.dart b/lib/features/email/presentation/widgets/pdf_viewer/pagination_pdf_viewer.dart new file mode 100644 index 000000000..4c7afbef3 --- /dev/null +++ b/lib/features/email/presentation/widgets/pdf_viewer/pagination_pdf_viewer.dart @@ -0,0 +1,226 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:pdf_render/pdf_render_widgets.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +enum ZoomState { + activate, + inactivate +} + +class PaginationPDFViewer extends StatefulWidget { + + final PdfViewerController? pdfViewerController; + + const PaginationPDFViewer({ + super.key, + this.pdfViewerController, + }); + + @override + State createState() => _PaginationPDFViewerState(); +} + +class _PaginationPDFViewerState extends State { + static const double _maxZoomLevelDefault = 4.0; + static const double _minZoomLevelDefault = 1.0; + + final ValueNotifier _zoomInPageNotifier = ValueNotifier(ZoomState.activate); + final ValueNotifier _zoomOutPageNotifier = ValueNotifier(ZoomState.inactivate); + final ValueNotifier _pageCurrentNotifier = ValueNotifier(1); + + double _zoomLevel = 1.0; + + @override + void initState() { + _zoomLevel = 1.0; + _pageCurrentNotifier.value = 1; + widget.pdfViewerController?.addListener(_pageChanged); + super.initState(); + } + + @override + void dispose() { + _zoomLevel = 1.0; + widget.pdfViewerController?.removeListener(_pageChanged); + _pageCurrentNotifier.dispose(); + _zoomInPageNotifier.dispose(); + _zoomOutPageNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.8), + borderRadius: const BorderRadius.all(Radius.circular(5)) + ), + height: 50, + padding: const EdgeInsetsDirectional.only(start: 16, end: 12), + margin: const EdgeInsets.only(bottom: 24), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context).page, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white, + fontSize: 14 + ), + ), + const SizedBox(width: 12), + ValueListenableBuilder( + valueListenable: _pageCurrentNotifier, + builder: (_, value, __) { + return Text( + '$value', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white, + fontSize: 14 + ), + ); + }), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + '/', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white, + fontSize: 14 + ), + ), + ), + Text( + '${widget.pdfViewerController?.pageCount}', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white, + fontSize: 14 + ), + ), + const Padding( + padding: EdgeInsetsDirectional.only(start: 12, end: 8), + child: VerticalDivider(color: Colors.grey), + ), + ValueListenableBuilder( + valueListenable: _zoomOutPageNotifier, + builder: (_, state, __) { + return IconButton( + onPressed: _zoomOutPage, + padding: const EdgeInsets.all(2), + icon: Icon( + Icons.horizontal_rule, + color: _getColorByZoomState(state), + size: 24, + ), + focusColor: Colors.black.withOpacity(0.3), + hoverColor: Colors.black.withOpacity(0.3), + tooltip: AppLocalizations.of(context).zoomOut, + ); + } + ), + const Padding( + padding: EdgeInsets.all(5), + child: Icon( + Icons.zoom_in, + color: Colors.white, + size: 28, + ), + ), + ValueListenableBuilder( + valueListenable: _zoomInPageNotifier, + builder: (_, state, __) { + return IconButton( + onPressed: _zoomInPage, + padding: const EdgeInsets.all(2), + icon: Icon( + Icons.add, + color: _getColorByZoomState(state), + size: 24, + ), + focusColor: Colors.black.withOpacity(0.3), + hoverColor: Colors.black.withOpacity(0.3), + tooltip: AppLocalizations.of(context).zoomIn, + ); + } + ) + ], + ), + ); + } + + Color _getColorByZoomState(ZoomState zoomState) { + switch (zoomState) { + case ZoomState.activate: + return Colors.white; + case ZoomState.inactivate: + return Colors.grey; + } + } + + void _pageChanged({String? property}) { + _pageCurrentNotifier.value = widget.pdfViewerController?.currentPageNumber ?? 1; + _updateZoomState(); + } + + void _zoomInPage() { + if (_zoomLevel < _maxZoomLevelDefault) { + if (_zoomLevel >= 1.0 && _zoomLevel < 1.25) { + _zoomLevel = 1.25; + } else if (_zoomLevel >= 1.25 && _zoomLevel < 1.50) { + _zoomLevel = 1.50; + } else if (_zoomLevel >= 1.50 && _zoomLevel < 2.0) { + _zoomLevel = 2.0; + } else if (_zoomLevel >= 2.0 && _zoomLevel < 2.50) { + _zoomLevel = 2.5; + } else if (_zoomLevel >= 2.5 && _zoomLevel < 3.0) { + _zoomLevel = 3.0; + } else if (_zoomLevel >= 3.0 && _zoomLevel < 3.5) { + _zoomLevel = 3.5; + } else if (_zoomLevel >= 3.5 && _zoomLevel < 4.0) { + _zoomLevel = 4.0; + } + _updateZoomState(); + widget.pdfViewerController?.setZoomRatio(zoomRatio: _zoomLevel); + } + } + + void _zoomOutPage() { + if (_zoomLevel > _minZoomLevelDefault) { + if (_zoomLevel > 1.0 && _zoomLevel <= 1.25) { + _zoomLevel = 1.0; + } else if (_zoomLevel > 1.25 && _zoomLevel <= 1.50) { + _zoomLevel = 1.25; + } else if (_zoomLevel > 1.50 && _zoomLevel <= 2.0) { + _zoomLevel = 1.50; + } else if (_zoomLevel > 2.0 && _zoomLevel <= 2.50) { + _zoomLevel = 2.0; + } else if (_zoomLevel > 2.5 && _zoomLevel <= 3.0) { + _zoomLevel = 2.5; + } else if (_zoomLevel > 3.0 && _zoomLevel <= 3.5) { + _zoomLevel = 3.0; + } else if (_zoomLevel > 3.5 && _zoomLevel <= 4.0) { + _zoomLevel = 3.5; + } + _updateZoomState(); + widget.pdfViewerController?.setZoomRatio(zoomRatio: _zoomLevel); + } + } + + void _updateZoomState() { + final zoomLevel = widget.pdfViewerController?.zoomRatio ?? 1.0; + log('_PaginationPDFViewerState::_updateZoomState:zoomLevel = $zoomLevel'); + if (zoomLevel <= _maxZoomLevelDefault && zoomLevel > _minZoomLevelDefault) { + _zoomOutPageNotifier.value = ZoomState.activate; + } else { + _zoomOutPageNotifier.value = ZoomState.inactivate; + } + + if (zoomLevel < _maxZoomLevelDefault && zoomLevel >= _minZoomLevelDefault) { + _zoomInPageNotifier.value = ZoomState.activate; + } else { + _zoomInPageNotifier.value = ZoomState.inactivate; + } + } +} diff --git a/lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart b/lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart new file mode 100644 index 000000000..aec4502e0 --- /dev/null +++ b/lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart @@ -0,0 +1,261 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:dio/dio.dart'; +import 'package:flutter/material.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:pdf_render/pdf_render_widgets.dart'; +import 'package:percent_indicator/circular_percent_indicator.dart'; +import 'package:tmail_ui_user/features/base/widget/circle_loading_widget.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/download_attachment_exceptions.dart'; +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/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +typedef DownloadPDFFileAction = Function(Uint8List bytes, String fileName); +typedef PrintPDFFileAction = Function(Uint8List bytes, String fileName); + +class PDFViewer extends StatefulWidget { + final Attachment attachment; + final AccountId accountId; + final String downloadUrl; + final DownloadPDFFileAction? downloadAction; + final PrintPDFFileAction? printAction; + + const PDFViewer({ + super.key, + required this.attachment, + required this.accountId, + required this.downloadUrl, + this.downloadAction, + this.printAction, + }); + + @override + State createState() => _PDFViewerState(); +} + +class _PDFViewerState extends State { + late DownloadAttachmentForWebInteractor? _downloadAttachmentForWebInteractor; + late StreamController> _downloadAttachmentStreamController; + late StreamSubscription> _downloadAttachmentStreamSubscription; + + StreamSubscription>? _downloadInteractorStreamSubscription; + CancelToken? _downloadAttachmentCancelToken; + + final PdfViewerController _pdfViewerController = PdfViewerController(); + final ValueNotifier _pdfViewStateNotifier = ValueNotifier(null); + + @override + void initState() { + super.initState(); + _initialStreamListener(); + _downloadAttachmentAction(); + } + + void _initialStreamListener() { + _downloadAttachmentStreamController = StreamController>.broadcast(); + + _downloadAttachmentStreamSubscription = _downloadAttachmentStreamController.stream.listen((viewState) { + viewState.fold( + (failure) => _pdfViewStateNotifier.value = failure, + (success) => _pdfViewStateNotifier.value = success + ); + }); + } + + void _downloadAttachmentAction() { + _downloadAttachmentForWebInteractor = getBinding(); + + if (_downloadAttachmentForWebInteractor == null) { + _pdfViewStateNotifier.value = DownloadAttachmentForWebFailure(exception: DownloadAttachmentInteractorIsNull()); + return; + } + + _downloadAttachmentCancelToken = CancelToken(); + _downloadInteractorStreamSubscription = _downloadAttachmentForWebInteractor!.execute( + DownloadTaskId(widget.attachment.blobId!.value), + widget.attachment, + widget.accountId, + widget.downloadUrl, + _downloadAttachmentStreamController, + cancelToken: _downloadAttachmentCancelToken + ).listen((viewState) { + viewState.fold( + (failure) => _pdfViewStateNotifier.value = failure, + (success) => _pdfViewStateNotifier.value = success + ); + }); + } + + @override + void dispose() { + _pdfViewerController.dispose(); + _pdfViewStateNotifier.dispose(); + _downloadAttachmentStreamSubscription.cancel(); + _downloadAttachmentStreamController.close(); + _downloadInteractorStreamSubscription?.cancel(); + _downloadInteractorStreamSubscription = null; + _downloadAttachmentCancelToken = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Stack( + children: [ + Center( + child: SizedBox( + width: double.infinity, + child: ValueListenableBuilder( + valueListenable: _pdfViewStateNotifier, + builder: (context, viewState, widget) { + if (viewState is DownloadAttachmentForWebSuccess) { + return PdfViewer.openData( + viewState.bytes, + viewerController: _pdfViewerController, + onError: (error) { + logError('_PDFViewerState::build:openData:onError:: $error'); + _pdfViewStateNotifier.value = DownloadAttachmentForWebFailure(exception: error); + }, + params: PdfViewerParams( + panAxis: PanAxis.vertical, + scrollByMouseWheel: 0.5, + layoutPages: (viewSize, pages) { + List rect = []; + final viewWidth = viewSize.width; + final viewHeight = viewSize.height; + final maxHeight = pages.fold(0.0, (maxHeight, page) => max(maxHeight, page.height)); + final maxWidth = pages.fold(0.0, (maxWidth, page) => max(maxWidth, page.width)); + final ratio = viewHeight / max(maxHeight, maxWidth); + log('_PDFViewerState::build: viewWidth = $viewWidth | viewHeight = $viewHeight | maxHeight = $maxHeight | ratio = $ratio'); + var top = 0.0; + double padding = 16.0; + for (var page in pages) { + final width = page.width * ratio; + final height = page.height * ratio; + final left = viewWidth > viewHeight ? (viewWidth / 2) - (width / 2) : 0.0; + rect.add(Rect.fromLTWH(left, top, width, height)); + top += height + padding; + } + return rect; + } + ), + ); + } else if (viewState is DownloadingAttachmentForWeb) { + return CircularPercentIndicator( + percent: viewState.progress / 100, + progressColor: AppColor.primaryColor, + lineWidth: 4.0, + backgroundColor: Colors.white, + radius: 40, + center: Text( + '${viewState.progress.round()}%', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w500 + ), + ), + ); + } else if (viewState is DownloadAttachmentForWebFailure) { + return Center( + child: Text( + AppLocalizations.of(context).noPreviewAvailable, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w500 + ), + ), + ); + } else { + return const Center( + child: CircleLoadingWidget( + size: 80, + strokeWidth: 4.0, + ) + ); + } + }, + ) + ), + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: ValueListenableBuilder( + valueListenable: _pdfViewStateNotifier, + builder: (context, viewState, child) { + if (viewState is DownloadAttachmentForWebSuccess) { + return TopBarPDFViewer( + attachment: widget.attachment, + downloadAction: () => widget.downloadAction?.call( + viewState.bytes, + widget.attachment.generateFileName() + ), + printAction: () => widget.printAction?.call( + viewState.bytes, + widget.attachment.generateFileName() + ), + ); + } + return child ?? const SizedBox.shrink(); + }, + child: TopBarPDFViewer( + attachment: widget.attachment, + closeAction: () { + _downloadAttachmentCancelToken?.cancel(); + Navigator.maybeOf(context)?.pop(); + }, + ), + ), + ), + ValueListenableBuilder( + valueListenable: _pdfViewerController, + builder: (_, __, ___) { + if (_pdfViewerController.isReady) { + return Align( + alignment: AlignmentDirectional.bottomCenter, + child: PaginationPDFViewer( + pdfViewerController: _pdfViewerController, + ), + ); + } else { + return const SizedBox.shrink(); + } + } + ), + ValueListenableBuilder( + valueListenable: _pdfViewerController, + builder: (context, m, child) { + if (!_pdfViewerController.isReady) return Container(); + final v = _pdfViewerController.viewRect; + final all = _pdfViewerController.fullSize; + final top = v.top / all.height * v.height; + final height = v.height / all.height * v.height; + return Positioned( + right: 0, + top: top, + height: height, + width: 8, + child: Container(color: AppColor.colorTextBody), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/email/presentation/widgets/pdf_viewer/top_bar_pdf_viewer.dart b/lib/features/email/presentation/widgets/pdf_viewer/top_bar_pdf_viewer.dart new file mode 100644 index 000000000..550be6407 --- /dev/null +++ b/lib/features/email/presentation/widgets/pdf_viewer/top_bar_pdf_viewer.dart @@ -0,0 +1,88 @@ +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; + final VoidCallback? downloadAction; + final VoidCallback? printAction; + final VoidCallback? closeAction; + + const TopBarPDFViewer({ + super.key, + required this.attachment, + this.downloadAction, + this.printAction, + this.closeAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + height: 52, + color: Colors.black.withOpacity(0.3), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: closeAction ?? Navigator.maybeOf(context)?.pop, + padding: const EdgeInsets.all(8), + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + size: 24, + ), + focusColor: Colors.black.withOpacity(0.3), + hoverColor: Colors.black.withOpacity(0.3), + tooltip: AppLocalizations.of(context).close, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + attachment.generateFileName(), + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Colors.white, + fontSize: 17, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (printAction != null) + Padding( + padding: const EdgeInsetsDirectional.only(start: 8), + child: IconButton( + onPressed: printAction, + padding: const EdgeInsets.all(8), + icon: const Icon( + Icons.print, + color: Colors.white, + size: 24, + ), + focusColor: Colors.black.withOpacity(0.3), + hoverColor: Colors.black.withOpacity(0.3), + tooltip: AppLocalizations.of(context).print, + ), + ), + if (downloadAction != null) + Padding( + padding: const EdgeInsetsDirectional.only(start: 8), + child: IconButton( + onPressed: downloadAction, + padding: const EdgeInsets.all(8), + icon: const Icon( + Icons.download, + color: Colors.white, + size: 24, + ), + focusColor: Colors.black.withOpacity(0.3), + hoverColor: Colors.black.withOpacity(0.3), + tooltip: AppLocalizations.of(context).download, + ), + ) + ], + ), + ); + } +} diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 4a476da4d..6076c1809 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-05-23T11:23:33.056385", + "@@last_modified": "2024-04-05T12:00:06.899366", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3719,5 +3719,35 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "download": "Download", + "@download": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "print": "Print", + "@print": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "page": "Page", + "@page": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "zoomIn": "Zoom In", + "@zoomIn": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "zoomOut": "Zoom Out", + "@zoomOut": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index ad73d1734..5ceea1c55 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3881,4 +3881,39 @@ class AppLocalizations { name: 'downloadMessageAsEMLFailed', ); } + + String get download { + return Intl.message( + 'Download', + name: 'download', + ); + } + + String get print { + return Intl.message( + 'Print', + name: 'print', + ); + } + + String get page { + return Intl.message( + 'Page', + name: 'page', + ); + } + + String get zoomIn { + return Intl.message( + 'Zoom In', + name: 'zoomIn', + ); + } + + String get zoomOut { + return Intl.message( + 'Zoom Out', + name: 'zoomOut', + ); + } } \ No newline at end of file diff --git a/model/pubspec.lock b/model/pubspec.lock index 5f7101a1d..2361d5268 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + archive: + dependency: transitive + description: + name: archive + sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + url: "https://pub.dev" + source: hosted + version: "3.5.1" args: dependency: transitive description: @@ -33,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" + bidi: + dependency: transitive + description: + name: bidi + sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63" + url: "https://pub.dev" + source: hosted + version: "2.0.10" boolean_selector: dependency: transitive description: @@ -503,6 +527,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: @@ -680,6 +712,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: @@ -720,6 +768,14 @@ 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" pub_semver: dependency: transitive description: @@ -736,6 +792,14 @@ 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: "direct main" description: diff --git a/pubspec.lock b/pubspec.lock index 1939f5869..f561da1ae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" better_open_file: dependency: "direct main" description: @@ -65,6 +73,14 @@ 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: @@ -1335,6 +1351,31 @@ 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_render: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: c76f69fb8db912e8420751e219e77d930dda1a28 + url: "https://github.com/linagora/flutter_pdf_render.git" + source: git + version: "1.4.7" + 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: @@ -1431,6 +1472,14 @@ 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: @@ -1455,6 +1504,14 @@ 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: diff --git a/pubspec.yaml b/pubspec.yaml index e7af70d3e..cc48eb1db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -99,6 +99,11 @@ dependencies: url: https://github.com/linagora/dns_client.git ref: twake-supported + pdf_render: + git: + url: https://github.com/linagora/flutter_pdf_render.git + ref: master + ### Dependencies from pub.dev ### cupertino_icons: 1.0.6 diff --git a/web/index.html b/web/index.html index 0935f4514..01ab2876c 100644 --- a/web/index.html +++ b/web/index.html @@ -81,6 +81,19 @@ + + + +