TF-2764 Add PDFViewer to preview PDF attachment

This commit is contained in:
dab246
2024-05-17 02:19:00 +07:00
committed by Dat H. Pham
parent 89a489eb06
commit 7524a97bc8
25 changed files with 1018 additions and 111 deletions
+64
View File
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.13.0"
archive:
dependency: transitive
description:
name: archive
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265
url: "https://pub.dev"
source: hosted
version: "3.5.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -33,6 +41,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -511,6 +535,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image:
dependency: transitive
description:
name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
url: "https://pub.dev"
source: hosted
version: "4.1.7"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@@ -703,6 +735,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -743,6 +791,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
printing:
dependency: transitive
description:
name: printing
sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc"
url: "https://pub.dev"
source: hosted
version: "5.12.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -759,6 +815,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.2.3"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
+2 -1
View File
@@ -13,4 +13,5 @@ linter:
rules: rules:
constant_identifier_names: false constant_identifier_names: false
non_constant_identifier_names: false non_constant_identifier_names: false
unnecessary_string_escapes: false unnecessary_string_escapes: false
avoid_web_libraries_in_flutter: false
+2
View File
@@ -4,4 +4,6 @@ class Constant {
static const pdfMimeType = 'application/pdf'; static const pdfMimeType = 'application/pdf';
static const textHtmlMimeType = 'text/html'; static const textHtmlMimeType = 'text/html';
static const octetStreamMimeType = 'application/octet-stream'; static const octetStreamMimeType = 'application/octet-stream';
static const pdfExtension = '.pdf';
static const imageType = 'image';
} }
@@ -3,8 +3,6 @@ import 'package:core/domain/preview/supported_preview_file_types.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType { extension MediaTypeExtension on MediaType {
static const String imageType = 'image';
bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType); bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType);
bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType); bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType);
@@ -216,10 +216,6 @@ class ImagePaths {
return AssetsPaths.images + imageName; return AssetsPaths.images + imageName;
} }
String _getIconPath(String iconName) {
return AssetsPaths.icons + iconName;
}
String getConfigurationImagePath(String imageName) { String getConfigurationImagePath(String imageName) {
return AssetsPaths.configurationImages + imageName; return AssetsPaths.configurationImages + imageName;
} }
@@ -75,7 +75,7 @@ class _TextFieldFormBuilderState extends State<TextFormFieldBuilder> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextFormField( return TextField(
key: widget.key, key: widget.key,
controller: _controller, controller: _controller,
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
@@ -105,7 +105,7 @@ class _TextFieldFormBuilderState extends State<TextFormFieldBuilder> {
} }
} }
}, },
onFieldSubmitted: widget.onTextSubmitted, onSubmitted: widget.onTextSubmitted,
onTap: widget.onTap, onTap: widget.onTap,
); );
} }
+9 -1
View File
@@ -1,5 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:core/data/model/print_attachment.dart'; import 'package:core/data/model/print_attachment.dart';
import 'package:core/utils/app_logger.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:core/utils/html/html_utils.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:printing/printing.dart';
class PrintUtils { class PrintUtils {
Future<void> printPDFFile(Uint8List bytes, String fileName) async {
await Printing.layoutPdf(
name: fileName,
onLayout: (_) => bytes);
}
Element? _createUserInformationElement({ Element? _createUserInformationElement({
required String appName, required String appName,
required String userName, required String userName,
+72
View File
@@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
archive:
dependency: transitive
description:
name: archive
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265
url: "https://pub.dev"
source: hosted
version: "3.5.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -17,6 +25,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -73,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.3+7" version: "0.3.3+7"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.3"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@@ -344,6 +376,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image:
dependency: transitive
description:
name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
url: "https://pub.dev"
source: hosted
version: "4.1.7"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -464,6 +504,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -496,6 +552,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.1" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
+2
View File
@@ -73,6 +73,8 @@ dependencies:
linkify: 5.0.0 linkify: 5.0.0
printing: 5.12.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -4,9 +4,15 @@ import 'package:tmail_ui_user/features/base/styles/circle_loading_widget_styles.
class CircleLoadingWidget extends StatelessWidget { class CircleLoadingWidget extends StatelessWidget {
final double? size; final double? size;
final double? strokeWidth;
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
const CircleLoadingWidget({super.key, this.size, this.padding}); const CircleLoadingWidget({
super.key,
this.size,
this.padding,
this.strokeWidth
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -16,9 +22,9 @@ class CircleLoadingWidget extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: size ?? CircleLoadingWidgetStyles.size, width: size ?? CircleLoadingWidgetStyles.size,
height: size ?? CircleLoadingWidgetStyles.size, height: size ?? CircleLoadingWidgetStyles.size,
child: const CircularProgressIndicator( child: CircularProgressIndicator(
color: CircleLoadingWidgetStyles.progressColor, color: CircleLoadingWidgetStyles.progressColor,
strokeWidth: CircleLoadingWidgetStyles.width, strokeWidth: strokeWidth ?? CircleLoadingWidgetStyles.width,
) )
), ),
); );
@@ -26,9 +32,9 @@ class CircleLoadingWidget extends StatelessWidget {
return SizedBox( return SizedBox(
width: size ?? CircleLoadingWidgetStyles.size, width: size ?? CircleLoadingWidgetStyles.size,
height: size ?? CircleLoadingWidgetStyles.size, height: size ?? CircleLoadingWidgetStyles.size,
child: const CircularProgressIndicator( child: CircularProgressIndicator(
color: CircleLoadingWidgetStyles.progressColor, color: CircleLoadingWidgetStyles.progressColor,
strokeWidth: CircleLoadingWidgetStyles.width, strokeWidth: strokeWidth ?? CircleLoadingWidgetStyles.width,
) )
); );
} }
@@ -1948,7 +1948,7 @@ class ComposerController extends BaseController {
return; return;
} }
if (fileUpload.type?.startsWith(MediaTypeExtension.imageType) == true) { if (fileUpload.type?.startsWith(Constant.imageType) == true) {
final fileInfo = await fileUpload.toFileInfo(); final fileInfo = await fileUpload.toFileInfo();
if (fileInfo != null) { if (fileInfo != null) {
_uploadInlineAttachmentsAction(fileInfo); _uploadInlineAttachmentsAction(fileInfo);
@@ -0,0 +1 @@
class DownloadAttachmentInteractorIsNull implements Exception {}
@@ -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/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_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/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/email_supervisor_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/controller/single_email_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'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart';
@@ -69,7 +68,6 @@ class EmailBindings extends BaseBindings {
Get.find<DownloadAttachmentForWebInteractor>(), Get.find<DownloadAttachmentForWebInteractor>(),
Get.find<GetAllIdentitiesInteractor>(), Get.find<GetAllIdentitiesInteractor>(),
Get.find<StoreOpenedEmailInteractor>(), Get.find<StoreOpenedEmailInteractor>(),
Get.find<ViewAttachmentForWebInteractor>(),
Get.find<PrintEmailInteractor>(), Get.find<PrintEmailInteractor>(),
Get.find<StoreEventAttendanceStatusInteractor>(), Get.find<StoreEventAttendanceStatusInteractor>(),
)); ));
@@ -150,8 +148,6 @@ class EmailBindings extends BaseBindings {
Get.find<AccountRepository>(), Get.find<AccountRepository>(),
Get.find<AuthenticationOIDCRepository>())); Get.find<AuthenticationOIDCRepository>()));
Get.lazyPut(() => GetStoredEmailStateInteractor(Get.find<EmailRepository>())); Get.lazyPut(() => GetStoredEmailStateInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => ViewAttachmentForWebInteractor(
Get.find<DownloadAttachmentForWebInteractor>()));
Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find<EmailRepository>())); Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => PrintEmailInteractor(Get.find<EmailRepository>())); Get.lazyPut(() => PrintEmailInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => StoreEventAttendanceStatusInteractor(Get.find<EmailRepository>())); Get.lazyPut(() => StoreEventAttendanceStatusInteractor(Get.find<EmailRepository>()));
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:better_open_file/better_open_file.dart' as open_file; import 'package:better_open_file/better_open_file.dart' as open_file;
import 'package:core/core.dart'; 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/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart';
import 'package:jmap_dart_client/jmap/mdn/mdn.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/email/eml_attachment.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
import 'package:permission_handler/permission_handler.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/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/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/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/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/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/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/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_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/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/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/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/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/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/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_loaded.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.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/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_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/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/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/action/mailbox_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.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<MailboxDashBoardController>(); final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final emailSupervisorController = Get.find<EmailSupervisorController>(); final emailSupervisorController = Get.find<EmailSupervisorController>();
final _downloadManager = Get.find<DownloadManager>(); final _downloadManager = Get.find<DownloadManager>();
final _printUtils = Get.find<PrintUtils>();
final _attachmentListScrollController = ScrollController(); final _attachmentListScrollController = ScrollController();
final emailContentScrollController = ScrollController(); final emailContentScrollController = ScrollController();
@@ -126,7 +127,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor;
final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor;
final StoreOpenedEmailInteractor _storeOpenedEmailInteractor; final StoreOpenedEmailInteractor _storeOpenedEmailInteractor;
final ViewAttachmentForWebInteractor _viewAttachmentForWebInteractor;
final PrintEmailInteractor _printEmailInteractor; final PrintEmailInteractor _printEmailInteractor;
final StoreEventAttendanceStatusInteractor _storeEventAttendanceStatusInteractor; final StoreEventAttendanceStatusInteractor _storeEventAttendanceStatusInteractor;
@@ -175,7 +175,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
this._downloadAttachmentForWebInteractor, this._downloadAttachmentForWebInteractor,
this._getAllIdentitiesInteractor, this._getAllIdentitiesInteractor,
this._storeOpenedEmailInteractor, this._storeOpenedEmailInteractor,
this._viewAttachmentForWebInteractor,
this._printEmailInteractor, this._printEmailInteractor,
this._storeEventAttendanceStatusInteractor, this._storeEventAttendanceStatusInteractor,
); );
@@ -210,12 +209,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_moveToMailboxSuccess(success); _moveToMailboxSuccess(success);
} else if (success is MarkAsStarEmailSuccess) { } else if (success is MarkAsStarEmailSuccess) {
_markAsEmailStarSuccess(success); _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) { } else if (success is DownloadAttachmentForWebSuccess) {
_downloadAttachmentForWebSuccessAction(success); _downloadAttachmentForWebSuccessAction(success);
} else if (success is StartDownloadAttachmentForWeb) { } else if (success is StartDownloadAttachmentForWeb) {
@@ -252,8 +245,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_downloadAttachmentsFailure(failure); _downloadAttachmentsFailure(failure);
} else if (failure is ExportAttachmentFailure) { } else if (failure is ExportAttachmentFailure) {
_exportAttachmentFailureAction(failure); _exportAttachmentFailureAction(failure);
} else if (failure is ViewAttachmentForWebFailure) {
_viewAttachmentForWebFailureAction(failure);
} else if (failure is DownloadAttachmentForWebFailure) { } else if (failure is DownloadAttachmentForWebFailure) {
_downloadAttachmentForWebFailureAction(failure); _downloadAttachmentForWebFailureAction(failure);
} else if (failure is ParseCalendarEventFailure) { } 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) { void _downloadAttachmentForWebSuccessAction(DownloadAttachmentForWebSuccess success) {
log('SingleEmailController::_downloadAttachmentForWebSuccessAction():'); log('SingleEmailController::_downloadAttachmentForWebSuccessAction():');
@@ -859,25 +829,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
success.attachment.generateFileName()); success.attachment.generateFileName());
} }
void _viewAttachmentForWebSuccessAction( void _downloadPDFFile(Uint8List bytes, String fileName) {
ViewAttachmentForWebSuccess success, _downloadManager.createAnchorElementDownloadFileWeb(bytes, fileName);
) { }
log('SingleEmailController::_viewAttachmentForWebSuccessAction():');
final mimeType = success.attachment.type?.mimeType ??
lookupMimeType('', headerBytes: success.bytes);
if (mimeType != Constant.pdfMimeType) {
_downloadAttachmentForWebSuccessAction(success);
return;
}
_updateAttachmentsViewState(success.attachment.blobId, Right(success)); void _printPDFFile(Uint8List bytes, String fileName) async {
await _printUtils.printPDFFile(bytes, fileName);
mailboxDashBoardController.deleteDownloadTask(success.taskId);
_downloadManager.openDownloadedFileWeb(
success.bytes,
success.attachment.type?.mimeType,
success.attachment.name);
} }
void _downloadAttachmentForWebFailureAction(DownloadAttachmentForWebFailure failure) { 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( void _updateAttachmentsViewState(
Id? attachmentBlobId, Id? attachmentBlobId,
Either<Failure, Success> viewState) { Either<Failure, Success> viewState) {
@@ -1533,13 +1485,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
if (responsiveUtils.isMobile(context)) { if (responsiveUtils.isMobile(context)) {
(AttachmentListBottomSheetBuilder(context, attachments, imagePaths, _attachmentListScrollController) (AttachmentListBottomSheetBuilder(context, attachments, imagePaths, _attachmentListScrollController)
..onCloseButtonAction(() => popBack()) ..onCloseButtonAction(() => popBack())
..onDownloadAttachmentFileAction((attachment) { ..onDownloadAttachmentFileAction((attachment) => handleDownloadAttachmentAction(context, attachment))
if (PlatformInfo.isWeb) { ..onViewAttachmentFileAction((attachment) => handleViewAttachmentAction(context, attachment))
downloadAttachmentForWeb(attachment);
} else {
exportAttachment(context, attachment);
}
})
).show(); ).show();
} else { } else {
Get.dialog( Get.dialog(
@@ -1551,20 +1498,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
scrollController: _attachmentListScrollController, scrollController: _attachmentListScrollController,
backgroundColor: Colors.black.withAlpha(24), backgroundColor: Colors.black.withAlpha(24),
onCloseButtonAction: () => popBack(), onCloseButtonAction: () => popBack(),
onDownloadAttachmentFileAction: (attachment) { onDownloadAttachmentFileAction: (attachment) => handleDownloadAttachmentAction(context, attachment),
if (PlatformInfo.isWeb) { onViewAttachmentFileAction: (attachment) => handleViewAttachmentAction(context, attachment),
downloadAttachmentForWeb(attachment);
} else {
exportAttachment(context, attachment);
}
},
// onViewAttachmentFileAction: (attachment) {
// if (PlatformInfo.isWeb) {
// viewAttachmentForWeb(attachment);
// } else {
// exportAttachment(context, attachment);
// }
// },
) )
), ),
barrierColor: AppColor.colorDefaultCupertinoActionSheet, barrierColor: AppColor.colorDefaultCupertinoActionSheet,
@@ -1839,4 +1774,56 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
downloadAttachmentForWeb(emlAttachment); 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<void> 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,
)
);
},
);
}
} }
@@ -1,7 +1,6 @@
import 'package:core/data/constants/constant.dart';
import 'package:core/domain/extensions/media_type_extension.dart'; import 'package:core/domain/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.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:http_parser/http_parser.dart';
import 'package:model/email/attachment.dart'; import 'package:model/email/attachment.dart';
@@ -9,7 +8,7 @@ extension AttachmentExtension on Attachment {
String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) { String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) {
final mediaType = type ?? fileMediaType; final mediaType = type ?? fileMediaType;
log('AttachmentExtension::getIcon(): mediaType: $mediaType');
if (isDisplayedPDFIcon) { if (isDisplayedPDFIcon) {
return imagePaths.icFilePdf; return imagePaths.icFilePdf;
} }
@@ -33,6 +32,7 @@ extension AttachmentExtension on Attachment {
return imagePaths.icFileEPup; return imagePaths.icFileEPup;
} }
bool get isDisplayedPDFIcon => type?.mimeType == 'application/pdf' || bool get isDisplayedPDFIcon => type?.mimeType == Constant.pdfMimeType
(type?.mimeType == 'application/octet-stream' && name?.endsWith('.pdf') == true); || (type?.mimeType == Constant.octetStreamMimeType
&& name?.endsWith(Constant.pdfExtension) == true);
} }
@@ -1,4 +1,3 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:get/get_utils/src/get_utils/get_utils.dart'; import 'package:get/get_utils/src/get_utils/get_utils.dart';
import 'package:core/core.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/properties/properties.dart';
import 'package:jmap_dart_client/jmap/core/session/session.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/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/email/presentation/model/email_unsubscribe.dart';
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart';
@@ -57,14 +55,11 @@ class EmailUtils {
static bool checkingIfAttachmentActionIsEnabled(Either<Failure, Success>? state) { static bool checkingIfAttachmentActionIsEnabled(Either<Failure, Success>? state) {
return state?.fold( return state?.fold(
(failure) { (failure) {
return failure is DownloadAttachmentForWebFailure return failure is DownloadAttachmentForWebFailure;
|| failure is ViewAttachmentForWebFailure;
}, },
(success) { (success) {
return success is DownloadAttachmentForWebSuccess return success is DownloadAttachmentForWebSuccess
|| success is ViewAttachmentForWebSuccess || success is IdleDownloadAttachmentForWeb;
|| success is IdleDownloadAttachmentForWeb
|| success is IdleViewAttachmentForWeb;
}) ?? false; }) ?? false;
} }
@@ -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<PaginationPDFViewer> createState() => _PaginationPDFViewerState();
}
class _PaginationPDFViewerState extends State<PaginationPDFViewer> {
static const double _maxZoomLevelDefault = 4.0;
static const double _minZoomLevelDefault = 1.0;
final ValueNotifier<ZoomState> _zoomInPageNotifier = ValueNotifier<ZoomState>(ZoomState.activate);
final ValueNotifier<ZoomState> _zoomOutPageNotifier = ValueNotifier<ZoomState>(ZoomState.inactivate);
final ValueNotifier<int> _pageCurrentNotifier = ValueNotifier<int>(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;
}
}
}
@@ -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<PDFViewer> createState() => _PDFViewerState();
}
class _PDFViewerState extends State<PDFViewer> {
late DownloadAttachmentForWebInteractor? _downloadAttachmentForWebInteractor;
late StreamController<dartz.Either<Failure, Success>> _downloadAttachmentStreamController;
late StreamSubscription<dartz.Either<Failure, Success>> _downloadAttachmentStreamSubscription;
StreamSubscription<dartz.Either<Failure, Success>>? _downloadInteractorStreamSubscription;
CancelToken? _downloadAttachmentCancelToken;
final PdfViewerController _pdfViewerController = PdfViewerController();
final ValueNotifier<Object?> _pdfViewStateNotifier = ValueNotifier<Object?>(null);
@override
void initState() {
super.initState();
_initialStreamListener();
_downloadAttachmentAction();
}
void _initialStreamListener() {
_downloadAttachmentStreamController = StreamController<dartz.Either<Failure, Success>>.broadcast();
_downloadAttachmentStreamSubscription = _downloadAttachmentStreamController.stream.listen((viewState) {
viewState.fold(
(failure) => _pdfViewStateNotifier.value = failure,
(success) => _pdfViewStateNotifier.value = success
);
});
}
void _downloadAttachmentAction() {
_downloadAttachmentForWebInteractor = getBinding<DownloadAttachmentForWebInteractor>();
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> rect = [];
final viewWidth = viewSize.width;
final viewHeight = viewSize.height;
final maxHeight = pages.fold<double>(0.0, (maxHeight, page) => max(maxHeight, page.height));
final maxWidth = pages.fold<double>(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),
);
},
),
],
),
);
}
}
@@ -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,
),
)
],
),
);
}
}
+31 -1
View File
@@ -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": "Initializing data...",
"@initializing_data": { "@initializing_data": {
"type": "text", "type": "text",
@@ -3719,5 +3719,35 @@
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "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": {}
} }
} }
@@ -3881,4 +3881,39 @@ class AppLocalizations {
name: 'downloadMessageAsEMLFailed', 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',
);
}
} }
+64
View File
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.13.0"
archive:
dependency: transitive
description:
name: archive
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265
url: "https://pub.dev"
source: hosted
version: "3.5.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -33,6 +41,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -503,6 +527,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image:
dependency: transitive
description:
name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
url: "https://pub.dev"
source: hosted
version: "4.1.7"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -680,6 +712,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -720,6 +768,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
printing:
dependency: transitive
description:
name: printing
sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc"
url: "https://pub.dev"
source: hosted
version: "5.12.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -736,6 +792,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.2.3"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
quiver: quiver:
dependency: "direct main" dependency: "direct main"
description: description:
+57
View File
@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" 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: better_open_file:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -65,6 +73,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.6.4" version: "3.6.4"
bidi:
dependency: transitive
description:
name: bidi
sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63"
url: "https://pub.dev"
source: hosted
version: "2.0.10"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -1335,6 +1351,31 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" 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: percent_indicator:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1431,6 +1472,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
printing:
dependency: transitive
description:
name: printing
sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc"
url: "https://pub.dev"
source: hosted
version: "5.12.0"
process: process:
dependency: transitive dependency: transitive
description: description:
@@ -1455,6 +1504,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.2.3"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
+5
View File
@@ -99,6 +99,11 @@ dependencies:
url: https://github.com/linagora/dns_client.git url: https://github.com/linagora/dns_client.git
ref: twake-supported ref: twake-supported
pdf_render:
git:
url: https://github.com/linagora/flutter_pdf_render.git
ref: master
### Dependencies from pub.dev ### ### Dependencies from pub.dev ###
cupertino_icons: 1.0.6 cupertino_icons: 1.0.6
+13
View File
@@ -81,6 +81,19 @@
<span class="open-button" id="open-button-id" onClick= {handleOpenTwakeMailApp()}></span> <span class="open-button" id="open-button-id" onClick= {handleOpenTwakeMailApp()}></span>
</div> </div>
</div> </div>
<!-- This script installs pdf.js for pdf_render -->
<script
src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.min.js"
type="text/javascript"
></script>
<script type="text/javascript">
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js";
pdfRenderOptions = {
cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/cmaps/",
cMapPacked: true,
};
</script>
<!-- This script installs service_worker.js to provide PWA functionality to <!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see: application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers --> https://developers.google.com/web/fundamentals/primers/service-workers -->