Просмотр исходного кода

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)
Matthias Rupp 3 лет назад
Родитель
Сommit
34657f820f

+ 114 - 0
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<RemotePhotoView> {
+  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<StatefulWidget> createState() {
+    return _RemotePhotoViewState();
+  }
+}

+ 9 - 56
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 {
             ],
           ),
         ),
-      ),
     );
   }
 }

+ 1 - 1
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:

+ 1 - 1
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