From 652f15e35614e0a73d51db68cb396a0ebe33aa8e Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 22 May 2024 16:32:40 +0700 Subject: [PATCH] TF-2764 Support press `Escape` shortcut key on keyboard to close viewer --- .../widgets/pdf_viewer/pdf_viewer.dart | 301 +++++++++--------- .../single_email_controller_test.dart | 3 + 2 files changed, 160 insertions(+), 144 deletions(-) diff --git a/lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart b/lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart index efc0de29d..fa9be2089 100644 --- a/lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart +++ b/lib/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart @@ -60,6 +60,7 @@ class _PDFViewerState extends State { final ValueNotifier _pdfViewStateNotifier = ValueNotifier(null); final DeviceInfoPlugin _deviceInfoPlugin = Get.find(); + final FocusNode _keyboardFocusNode = FocusNode(); @override void initState() { @@ -116,161 +117,167 @@ class _PDFViewerState extends State { _downloadInteractorStreamSubscription?.cancel(); _downloadInteractorStreamSubscription = null; _downloadAttachmentCancelToken = null; + _keyboardFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return Center( - child: Stack( - children: [ - Center( - child: SizedBox( - width: double.infinity, - child: ValueListenableBuilder( - valueListenable: _pdfViewStateNotifier, - builder: (context, viewState, widget) { - if (viewState is DownloadAttachmentForWebSuccess) { - return PdfViewer.openData( - viewState.bytes, - viewerController: _pdfViewerController, - onError: (error) { - logError('_PDFViewerState::build:openData:onError:: $error'); - _pdfViewStateNotifier.value = DownloadAttachmentForWebFailure(exception: error); - }, - params: PdfViewerParams( - panAxis: PanAxis.vertical, - scrollByMouseWheel: 0.5, - layoutPages: (viewSize, pages) { - List rect = []; - final viewWidth = viewSize.width; - final viewHeight = viewSize.height; - final maxHeight = pages.fold(0.0, (maxHeight, page) => max(maxHeight, page.height)); - final maxWidth = pages.fold(0.0, (maxWidth, page) => max(maxWidth, page.width)); - final ratio = viewHeight / max(maxHeight, maxWidth); - log('_PDFViewerState::build: viewWidth = $viewWidth | viewHeight = $viewHeight | maxHeight = $maxHeight | ratio = $ratio'); - var top = 0.0; - double padding = 16.0; - for (var page in pages) { - final width = page.width * ratio; - final height = page.height * ratio; - final left = viewWidth > viewHeight ? (viewWidth / 2) - (width / 2) : 0.0; - rect.add(Rect.fromLTWH(left, top, width, height)); - top += height + padding; - } - return rect; + return RawKeyboardListener( + focusNode: _keyboardFocusNode, + autofocus: true, + onKey: _handleKeyboardEventListener, + child: 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); }, - onClickOutSidePageViewer: _closeView - ), - ); - } 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 + params: PdfViewerParams( + panAxis: PanAxis.vertical, + scrollByMouseWheel: 0.5, + layoutPages: (viewSize, pages) { + List rect = []; + final viewWidth = viewSize.width; + final viewHeight = viewSize.height; + final maxHeight = pages.fold(0.0, (maxHeight, page) => max(maxHeight, page.height)); + final maxWidth = pages.fold(0.0, (maxWidth, page) => max(maxWidth, page.width)); + final ratio = viewHeight / max(maxHeight, maxWidth); + log('_PDFViewerState::build: viewWidth = $viewWidth | viewHeight = $viewHeight | maxHeight = $maxHeight | ratio = $ratio'); + var top = 0.0; + double padding = 16.0; + for (var page in pages) { + final width = page.width * ratio; + final height = page.height * ratio; + final left = viewWidth > viewHeight ? (viewWidth / 2) - (width / 2) : 0.0; + rect.add(Rect.fromLTWH(left, top, width, height)); + top += height + padding; + } + return rect; + }, + onClickOutSidePageViewer: _closeView ), - ), - ); - } 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 FutureBuilder( - future: _deviceInfoPlugin.deviceInfo, - builder: (context, deviceInfo) { - BrowserName? browserName; - if (deviceInfo.hasData && deviceInfo.data is WebBrowserInfo) { - browserName = (deviceInfo.data as WebBrowserInfo).browserName; - } - return TopBarPDFViewer( - attachment: widget.attachment, - downloadAction: () => widget.downloadAction?.call( - viewState.bytes, - widget.attachment.generateFileName() - ), - printAction: isBrowserSupportedPrinting(browserName) ? - () => widget.printAction?.call(viewState.bytes, widget.attachment.generateFileName()) - : null, ); - }); - } - return child ?? const SizedBox.shrink(); - }, - child: TopBarPDFViewer( - attachment: widget.attachment, - closeAction: () { - _downloadAttachmentCancelToken?.cancel(); - Navigator.maybeOf(context)?.pop(); - }, + } 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, + ) + ); + } + }, + ) ), ), - ), - ValueListenableBuilder( - valueListenable: _pdfViewerController, - builder: (_, __, ___) { - if (_pdfViewerController.isReady) { - return Align( - alignment: AlignmentDirectional.bottomCenter, - child: PaginationPDFViewer( - pdfViewerController: _pdfViewerController, - ), - ); - } else { - return const SizedBox.shrink(); + Align( + alignment: AlignmentDirectional.topCenter, + child: ValueListenableBuilder( + valueListenable: _pdfViewStateNotifier, + builder: (context, viewState, child) { + if (viewState is DownloadAttachmentForWebSuccess) { + return FutureBuilder( + future: _deviceInfoPlugin.deviceInfo, + builder: (context, deviceInfo) { + BrowserName? browserName; + if (deviceInfo.hasData && deviceInfo.data is WebBrowserInfo) { + browserName = (deviceInfo.data as WebBrowserInfo).browserName; + } + return TopBarPDFViewer( + attachment: widget.attachment, + downloadAction: () => widget.downloadAction?.call( + viewState.bytes, + widget.attachment.generateFileName() + ), + printAction: isBrowserSupportedPrinting(browserName) ? + () => widget.printAction?.call(viewState.bytes, widget.attachment.generateFileName()) + : null, + ); + }); + } + 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), - ); - }, - ), - ], + ), + 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), + ); + }, + ), + ], + ), ), ); } @@ -279,4 +286,10 @@ class _PDFViewerState extends State { _downloadAttachmentCancelToken?.cancel(); Navigator.maybeOf(context)?.pop(); } + + void _handleKeyboardEventListener(RawKeyEvent event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) { + _closeView(); + } + } } diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index e5cc10875..a64474e93 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -8,6 +8,9 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/calendar_event_capability.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_accept_response.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart';