TF-2764 Add PDFViewer to preview PDF attachment
This commit is contained in:
@@ -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,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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/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<DownloadAttachmentForWebInteractor>(),
|
||||
Get.find<GetAllIdentitiesInteractor>(),
|
||||
Get.find<StoreOpenedEmailInteractor>(),
|
||||
Get.find<ViewAttachmentForWebInteractor>(),
|
||||
Get.find<PrintEmailInteractor>(),
|
||||
Get.find<StoreEventAttendanceStatusInteractor>(),
|
||||
));
|
||||
@@ -150,8 +148,6 @@ class EmailBindings extends BaseBindings {
|
||||
Get.find<AccountRepository>(),
|
||||
Get.find<AuthenticationOIDCRepository>()));
|
||||
Get.lazyPut(() => GetStoredEmailStateInteractor(Get.find<EmailRepository>()));
|
||||
Get.lazyPut(() => ViewAttachmentForWebInteractor(
|
||||
Get.find<DownloadAttachmentForWebInteractor>()));
|
||||
Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find<EmailRepository>()));
|
||||
Get.lazyPut(() => PrintEmailInteractor(Get.find<EmailRepository>()));
|
||||
Get.lazyPut(() => StoreEventAttendanceStatusInteractor(Get.find<EmailRepository>()));
|
||||
|
||||
@@ -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<MailboxDashBoardController>();
|
||||
final emailSupervisorController = Get.find<EmailSupervisorController>();
|
||||
final _downloadManager = Get.find<DownloadManager>();
|
||||
final _printUtils = Get.find<PrintUtils>();
|
||||
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<Failure, Success> 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<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/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);
|
||||
}
|
||||
@@ -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<Failure, Success>? 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user