From 34657f820f2e42f37122163e16f451a085403012 Mon Sep 17 00:00:00 2001 From: Matthias Rupp Date: Mon, 20 Jun 2022 20:29:42 +0200 Subject: [PATCH] Allow zooming in image viewer (#227) * Allow zooming in image viewer * Use thumbnailProvider as initial provider * Set maximum zoom level to 100% * Implement custom swipe listener in remote_photo_view * Dart format * Disable swipe gestures when zoomed in (prevents panning) --- .../asset_viewer/ui/remote_photo_view.dart | 114 ++++++++++++++++++ .../asset_viewer/views/image_viewer_page.dart | 65 ++-------- mobile/pubspec.lock | 2 +- mobile/pubspec.yaml | 2 +- 4 files changed, 125 insertions(+), 58 deletions(-) create mode 100644 mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart diff --git a/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart new file mode 100644 index 0000000000..a9a19b0d51 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart @@ -0,0 +1,114 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:photo_view/photo_view.dart'; + +enum _RemoteImageStatus { empty, thumbnail, full } + +class _RemotePhotoViewState extends State { + late CachedNetworkImageProvider _imageProvider; + _RemoteImageStatus _status = _RemoteImageStatus.empty; + bool _zoomedIn = false; + + static const int swipeThreshold = 100; + + @override + Widget build(BuildContext context) { + bool allowMoving = _status == _RemoteImageStatus.full; + + return PhotoView( + imageProvider: _imageProvider, + minScale: PhotoViewComputedScale.contained, + maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, + enablePanAlways: true, + scaleStateChangedCallback: _scaleStateChanged, + onScaleEnd: _onScaleListener); + } + + void _onScaleListener(BuildContext context, ScaleEndDetails details, + PhotoViewControllerValue controllerValue) { + // Disable swipe events when zoomed in + if (_zoomedIn) return; + + if (controllerValue.position.dy > swipeThreshold) { + widget.onSwipeDown(); + } else if (controllerValue.position.dy < -swipeThreshold) { + widget.onSwipeUp(); + } + } + + void _scaleStateChanged(PhotoViewScaleState state) { + _zoomedIn = state == PhotoViewScaleState.zoomedIn; + } + + CachedNetworkImageProvider _authorizedImageProvider(String url) { + return CachedNetworkImageProvider(url, + headers: {"Authorization": widget.authToken}, cacheKey: url); + } + + void _performStateTransition( + _RemoteImageStatus newStatus, CachedNetworkImageProvider provider) { + // Transition to same status is forbidden + if (_status == newStatus) return; + // Transition full -> thumbnail is forbidden + if (_status == _RemoteImageStatus.full && + newStatus == _RemoteImageStatus.thumbnail) return; + + if (!mounted) return; + + setState(() { + _status = newStatus; + _imageProvider = provider; + }); + } + + void _loadImages() { + CachedNetworkImageProvider thumbnailProvider = + _authorizedImageProvider(widget.thumbnailUrl); + _imageProvider = thumbnailProvider; + + thumbnailProvider + .resolve(const ImageConfiguration()) + .addListener(ImageStreamListener((ImageInfo imageInfo, _) { + _performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider); + })); + + CachedNetworkImageProvider fullProvider = + _authorizedImageProvider(widget.imageUrl); + fullProvider + .resolve(const ImageConfiguration()) + .addListener(ImageStreamListener((ImageInfo imageInfo, _) { + _performStateTransition(_RemoteImageStatus.full, fullProvider); + })); + } + + @override + void initState() { + _loadImages(); + super.initState(); + } +} + +class RemotePhotoView extends StatefulWidget { + const RemotePhotoView( + {Key? key, + required this.thumbnailUrl, + required this.imageUrl, + required this.authToken, + required this.onSwipeDown, + required this.onSwipeUp}) + : super(key: key); + + final String thumbnailUrl; + final String imageUrl; + final String authToken; + + final void Function() onSwipeDown; + final void Function() onSwipeUp; + + @override + State createState() { + return _RemotePhotoViewState(); + } +} diff --git a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart index 6dd7fd1457..8954b987b6 100644 --- a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart @@ -1,8 +1,6 @@ import 'package:auto_route/auto_route.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; @@ -10,6 +8,7 @@ import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_stat import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart'; @@ -63,64 +62,19 @@ class ImageViewerPage extends HookConsumerWidget { ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context); }, ), - body: SwipeDetector( - onSwipeDown: (_) { - AutoRouter.of(context).pop(); - }, - onSwipeUp: (_) { - showInfo(); - }, - child: SafeArea( + body: SafeArea( child: Stack( children: [ Center( child: Hero( tag: heroTag, - child: CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: imageUrl, - httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, - fadeInDuration: const Duration(milliseconds: 250), - errorWidget: (context, url, error) => ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: Wrap( - spacing: 32, - runSpacing: 32, - alignment: WrapAlignment.center, - children: [ - const Text( - "Failed To Render Image - Possibly Corrupted Data", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 16, color: Colors.white), - ), - SingleChildScrollView( - child: Text( - error.toString(), - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.grey[400]), - ), - ), - ], - ), - ), - placeholder: (context, url) { - return CachedNetworkImage( - cacheKey: thumbnailUrl, - fit: BoxFit.cover, - imageUrl: thumbnailUrl, - httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, - placeholderFadeInDuration: const Duration(milliseconds: 0), - progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale( - scale: 0.2, - child: CircularProgressIndicator(value: downloadProgress.progress), - ), - errorWidget: (context, url, error) => Icon( - Icons.error, - color: Colors.grey[300], - ), - ); - }, - ), + child: RemotePhotoView( + thumbnailUrl: thumbnailUrl, + imageUrl: imageUrl, + authToken: "Bearer ${box.get(accessTokenKey)}", + onSwipeDown: () => AutoRouter.of(context).pop(), + onSwipeUp: () => showInfo(), + ) ), ), if (downloadAssetStatus == DownloadAssetStatus.loading) @@ -130,7 +84,6 @@ class ImageViewerPage extends HookConsumerWidget { ], ), ), - ), ); } } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index d06aedafea..d1b1a5771b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -757,7 +757,7 @@ packages: name: photo_view url: "https://pub.dartlang.org" source: hosted - version: "0.13.0" + version: "0.14.0" platform: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 46a1787fa3..71cce3d37c 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: chewie: ^1.2.2 sliver_tools: ^0.2.5 badges: ^2.0.2 - photo_view: ^0.13.0 + photo_view: ^0.14.0 socket_io_client: ^2.0.0-beta.4-nullsafety.0 flutter_map: ^0.14.0 flutter_udid: ^2.0.0