mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	(fix)mobile: Improve the gallery to improve scale, double tap, and swipe gesture detection (#1502)
* photoviewgallery * stiffer scrolling to react more like google photos * adds a dx threshhold for the swipe/up down from the original dropped point * stopped wrapping imageview in gallery viewer to avoid the double photoview issue. breaks imageview page pinch-to-zoom, so i need to fix that for other callers * refactors gallery view to use remoteimage directly and breaks imageviewpage * removed image_viewer_page * adds minscale * adds photo_view to repository * double tap to zoom out with hacked commit * double tapping! * got up and down swipe gestures working * fixed wrong cache and headers in image providers * fixed image quality and added videos back in * local loading asset image fix * precaches images * fixes lint errors * deleted remote_photo_view and more linters * fixes scale * load preview and load original * precache does original / preview as well * refactored image providers to nice functions and added JPEG thumbnail format to remote image thumbnail lookup * moved photo_view to shared/ui/ * three stage loading with webp and fixes some thumbnail fits * fixed local thumbnail * fixed paging in iOS
This commit is contained in:
		@@ -1,205 +0,0 @@
 | 
				
			|||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					 | 
				
			||||||
import 'package:openapi/api.dart';
 | 
					 | 
				
			||||||
import 'package:photo_manager/photo_manager.dart'
 | 
					 | 
				
			||||||
    show AssetEntityImageProvider, ThumbnailSize;
 | 
					 | 
				
			||||||
import 'package:photo_view/photo_view.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum _RemoteImageStatus { empty, thumbnail, preview, full }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _RemotePhotoViewState extends State<RemotePhotoView> {
 | 
					 | 
				
			||||||
  late ImageProvider _imageProvider;
 | 
					 | 
				
			||||||
  _RemoteImageStatus _status = _RemoteImageStatus.empty;
 | 
					 | 
				
			||||||
  bool _zoomedIn = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  late ImageProvider _fullProvider;
 | 
					 | 
				
			||||||
  late ImageProvider _previewProvider;
 | 
					 | 
				
			||||||
  late ImageProvider _thumbnailProvider;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    final bool forbidZoom = _status == _RemoteImageStatus.thumbnail;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return IgnorePointer(
 | 
					 | 
				
			||||||
      ignoring: forbidZoom,
 | 
					 | 
				
			||||||
      child: Listener(
 | 
					 | 
				
			||||||
        onPointerMove: handleSwipUpDown,
 | 
					 | 
				
			||||||
        child: PhotoView(
 | 
					 | 
				
			||||||
          imageProvider: _imageProvider,
 | 
					 | 
				
			||||||
          minScale: PhotoViewComputedScale.contained,
 | 
					 | 
				
			||||||
          enablePanAlways: false,
 | 
					 | 
				
			||||||
          scaleStateChangedCallback: _scaleStateChanged,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void handleSwipUpDown(PointerMoveEvent details) {
 | 
					 | 
				
			||||||
    int sensitivity = 15;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (_zoomedIn) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (details.delta.dy > sensitivity) {
 | 
					 | 
				
			||||||
      widget.onSwipeDown();
 | 
					 | 
				
			||||||
    } else if (details.delta.dy < -sensitivity) {
 | 
					 | 
				
			||||||
      widget.onSwipeUp();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _scaleStateChanged(PhotoViewScaleState state) {
 | 
					 | 
				
			||||||
    _zoomedIn = state != PhotoViewScaleState.initial;
 | 
					 | 
				
			||||||
    if (_zoomedIn) {
 | 
					 | 
				
			||||||
      widget.isZoomedListener.value = true;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      widget.isZoomedListener.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    widget.isZoomedFunction();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  CachedNetworkImageProvider _authorizedImageProvider(
 | 
					 | 
				
			||||||
    String url,
 | 
					 | 
				
			||||||
    String cacheKey,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return CachedNetworkImageProvider(
 | 
					 | 
				
			||||||
      url,
 | 
					 | 
				
			||||||
      headers: {"Authorization": widget.authToken},
 | 
					 | 
				
			||||||
      cacheKey: cacheKey,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _performStateTransition(
 | 
					 | 
				
			||||||
    _RemoteImageStatus newStatus,
 | 
					 | 
				
			||||||
    ImageProvider provider,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    if (_status == newStatus) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (_status == _RemoteImageStatus.full &&
 | 
					 | 
				
			||||||
        newStatus == _RemoteImageStatus.thumbnail) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (_status == _RemoteImageStatus.preview &&
 | 
					 | 
				
			||||||
        newStatus == _RemoteImageStatus.thumbnail) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (_status == _RemoteImageStatus.full &&
 | 
					 | 
				
			||||||
        newStatus == _RemoteImageStatus.preview) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!mounted) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setState(() {
 | 
					 | 
				
			||||||
      _status = newStatus;
 | 
					 | 
				
			||||||
      _imageProvider = provider;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _loadImages() {
 | 
					 | 
				
			||||||
    if (widget.asset.isLocal) {
 | 
					 | 
				
			||||||
      _imageProvider = AssetEntityImageProvider(
 | 
					 | 
				
			||||||
        widget.asset.local!,
 | 
					 | 
				
			||||||
        isOriginal: false,
 | 
					 | 
				
			||||||
        thumbnailSize: const ThumbnailSize.square(250),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      _fullProvider = AssetEntityImageProvider(widget.asset.local!);
 | 
					 | 
				
			||||||
      _fullProvider.resolve(const ImageConfiguration()).addListener(
 | 
					 | 
				
			||||||
        ImageStreamListener((ImageInfo image, _) {
 | 
					 | 
				
			||||||
          _performStateTransition(
 | 
					 | 
				
			||||||
            _RemoteImageStatus.full,
 | 
					 | 
				
			||||||
            _fullProvider,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    _thumbnailProvider = _authorizedImageProvider(
 | 
					 | 
				
			||||||
      getThumbnailUrl(widget.asset.remote!),
 | 
					 | 
				
			||||||
      getThumbnailCacheKey(widget.asset.remote!),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    _imageProvider = _thumbnailProvider;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    _thumbnailProvider.resolve(const ImageConfiguration()).addListener(
 | 
					 | 
				
			||||||
      ImageStreamListener((ImageInfo imageInfo, _) {
 | 
					 | 
				
			||||||
        _performStateTransition(
 | 
					 | 
				
			||||||
          _RemoteImageStatus.thumbnail,
 | 
					 | 
				
			||||||
          _thumbnailProvider,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (widget.loadPreview) {
 | 
					 | 
				
			||||||
      _previewProvider = _authorizedImageProvider(
 | 
					 | 
				
			||||||
        getThumbnailUrl(widget.asset.remote!, type: ThumbnailFormat.JPEG),
 | 
					 | 
				
			||||||
        getThumbnailCacheKey(widget.asset.remote!, type: ThumbnailFormat.JPEG),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      _previewProvider.resolve(const ImageConfiguration()).addListener(
 | 
					 | 
				
			||||||
        ImageStreamListener((ImageInfo imageInfo, _) {
 | 
					 | 
				
			||||||
          _performStateTransition(_RemoteImageStatus.preview, _previewProvider);
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (widget.loadOriginal) {
 | 
					 | 
				
			||||||
      _fullProvider = _authorizedImageProvider(
 | 
					 | 
				
			||||||
        getImageUrl(widget.asset.remote!),
 | 
					 | 
				
			||||||
        getImageCacheKey(widget.asset.remote!),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      _fullProvider.resolve(const ImageConfiguration()).addListener(
 | 
					 | 
				
			||||||
        ImageStreamListener((ImageInfo imageInfo, _) {
 | 
					 | 
				
			||||||
          _performStateTransition(_RemoteImageStatus.full, _fullProvider);
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void initState() {
 | 
					 | 
				
			||||||
    super.initState();
 | 
					 | 
				
			||||||
    _loadImages();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void dispose() async {
 | 
					 | 
				
			||||||
    super.dispose();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (_status == _RemoteImageStatus.full) {
 | 
					 | 
				
			||||||
      await _fullProvider.evict();
 | 
					 | 
				
			||||||
    } else if (_status == _RemoteImageStatus.preview) {
 | 
					 | 
				
			||||||
      await _previewProvider.evict();
 | 
					 | 
				
			||||||
    } else if (_status == _RemoteImageStatus.thumbnail) {
 | 
					 | 
				
			||||||
      await _thumbnailProvider.evict();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await _imageProvider.evict();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RemotePhotoView extends StatefulWidget {
 | 
					 | 
				
			||||||
  const RemotePhotoView({
 | 
					 | 
				
			||||||
    Key? key,
 | 
					 | 
				
			||||||
    required this.asset,
 | 
					 | 
				
			||||||
    required this.authToken,
 | 
					 | 
				
			||||||
    required this.loadPreview,
 | 
					 | 
				
			||||||
    required this.loadOriginal,
 | 
					 | 
				
			||||||
    required this.isZoomedFunction,
 | 
					 | 
				
			||||||
    required this.isZoomedListener,
 | 
					 | 
				
			||||||
    required this.onSwipeDown,
 | 
					 | 
				
			||||||
    required this.onSwipeUp,
 | 
					 | 
				
			||||||
  }) : super(key: key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final Asset asset;
 | 
					 | 
				
			||||||
  final String authToken;
 | 
					 | 
				
			||||||
  final bool loadPreview;
 | 
					 | 
				
			||||||
  final bool loadOriginal;
 | 
					 | 
				
			||||||
  final void Function() onSwipeDown;
 | 
					 | 
				
			||||||
  final void Function() onSwipeUp;
 | 
					 | 
				
			||||||
  final void Function() isZoomedFunction;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final ValueNotifier<bool> isZoomedListener;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  State<StatefulWidget> createState() {
 | 
					 | 
				
			||||||
    return _RemotePhotoViewState();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,4 +1,7 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
@@ -9,14 +12,21 @@ import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
 | 
					import 'package:immich_mobile/modules/home/services/asset.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
 | 
					import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
					import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
					import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart' as api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ignore: must_be_immutable
 | 
					// ignore: must_be_immutable
 | 
				
			||||||
class GalleryViewerPage extends HookConsumerWidget {
 | 
					class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			||||||
@@ -40,7 +50,8 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
    final isZoomed = useState<bool>(false);
 | 
					    final isZoomed = useState<bool>(false);
 | 
				
			||||||
    final indexOfAsset = useState(assetList.indexOf(asset));
 | 
					    final indexOfAsset = useState(assetList.indexOf(asset));
 | 
				
			||||||
    final isPlayingMotionVideo = useState(false);
 | 
					    final isPlayingMotionVideo = useState(false);
 | 
				
			||||||
    ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
 | 
					    late Offset localPosition;
 | 
				
			||||||
 | 
					    final authToken = 'Bearer ${box.get(accessTokenKey)}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PageController controller =
 | 
					    PageController controller =
 | 
				
			||||||
        PageController(initialPage: assetList.indexOf(asset));
 | 
					        PageController(initialPage: assetList.indexOf(asset));
 | 
				
			||||||
@@ -57,7 +68,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
      [],
 | 
					      [],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getAssetExif() async {
 | 
					    void getAssetExif() async {
 | 
				
			||||||
      if (assetList[indexOfAsset.value].isRemote) {
 | 
					      if (assetList[indexOfAsset.value].isRemote) {
 | 
				
			||||||
        assetDetail = await ref
 | 
					        assetDetail = await ref
 | 
				
			||||||
            .watch(assetServiceProvider)
 | 
					            .watch(assetServiceProvider)
 | 
				
			||||||
@@ -68,7 +79,84 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Thumbnail image of a remote asset. Required asset.remote != null
 | 
				
			||||||
 | 
					    ImageProvider remoteThumbnailImageProvider(Asset asset, api.ThumbnailFormat type) {
 | 
				
			||||||
 | 
					      return CachedNetworkImageProvider(
 | 
				
			||||||
 | 
					        getThumbnailUrl(
 | 
				
			||||||
 | 
					          asset.remote!,
 | 
				
			||||||
 | 
					          type: type,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        cacheKey: getThumbnailCacheKey(
 | 
				
			||||||
 | 
					          asset.remote!,
 | 
				
			||||||
 | 
					          type: type,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        headers: {"Authorization": authToken},
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Original (large) image of a remote asset. Required asset.remote != null
 | 
				
			||||||
 | 
					    ImageProvider originalImageProvider(Asset asset) {
 | 
				
			||||||
 | 
					      return CachedNetworkImageProvider(
 | 
				
			||||||
 | 
					        getImageUrl(asset.remote!),
 | 
				
			||||||
 | 
					        cacheKey: getImageCacheKey(asset.remote!),
 | 
				
			||||||
 | 
					        headers: {"Authorization": authToken},
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Thumbnail image of a local asset. Required asset.local != null
 | 
				
			||||||
 | 
					    ImageProvider localThumbnailImageProvider(Asset asset) {
 | 
				
			||||||
 | 
					      return AssetEntityImageProvider(
 | 
				
			||||||
 | 
					        asset.local!,
 | 
				
			||||||
 | 
					        isOriginal: false,
 | 
				
			||||||
 | 
					        thumbnailSize: const ThumbnailSize.square(250),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Original (large) image of a local asset. Required asset.local != null
 | 
				
			||||||
 | 
					    ImageProvider localImageProvider(Asset asset) {
 | 
				
			||||||
 | 
					      return AssetEntityImageProvider(asset.local!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void precacheNextImage(int index) {
 | 
				
			||||||
 | 
					      if (index < assetList.length && index > 0) {
 | 
				
			||||||
 | 
					        final asset = assetList[index];
 | 
				
			||||||
 | 
					        if (asset.isLocal) {
 | 
				
			||||||
 | 
					          // Preload the local asset
 | 
				
			||||||
 | 
					          precacheImage(localImageProvider(asset), context);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Probably load WEBP either way
 | 
				
			||||||
 | 
					          precacheImage(
 | 
				
			||||||
 | 
					            remoteThumbnailImageProvider(
 | 
				
			||||||
 | 
					              asset, 
 | 
				
			||||||
 | 
					              api.ThumbnailFormat.WEBP,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            context,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          if (isLoadPreview.value) {
 | 
				
			||||||
 | 
					            // Precache the JPEG thumbnail
 | 
				
			||||||
 | 
					            precacheImage(
 | 
				
			||||||
 | 
					              remoteThumbnailImageProvider(
 | 
				
			||||||
 | 
					                asset,
 | 
				
			||||||
 | 
					                api.ThumbnailFormat.JPEG,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              context,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (isLoadOriginal.value) {
 | 
				
			||||||
 | 
					            // Preload the original asset
 | 
				
			||||||
 | 
					            precacheImage(
 | 
				
			||||||
 | 
					              originalImageProvider(asset),
 | 
				
			||||||
 | 
					              context,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void showInfo() {
 | 
					    void showInfo() {
 | 
				
			||||||
 | 
					      if (assetList[indexOfAsset.value].isRemote) {
 | 
				
			||||||
        showModalBottomSheet(
 | 
					        showModalBottomSheet(
 | 
				
			||||||
          shape: RoundedRectangleBorder(
 | 
					          shape: RoundedRectangleBorder(
 | 
				
			||||||
            borderRadius: BorderRadius.circular(15.0),
 | 
					            borderRadius: BorderRadius.circular(15.0),
 | 
				
			||||||
@@ -82,14 +170,6 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    //make isZoomed listener call instead
 | 
					 | 
				
			||||||
    void isZoomedMethod() {
 | 
					 | 
				
			||||||
      if (isZoomedListener.value) {
 | 
					 | 
				
			||||||
        isZoomed.value = true;
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        isZoomed.value = false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void handleDelete(Asset deleteAsset) {
 | 
					    void handleDelete(Asset deleteAsset) {
 | 
				
			||||||
@@ -122,6 +202,28 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void handleSwipeUpDown(DragUpdateDetails details) {
 | 
				
			||||||
 | 
					      int sensitivity = 15;
 | 
				
			||||||
 | 
					      int dxThreshhold = 50;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isZoomed.value) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Check for delta from initial down point
 | 
				
			||||||
 | 
					      final d = details.localPosition - localPosition;
 | 
				
			||||||
 | 
					      // If the magnitude of the dx swipe is large, we probably didn't mean to go down
 | 
				
			||||||
 | 
					      if (d.dx.abs() > dxThreshhold) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (details.delta.dy > sensitivity) {
 | 
				
			||||||
 | 
					        AutoRouter.of(context).pop();
 | 
				
			||||||
 | 
					      } else if (details.delta.dy < -sensitivity) {
 | 
				
			||||||
 | 
					        showInfo();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      backgroundColor: Colors.black,
 | 
					      backgroundColor: Colors.black,
 | 
				
			||||||
      appBar: TopControlAppBar(
 | 
					      appBar: TopControlAppBar(
 | 
				
			||||||
@@ -150,61 +252,93 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
        onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
 | 
					        onAddToAlbumPressed: () => addToAlbum(assetList[indexOfAsset.value]),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: SafeArea(
 | 
					      body: SafeArea(
 | 
				
			||||||
        child: PageView.builder(
 | 
					        child: PhotoViewGallery.builder(
 | 
				
			||||||
          controller: controller,
 | 
					          scaleStateChangedCallback: (state) => isZoomed.value = state != PhotoViewScaleState.initial,
 | 
				
			||||||
          pageSnapping: true,
 | 
					          pageController: controller,
 | 
				
			||||||
          physics: isZoomed.value
 | 
					          scrollPhysics: isZoomed.value
 | 
				
			||||||
              ? const NeverScrollableScrollPhysics()
 | 
					              ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
 | 
				
			||||||
              : const BouncingScrollPhysics(),
 | 
					              : (Platform.isIOS 
 | 
				
			||||||
 | 
					                ? const BouncingScrollPhysics()  // Use bouncing physics for iOS
 | 
				
			||||||
 | 
					                : const ImmichPageViewScrollPhysics() // Use heavy physics for Android
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
          itemCount: assetList.length,
 | 
					          itemCount: assetList.length,
 | 
				
			||||||
          scrollDirection: Axis.horizontal,
 | 
					          scrollDirection: Axis.horizontal,
 | 
				
			||||||
          onPageChanged: (value) {
 | 
					          onPageChanged: (value) {
 | 
				
			||||||
 | 
					            // Precache image
 | 
				
			||||||
 | 
					            if (indexOfAsset.value < value) {
 | 
				
			||||||
 | 
					              // Moving forwards, so precache the next asset
 | 
				
			||||||
 | 
					              precacheNextImage(value + 1);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              // Moving backwards, so precache previous asset
 | 
				
			||||||
 | 
					              precacheNextImage(value - 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            indexOfAsset.value = value;
 | 
					            indexOfAsset.value = value;
 | 
				
			||||||
            HapticFeedback.selectionClick();
 | 
					            HapticFeedback.selectionClick();
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          itemBuilder: (context, index) {
 | 
					          loadingBuilder: isLoadPreview.value ? (context, event) {
 | 
				
			||||||
            getAssetExif();
 | 
					            final asset = assetList[indexOfAsset.value];
 | 
				
			||||||
 | 
					            if (!asset.isLocal) {
 | 
				
			||||||
 | 
					              // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive
 | 
				
			||||||
 | 
					              // Three-Stage Loading (WEBP -> JPEG -> Original)
 | 
				
			||||||
 | 
					              final webPThumbnail = CachedNetworkImage(
 | 
				
			||||||
 | 
					                imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.WEBP),
 | 
				
			||||||
 | 
					                cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.WEBP),
 | 
				
			||||||
 | 
					                httpHeaders: { 'Authorization': authToken },
 | 
				
			||||||
 | 
					                progressIndicatorBuilder: (_, __, ___) => const Center(child: ImmichLoadingIndicator(),),
 | 
				
			||||||
 | 
					                fit: BoxFit.contain,
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (assetList[index].isImage) {
 | 
					              return CachedNetworkImage(
 | 
				
			||||||
              if (isPlayingMotionVideo.value) {
 | 
					                imageUrl: getThumbnailUrl(asset.remote!, type: api.ThumbnailFormat.JPEG),
 | 
				
			||||||
                return VideoViewerPage(
 | 
					                cacheKey: getThumbnailCacheKey(asset.remote!, type: api.ThumbnailFormat.JPEG),
 | 
				
			||||||
                  asset: assetList[index],
 | 
					                httpHeaders: { 'Authorization': authToken },
 | 
				
			||||||
                  isMotionVideo: true,
 | 
					                fit: BoxFit.contain,
 | 
				
			||||||
                  onVideoEnded: () {
 | 
					                placeholder: (_, __) => webPThumbnail,
 | 
				
			||||||
                    isPlayingMotionVideo.value = false;
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                return ImageViewerPage(
 | 
					              return Image(
 | 
				
			||||||
                  authToken: 'Bearer ${box.get(accessTokenKey)}',
 | 
					                image: localThumbnailImageProvider(asset),
 | 
				
			||||||
                  isZoomedFunction: isZoomedMethod,
 | 
					                fit: BoxFit.contain,
 | 
				
			||||||
                  isZoomedListener: isZoomedListener,
 | 
					 | 
				
			||||||
                  asset: assetList[index],
 | 
					 | 
				
			||||||
                  heroTag: assetList[index].id,
 | 
					 | 
				
			||||||
                  loadPreview: isLoadPreview.value,
 | 
					 | 
				
			||||||
                  loadOriginal: isLoadOriginal.value,
 | 
					 | 
				
			||||||
                  showExifSheet: showInfo,
 | 
					 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					          } : null,
 | 
				
			||||||
 | 
					          builder: (context, index) {
 | 
				
			||||||
 | 
					            getAssetExif();
 | 
				
			||||||
 | 
					            if (assetList[index].isImage && !isPlayingMotionVideo.value) {
 | 
				
			||||||
 | 
					              // Show photo
 | 
				
			||||||
 | 
					              final ImageProvider provider;
 | 
				
			||||||
 | 
					              if (assetList[index].isLocal) {
 | 
				
			||||||
 | 
					                provider = localImageProvider(assetList[index]);
 | 
				
			||||||
              } else {
 | 
					              } else {
 | 
				
			||||||
              return GestureDetector(
 | 
					                if (isLoadOriginal.value) {
 | 
				
			||||||
                onVerticalDragUpdate: (details) {
 | 
					                  provider = originalImageProvider(assetList[index]);
 | 
				
			||||||
                  const int sensitivity = 15;
 | 
					                } else {
 | 
				
			||||||
                  if (details.delta.dy > sensitivity) {
 | 
					                  provider = remoteThumbnailImageProvider(
 | 
				
			||||||
                    // swipe down
 | 
					                    assetList[index], 
 | 
				
			||||||
                    AutoRouter.of(context).pop();
 | 
					                    api.ThumbnailFormat.JPEG,
 | 
				
			||||||
                  } else if (details.delta.dy < -sensitivity) {
 | 
					                  );
 | 
				
			||||||
                    // swipe up
 | 
					 | 
				
			||||||
                    showInfo();
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                },
 | 
					              }
 | 
				
			||||||
                child: Hero(
 | 
					              return PhotoViewGalleryPageOptions(
 | 
				
			||||||
                  tag: assetList[index].id,
 | 
					                onDragStart: (_, details, __) => localPosition = details.localPosition,
 | 
				
			||||||
 | 
					                onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
				
			||||||
 | 
					                imageProvider: provider,
 | 
				
			||||||
 | 
					                heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
 | 
				
			||||||
 | 
					                minScale: PhotoViewComputedScale.contained,
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              return PhotoViewGalleryPageOptions.customChild(
 | 
				
			||||||
 | 
					                onDragStart: (_, details, __) => localPosition = details.localPosition,
 | 
				
			||||||
 | 
					                onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
 | 
				
			||||||
 | 
					                heroAttributes: PhotoViewHeroAttributes(tag: assetList[index].id),
 | 
				
			||||||
                child: VideoViewerPage(
 | 
					                child: VideoViewerPage(
 | 
				
			||||||
                  asset: assetList[index],
 | 
					                  asset: assetList[index],
 | 
				
			||||||
                    isMotionVideo: false,
 | 
					                  isMotionVideo: isPlayingMotionVideo.value,
 | 
				
			||||||
                    onVideoEnded: () {},
 | 
					                  onVideoEnded: () {
 | 
				
			||||||
                  ),
 | 
					                    if (isPlayingMotionVideo.value) {
 | 
				
			||||||
 | 
					                      isPlayingMotionVideo.value = false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -214,3 +348,19 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImmichPageViewScrollPhysics extends ScrollPhysics {
 | 
				
			||||||
 | 
					  const ImmichPageViewScrollPhysics({super.parent});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  ImmichPageViewScrollPhysics applyTo(ScrollPhysics? ancestor) {
 | 
				
			||||||
 | 
					    return ImmichPageViewScrollPhysics(parent: buildParent(ancestor)!);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  SpringDescription get spring => const SpringDescription(
 | 
				
			||||||
 | 
					    mass: 100,
 | 
				
			||||||
 | 
					    stiffness: 100,
 | 
				
			||||||
 | 
					    damping: .90,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,84 +0,0 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ignore: must_be_immutable
 | 
					 | 
				
			||||||
class ImageViewerPage extends HookConsumerWidget {
 | 
					 | 
				
			||||||
  final String heroTag;
 | 
					 | 
				
			||||||
  final Asset asset;
 | 
					 | 
				
			||||||
  final String authToken;
 | 
					 | 
				
			||||||
  final ValueNotifier<bool> isZoomedListener;
 | 
					 | 
				
			||||||
  final void Function() isZoomedFunction;
 | 
					 | 
				
			||||||
  final void Function()? showExifSheet;
 | 
					 | 
				
			||||||
  final bool loadPreview;
 | 
					 | 
				
			||||||
  final bool loadOriginal;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ImageViewerPage({
 | 
					 | 
				
			||||||
    Key? key,
 | 
					 | 
				
			||||||
    required this.heroTag,
 | 
					 | 
				
			||||||
    required this.asset,
 | 
					 | 
				
			||||||
    required this.authToken,
 | 
					 | 
				
			||||||
    required this.isZoomedFunction,
 | 
					 | 
				
			||||||
    required this.isZoomedListener,
 | 
					 | 
				
			||||||
    required this.loadPreview,
 | 
					 | 
				
			||||||
    required this.loadOriginal,
 | 
					 | 
				
			||||||
    this.showExifSheet,
 | 
					 | 
				
			||||||
  }) : super(key: key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Asset? assetDetail;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					 | 
				
			||||||
    final downloadAssetStatus =
 | 
					 | 
				
			||||||
        ref.watch(imageViewerStateProvider).downloadAssetStatus;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    getAssetExif() async {
 | 
					 | 
				
			||||||
      if (asset.isRemote) {
 | 
					 | 
				
			||||||
        assetDetail =
 | 
					 | 
				
			||||||
            await ref.watch(assetServiceProvider).getAssetById(asset.id);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // TODO local exif parsing?
 | 
					 | 
				
			||||||
        assetDetail = asset;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    useEffect(
 | 
					 | 
				
			||||||
      () {
 | 
					 | 
				
			||||||
        getAssetExif();
 | 
					 | 
				
			||||||
        return null;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      [],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Stack(
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        Center(
 | 
					 | 
				
			||||||
          child: Hero(
 | 
					 | 
				
			||||||
            tag: heroTag,
 | 
					 | 
				
			||||||
            child: RemotePhotoView(
 | 
					 | 
				
			||||||
              asset: asset,
 | 
					 | 
				
			||||||
              authToken: authToken,
 | 
					 | 
				
			||||||
              loadPreview: loadPreview,
 | 
					 | 
				
			||||||
              loadOriginal: loadOriginal,
 | 
					 | 
				
			||||||
              isZoomedFunction: isZoomedFunction,
 | 
					 | 
				
			||||||
              isZoomedListener: isZoomedListener,
 | 
					 | 
				
			||||||
              onSwipeDown: () => AutoRouter.of(context).pop(),
 | 
					 | 
				
			||||||
              onSwipeUp: (asset.isRemote && showExifSheet  != null) ? showExifSheet! : () {},
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        if (downloadAssetStatus == DownloadAssetStatus.loading)
 | 
					 | 
				
			||||||
          const Center(
 | 
					 | 
				
			||||||
            child: ImmichLoadingIndicator(),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/modules/album/views/select_additional_user_for_sha
 | 
				
			|||||||
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
 | 
					import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/views/sharing_page.dart';
 | 
					import 'package:immich_mobile/modules/album/views/sharing_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
 | 
					import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
 | 
					import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
 | 
				
			||||||
@@ -52,7 +51,6 @@ part 'router.gr.dart';
 | 
				
			|||||||
      transitionsBuilder: TransitionsBuilders.fadeIn,
 | 
					      transitionsBuilder: TransitionsBuilders.fadeIn,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    AutoRoute(page: GalleryViewerPage, guards: [AuthGuard]),
 | 
					    AutoRoute(page: GalleryViewerPage, guards: [AuthGuard]),
 | 
				
			||||||
    AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
 | 
					 | 
				
			||||||
    AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
 | 
					    AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
 | 
				
			||||||
    AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
 | 
					    AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
 | 
				
			||||||
    AutoRoute(page: SearchResultPage, guards: [AuthGuard]),
 | 
					    AutoRoute(page: SearchResultPage, guards: [AuthGuard]),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,21 +48,6 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
          child: GalleryViewerPage(
 | 
					          child: GalleryViewerPage(
 | 
				
			||||||
              key: args.key, assetList: args.assetList, asset: args.asset));
 | 
					              key: args.key, assetList: args.assetList, asset: args.asset));
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    ImageViewerRoute.name: (routeData) {
 | 
					 | 
				
			||||||
      final args = routeData.argsAs<ImageViewerRouteArgs>();
 | 
					 | 
				
			||||||
      return MaterialPageX<dynamic>(
 | 
					 | 
				
			||||||
          routeData: routeData,
 | 
					 | 
				
			||||||
          child: ImageViewerPage(
 | 
					 | 
				
			||||||
              key: args.key,
 | 
					 | 
				
			||||||
              heroTag: args.heroTag,
 | 
					 | 
				
			||||||
              asset: args.asset,
 | 
					 | 
				
			||||||
              authToken: args.authToken,
 | 
					 | 
				
			||||||
              isZoomedFunction: args.isZoomedFunction,
 | 
					 | 
				
			||||||
              isZoomedListener: args.isZoomedListener,
 | 
					 | 
				
			||||||
              loadPreview: args.loadPreview,
 | 
					 | 
				
			||||||
              loadOriginal: args.loadOriginal,
 | 
					 | 
				
			||||||
              showExifSheet: args.showExifSheet));
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    VideoViewerRoute.name: (routeData) {
 | 
					    VideoViewerRoute.name: (routeData) {
 | 
				
			||||||
      final args = routeData.argsAs<VideoViewerRouteArgs>();
 | 
					      final args = routeData.argsAs<VideoViewerRouteArgs>();
 | 
				
			||||||
      return MaterialPageX<dynamic>(
 | 
					      return MaterialPageX<dynamic>(
 | 
				
			||||||
@@ -204,8 +189,6 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
            ]),
 | 
					            ]),
 | 
				
			||||||
        RouteConfig(GalleryViewerRoute.name,
 | 
					        RouteConfig(GalleryViewerRoute.name,
 | 
				
			||||||
            path: '/gallery-viewer-page', guards: [authGuard]),
 | 
					            path: '/gallery-viewer-page', guards: [authGuard]),
 | 
				
			||||||
        RouteConfig(ImageViewerRoute.name,
 | 
					 | 
				
			||||||
            path: '/image-viewer-page', guards: [authGuard]),
 | 
					 | 
				
			||||||
        RouteConfig(VideoViewerRoute.name,
 | 
					        RouteConfig(VideoViewerRoute.name,
 | 
				
			||||||
            path: '/video-viewer-page', guards: [authGuard]),
 | 
					            path: '/video-viewer-page', guards: [authGuard]),
 | 
				
			||||||
        RouteConfig(BackupControllerRoute.name,
 | 
					        RouteConfig(BackupControllerRoute.name,
 | 
				
			||||||
@@ -299,71 +282,6 @@ class GalleryViewerRouteArgs {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// generated route for
 | 
					 | 
				
			||||||
/// [ImageViewerPage]
 | 
					 | 
				
			||||||
class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
 | 
					 | 
				
			||||||
  ImageViewerRoute(
 | 
					 | 
				
			||||||
      {Key? key,
 | 
					 | 
				
			||||||
      required String heroTag,
 | 
					 | 
				
			||||||
      required Asset asset,
 | 
					 | 
				
			||||||
      required String authToken,
 | 
					 | 
				
			||||||
      required void Function() isZoomedFunction,
 | 
					 | 
				
			||||||
      required ValueNotifier<bool> isZoomedListener,
 | 
					 | 
				
			||||||
      required bool loadPreview,
 | 
					 | 
				
			||||||
      required bool loadOriginal,
 | 
					 | 
				
			||||||
      void Function()? showExifSheet})
 | 
					 | 
				
			||||||
      : super(ImageViewerRoute.name,
 | 
					 | 
				
			||||||
            path: '/image-viewer-page',
 | 
					 | 
				
			||||||
            args: ImageViewerRouteArgs(
 | 
					 | 
				
			||||||
                key: key,
 | 
					 | 
				
			||||||
                heroTag: heroTag,
 | 
					 | 
				
			||||||
                asset: asset,
 | 
					 | 
				
			||||||
                authToken: authToken,
 | 
					 | 
				
			||||||
                isZoomedFunction: isZoomedFunction,
 | 
					 | 
				
			||||||
                isZoomedListener: isZoomedListener,
 | 
					 | 
				
			||||||
                loadPreview: loadPreview,
 | 
					 | 
				
			||||||
                loadOriginal: loadOriginal,
 | 
					 | 
				
			||||||
                showExifSheet: showExifSheet));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static const String name = 'ImageViewerRoute';
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ImageViewerRouteArgs {
 | 
					 | 
				
			||||||
  const ImageViewerRouteArgs(
 | 
					 | 
				
			||||||
      {this.key,
 | 
					 | 
				
			||||||
      required this.heroTag,
 | 
					 | 
				
			||||||
      required this.asset,
 | 
					 | 
				
			||||||
      required this.authToken,
 | 
					 | 
				
			||||||
      required this.isZoomedFunction,
 | 
					 | 
				
			||||||
      required this.isZoomedListener,
 | 
					 | 
				
			||||||
      required this.loadPreview,
 | 
					 | 
				
			||||||
      required this.loadOriginal,
 | 
					 | 
				
			||||||
      this.showExifSheet});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final Key? key;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final String heroTag;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final Asset asset;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final String authToken;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final void Function() isZoomedFunction;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final ValueNotifier<bool> isZoomedListener;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final bool loadPreview;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final bool loadOriginal;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final void Function()? showExifSheet;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  String toString() {
 | 
					 | 
				
			||||||
    return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, loadPreview: $loadPreview, loadOriginal: $loadOriginal, showExifSheet: $showExifSheet}';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// generated route for
 | 
					/// generated route for
 | 
				
			||||||
/// [VideoViewerPage]
 | 
					/// [VideoViewerPage]
 | 
				
			||||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
 | 
					class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										653
									
								
								mobile/lib/shared/ui/photo_view/photo_view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										653
									
								
								mobile/lib/shared/ui/photo_view/photo_view.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,653 @@
 | 
				
			|||||||
 | 
					library photo_view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_controller.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_scalestate_controller.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/core/photo_view_core.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_wrappers.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export 'src/controller/photo_view_controller.dart';
 | 
				
			||||||
 | 
					export 'src/controller/photo_view_scalestate_controller.dart';
 | 
				
			||||||
 | 
					export 'src/core/photo_view_gesture_detector.dart'
 | 
				
			||||||
 | 
					    show PhotoViewGestureDetectorScope, PhotoViewPageViewScrollPhysics;
 | 
				
			||||||
 | 
					export 'src/photo_view_computed_scale.dart';
 | 
				
			||||||
 | 
					export 'src/photo_view_scale_state.dart';
 | 
				
			||||||
 | 
					export 'src/utils/photo_view_hero_attributes.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A [StatefulWidget] that contains all the photo view rendering elements.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Sample code to use within an image:
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// PhotoView(
 | 
				
			||||||
 | 
					///  imageProvider: imageProvider,
 | 
				
			||||||
 | 
					///  loadingBuilder: (context, progress) => Center(
 | 
				
			||||||
 | 
					///            child: Container(
 | 
				
			||||||
 | 
					///              width: 20.0,
 | 
				
			||||||
 | 
					///              height: 20.0,
 | 
				
			||||||
 | 
					///              child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					///                value: _progress == null
 | 
				
			||||||
 | 
					///                    ? null
 | 
				
			||||||
 | 
					///                    : _progress.cumulativeBytesLoaded /
 | 
				
			||||||
 | 
					///                        _progress.expectedTotalBytes,
 | 
				
			||||||
 | 
					///              ),
 | 
				
			||||||
 | 
					///            ),
 | 
				
			||||||
 | 
					///          ),
 | 
				
			||||||
 | 
					///  backgroundDecoration: BoxDecoration(color: Colors.black),
 | 
				
			||||||
 | 
					///  gaplessPlayback: false,
 | 
				
			||||||
 | 
					///  customSize: MediaQuery.of(context).size,
 | 
				
			||||||
 | 
					///  heroAttributes: const HeroAttributes(
 | 
				
			||||||
 | 
					///   tag: "someTag",
 | 
				
			||||||
 | 
					///   transitionOnUserGestures: true,
 | 
				
			||||||
 | 
					///  ),
 | 
				
			||||||
 | 
					///  scaleStateChangedCallback: this.onScaleStateChanged,
 | 
				
			||||||
 | 
					///  enableRotation: true,
 | 
				
			||||||
 | 
					///  controller:  controller,
 | 
				
			||||||
 | 
					///  minScale: PhotoViewComputedScale.contained * 0.8,
 | 
				
			||||||
 | 
					///  maxScale: PhotoViewComputedScale.covered * 1.8,
 | 
				
			||||||
 | 
					///  initialScale: PhotoViewComputedScale.contained,
 | 
				
			||||||
 | 
					///  basePosition: Alignment.center,
 | 
				
			||||||
 | 
					///  scaleStateCycle: scaleStateCycle
 | 
				
			||||||
 | 
					/// );
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// You can customize to show an custom child instead of an image:
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// PhotoView.customChild(
 | 
				
			||||||
 | 
					///  child: Container(
 | 
				
			||||||
 | 
					///    width: 220.0,
 | 
				
			||||||
 | 
					///    height: 250.0,
 | 
				
			||||||
 | 
					///    child: const Text(
 | 
				
			||||||
 | 
					///      "Hello there, this is a text",
 | 
				
			||||||
 | 
					///    )
 | 
				
			||||||
 | 
					///  ),
 | 
				
			||||||
 | 
					///  childSize: const Size(220.0, 250.0),
 | 
				
			||||||
 | 
					///  backgroundDecoration: BoxDecoration(color: Colors.black),
 | 
				
			||||||
 | 
					///  gaplessPlayback: false,
 | 
				
			||||||
 | 
					///  customSize: MediaQuery.of(context).size,
 | 
				
			||||||
 | 
					///  heroAttributes: const HeroAttributes(
 | 
				
			||||||
 | 
					///   tag: "someTag",
 | 
				
			||||||
 | 
					///   transitionOnUserGestures: true,
 | 
				
			||||||
 | 
					///  ),
 | 
				
			||||||
 | 
					///  scaleStateChangedCallback: this.onScaleStateChanged,
 | 
				
			||||||
 | 
					///  enableRotation: true,
 | 
				
			||||||
 | 
					///  controller:  controller,
 | 
				
			||||||
 | 
					///  minScale: PhotoViewComputedScale.contained * 0.8,
 | 
				
			||||||
 | 
					///  maxScale: PhotoViewComputedScale.covered * 1.8,
 | 
				
			||||||
 | 
					///  initialScale: PhotoViewComputedScale.contained,
 | 
				
			||||||
 | 
					///  basePosition: Alignment.center,
 | 
				
			||||||
 | 
					///  scaleStateCycle: scaleStateCycle
 | 
				
			||||||
 | 
					/// );
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// The [maxScale], [minScale] and [initialScale] options may be [double] or a [PhotoViewComputedScale] constant
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Sample using [maxScale], [minScale] and [initialScale]
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// PhotoView(
 | 
				
			||||||
 | 
					///  imageProvider: imageProvider,
 | 
				
			||||||
 | 
					///  minScale: PhotoViewComputedScale.contained * 0.8,
 | 
				
			||||||
 | 
					///  maxScale: PhotoViewComputedScale.covered * 1.8,
 | 
				
			||||||
 | 
					///  initialScale: PhotoViewComputedScale.contained * 1.1,
 | 
				
			||||||
 | 
					/// );
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// [customSize] is used to define the viewPort size in which the image will be
 | 
				
			||||||
 | 
					/// scaled to. This argument is rarely used. By default is the size that this widget assumes.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The argument [gaplessPlayback] is used to continue showing the old image
 | 
				
			||||||
 | 
					/// (`true`), or briefly show nothing (`false`), when the [imageProvider]
 | 
				
			||||||
 | 
					/// changes.By default it's set to `false`.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// To use within an hero animation, specify [heroAttributes]. When
 | 
				
			||||||
 | 
					/// [heroAttributes] is specified, the image provider retrieval process should
 | 
				
			||||||
 | 
					/// be sync.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Sample using hero animation:
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// // screen1
 | 
				
			||||||
 | 
					///   ...
 | 
				
			||||||
 | 
					///   Hero(
 | 
				
			||||||
 | 
					///     tag: "someTag",
 | 
				
			||||||
 | 
					///     child: Image.asset(
 | 
				
			||||||
 | 
					///       "assets/large-image.jpg",
 | 
				
			||||||
 | 
					///       width: 150.0
 | 
				
			||||||
 | 
					///     ),
 | 
				
			||||||
 | 
					///   )
 | 
				
			||||||
 | 
					/// // screen2
 | 
				
			||||||
 | 
					/// ...
 | 
				
			||||||
 | 
					/// child: PhotoView(
 | 
				
			||||||
 | 
					///   imageProvider: AssetImage("assets/large-image.jpg"),
 | 
				
			||||||
 | 
					///   heroAttributes: const HeroAttributes(tag: "someTag"),
 | 
				
			||||||
 | 
					/// )
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// **Note: If you don't want to the zoomed image do not overlaps the size of the container, use [ClipRect](https://docs.flutter.io/flutter/widgets/ClipRect-class.html)**
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ## Controllers
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Controllers, when specified to PhotoView widget, enables the author(you) to listen for state updates through a `Stream` and change those values externally.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// While [PhotoViewScaleStateController] is only responsible for the `scaleState`, [PhotoViewController] is responsible for all fields os [PhotoViewControllerValue].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// To use them, pass a instance of those items on [controller] or [scaleStateController];
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Since those follows the standard controller pattern found in widgets like [PageView] and [ScrollView], whoever instantiates it, should [dispose] it afterwards.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Example of [controller] usage, only listening for state changes:
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// class _ExampleWidgetState extends State<ExampleWidget> {
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   PhotoViewController controller;
 | 
				
			||||||
 | 
					///   double scaleCopy;
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   @override
 | 
				
			||||||
 | 
					///   void initState() {
 | 
				
			||||||
 | 
					///     super.initState();
 | 
				
			||||||
 | 
					///     controller = PhotoViewController()
 | 
				
			||||||
 | 
					///       ..outputStateStream.listen(listener);
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   @override
 | 
				
			||||||
 | 
					///   void dispose() {
 | 
				
			||||||
 | 
					///     controller.dispose();
 | 
				
			||||||
 | 
					///     super.dispose();
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   void listener(PhotoViewControllerValue value){
 | 
				
			||||||
 | 
					///     setState((){
 | 
				
			||||||
 | 
					///       scaleCopy = value.scale;
 | 
				
			||||||
 | 
					///     })
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   @override
 | 
				
			||||||
 | 
					///   Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					///     return Stack(
 | 
				
			||||||
 | 
					///       children: <Widget>[
 | 
				
			||||||
 | 
					///         Positioned.fill(
 | 
				
			||||||
 | 
					///             child: PhotoView(
 | 
				
			||||||
 | 
					///               imageProvider: AssetImage("assets/pudim.png"),
 | 
				
			||||||
 | 
					///               controller: controller,
 | 
				
			||||||
 | 
					///             );
 | 
				
			||||||
 | 
					///         ),
 | 
				
			||||||
 | 
					///         Text("Scale applied: $scaleCopy")
 | 
				
			||||||
 | 
					///       ],
 | 
				
			||||||
 | 
					///     );
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// An example of [scaleStateController] with state changes:
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// class _ExampleWidgetState extends State<ExampleWidget> {
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   PhotoViewScaleStateController scaleStateController;
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   @override
 | 
				
			||||||
 | 
					///   void initState() {
 | 
				
			||||||
 | 
					///     super.initState();
 | 
				
			||||||
 | 
					///     scaleStateController = PhotoViewScaleStateController();
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   @override
 | 
				
			||||||
 | 
					///   void dispose() {
 | 
				
			||||||
 | 
					///     scaleStateController.dispose();
 | 
				
			||||||
 | 
					///     super.dispose();
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   void goBack(){
 | 
				
			||||||
 | 
					///     scaleStateController.scaleState = PhotoViewScaleState.originalSize;
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					///   @override
 | 
				
			||||||
 | 
					///   Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					///     return Stack(
 | 
				
			||||||
 | 
					///       children: <Widget>[
 | 
				
			||||||
 | 
					///         Positioned.fill(
 | 
				
			||||||
 | 
					///             child: PhotoView(
 | 
				
			||||||
 | 
					///               imageProvider: AssetImage("assets/pudim.png"),
 | 
				
			||||||
 | 
					///               scaleStateController: scaleStateController,
 | 
				
			||||||
 | 
					///             );
 | 
				
			||||||
 | 
					///         ),
 | 
				
			||||||
 | 
					///         FlatButton(
 | 
				
			||||||
 | 
					///           child: Text("Go to original size"),
 | 
				
			||||||
 | 
					///           onPressed: goBack,
 | 
				
			||||||
 | 
					///         );
 | 
				
			||||||
 | 
					///       ],
 | 
				
			||||||
 | 
					///     );
 | 
				
			||||||
 | 
					///   }
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					class PhotoView extends StatefulWidget {
 | 
				
			||||||
 | 
					  /// Creates a widget that displays a zoomable image.
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// To show an image from the network or from an asset bundle, use their respective
 | 
				
			||||||
 | 
					  /// image providers, ie: [AssetImage] or [NetworkImage]
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Internally, the image is rendered within an [Image] widget.
 | 
				
			||||||
 | 
					  const PhotoView({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.imageProvider,
 | 
				
			||||||
 | 
					    this.loadingBuilder,
 | 
				
			||||||
 | 
					    this.backgroundDecoration,
 | 
				
			||||||
 | 
					    this.wantKeepAlive = false,
 | 
				
			||||||
 | 
					    this.gaplessPlayback = false,
 | 
				
			||||||
 | 
					    this.heroAttributes,
 | 
				
			||||||
 | 
					    this.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					    this.enableRotation = false,
 | 
				
			||||||
 | 
					    this.controller,
 | 
				
			||||||
 | 
					    this.scaleStateController,
 | 
				
			||||||
 | 
					    this.maxScale,
 | 
				
			||||||
 | 
					    this.minScale,
 | 
				
			||||||
 | 
					    this.initialScale,
 | 
				
			||||||
 | 
					    this.basePosition,
 | 
				
			||||||
 | 
					    this.scaleStateCycle,
 | 
				
			||||||
 | 
					    this.onTapUp,
 | 
				
			||||||
 | 
					    this.onTapDown,
 | 
				
			||||||
 | 
					    this.onDragStart,
 | 
				
			||||||
 | 
					    this.onDragEnd,
 | 
				
			||||||
 | 
					    this.onDragUpdate,
 | 
				
			||||||
 | 
					    this.onScaleEnd,
 | 
				
			||||||
 | 
					    this.customSize,
 | 
				
			||||||
 | 
					    this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    this.tightMode,
 | 
				
			||||||
 | 
					    this.filterQuality,
 | 
				
			||||||
 | 
					    this.disableGestures,
 | 
				
			||||||
 | 
					    this.errorBuilder,
 | 
				
			||||||
 | 
					    this.enablePanAlways,
 | 
				
			||||||
 | 
					  })  : child = null,
 | 
				
			||||||
 | 
					        childSize = null,
 | 
				
			||||||
 | 
					        super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Creates a widget that displays a zoomable child.
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// It has been created to resemble [PhotoView] behavior within widgets that aren't an image, such as [Container], [Text] or a svg.
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Instead of a [imageProvider], this constructor will receive a [child] and a [childSize].
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  const PhotoView.customChild({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.child,
 | 
				
			||||||
 | 
					    this.childSize,
 | 
				
			||||||
 | 
					    this.backgroundDecoration,
 | 
				
			||||||
 | 
					    this.wantKeepAlive = false,
 | 
				
			||||||
 | 
					    this.heroAttributes,
 | 
				
			||||||
 | 
					    this.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					    this.enableRotation = false,
 | 
				
			||||||
 | 
					    this.controller,
 | 
				
			||||||
 | 
					    this.scaleStateController,
 | 
				
			||||||
 | 
					    this.maxScale,
 | 
				
			||||||
 | 
					    this.minScale,
 | 
				
			||||||
 | 
					    this.initialScale,
 | 
				
			||||||
 | 
					    this.basePosition,
 | 
				
			||||||
 | 
					    this.scaleStateCycle,
 | 
				
			||||||
 | 
					    this.onTapUp,
 | 
				
			||||||
 | 
					    this.onTapDown,
 | 
				
			||||||
 | 
					    this.onDragStart,
 | 
				
			||||||
 | 
					    this.onDragEnd,
 | 
				
			||||||
 | 
					    this.onDragUpdate,
 | 
				
			||||||
 | 
					    this.onScaleEnd,
 | 
				
			||||||
 | 
					    this.customSize,
 | 
				
			||||||
 | 
					    this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    this.tightMode,
 | 
				
			||||||
 | 
					    this.filterQuality,
 | 
				
			||||||
 | 
					    this.disableGestures,
 | 
				
			||||||
 | 
					    this.enablePanAlways,
 | 
				
			||||||
 | 
					  })  : errorBuilder = null,
 | 
				
			||||||
 | 
					        imageProvider = null,
 | 
				
			||||||
 | 
					        gaplessPlayback = false,
 | 
				
			||||||
 | 
					        loadingBuilder = null,
 | 
				
			||||||
 | 
					        super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Given a [imageProvider] it resolves into an zoomable image widget using. It
 | 
				
			||||||
 | 
					  /// is required
 | 
				
			||||||
 | 
					  final ImageProvider? imageProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// While [imageProvider] is not resolved, [loadingBuilder] is called by [PhotoView]
 | 
				
			||||||
 | 
					  /// into the screen, by default it is a centered [CircularProgressIndicator]
 | 
				
			||||||
 | 
					  final LoadingBuilder? loadingBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Show loadFailedChild when the image failed to load
 | 
				
			||||||
 | 
					  final ImageErrorWidgetBuilder? errorBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Changes the background behind image, defaults to `Colors.black`.
 | 
				
			||||||
 | 
					  final BoxDecoration? backgroundDecoration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// This is used to keep the state of an image in the gallery (e.g. scale state).
 | 
				
			||||||
 | 
					  /// `false` -> resets the state (default)
 | 
				
			||||||
 | 
					  /// `true`  -> keeps the state
 | 
				
			||||||
 | 
					  final bool wantKeepAlive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// This is used to continue showing the old image (`true`), or briefly show
 | 
				
			||||||
 | 
					  /// nothing (`false`), when the `imageProvider` changes. By default it's set
 | 
				
			||||||
 | 
					  /// to `false`.
 | 
				
			||||||
 | 
					  final bool gaplessPlayback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Attributes that are going to be passed to [PhotoViewCore]'s
 | 
				
			||||||
 | 
					  /// [Hero]. Leave this property undefined if you don't want a hero animation.
 | 
				
			||||||
 | 
					  final PhotoViewHeroAttributes? heroAttributes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Defines the size of the scaling base of the image inside [PhotoView],
 | 
				
			||||||
 | 
					  /// by default it is `MediaQuery.of(context).size`.
 | 
				
			||||||
 | 
					  final Size? customSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A [Function] to be called whenever the scaleState changes, this happens when the user double taps the content ou start to pinch-in.
 | 
				
			||||||
 | 
					  final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A flag that enables the rotation gesture support
 | 
				
			||||||
 | 
					  final bool enableRotation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The specified custom child to be shown instead of a image
 | 
				
			||||||
 | 
					  final Widget? child;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The size of the custom [child]. [PhotoView] uses this value to compute the relation between the child and the container's size to calculate the scale value.
 | 
				
			||||||
 | 
					  final Size? childSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Defines the maximum size in which the image will be allowed to assume, it
 | 
				
			||||||
 | 
					  /// is proportional to the original image size. Can be either a double (absolute value) or a
 | 
				
			||||||
 | 
					  /// [PhotoViewComputedScale], that can be multiplied by a double
 | 
				
			||||||
 | 
					  final dynamic maxScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Defines the minimum size in which the image will be allowed to assume, it
 | 
				
			||||||
 | 
					  /// is proportional to the original image size. Can be either a double (absolute value) or a
 | 
				
			||||||
 | 
					  /// [PhotoViewComputedScale], that can be multiplied by a double
 | 
				
			||||||
 | 
					  final dynamic minScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Defines the initial size in which the image will be assume in the mounting of the component, it
 | 
				
			||||||
 | 
					  /// is proportional to the original image size. Can be either a double (absolute value) or a
 | 
				
			||||||
 | 
					  /// [PhotoViewComputedScale], that can be multiplied by a double
 | 
				
			||||||
 | 
					  final dynamic initialScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A way to control PhotoView transformation factors externally and listen to its updates
 | 
				
			||||||
 | 
					  final PhotoViewControllerBase? controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A way to control PhotoViewScaleState value externally and listen to its updates
 | 
				
			||||||
 | 
					  final PhotoViewScaleStateController? scaleStateController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The alignment of the scale origin in relation to the widget size. Default is [Alignment.center]
 | 
				
			||||||
 | 
					  final Alignment? basePosition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Defines de next [PhotoViewScaleState] given the actual one. Default is [defaultScaleStateCycle]
 | 
				
			||||||
 | 
					  final ScaleStateCycle? scaleStateCycle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A pointer that will trigger a tap has stopped contacting the screen at a
 | 
				
			||||||
 | 
					  /// particular location.
 | 
				
			||||||
 | 
					  final PhotoViewImageTapUpCallback? onTapUp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A pointer that might cause a tap has contacted the screen at a particular
 | 
				
			||||||
 | 
					  /// location.
 | 
				
			||||||
 | 
					  final PhotoViewImageTapDownCallback? onTapDown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A pointer that might cause a tap has contacted the screen at a particular
 | 
				
			||||||
 | 
					  /// location.
 | 
				
			||||||
 | 
					  final PhotoViewImageDragStartCallback? onDragStart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A pointer that might cause a tap has contacted the screen at a particular
 | 
				
			||||||
 | 
					  /// location.
 | 
				
			||||||
 | 
					  final PhotoViewImageDragEndCallback? onDragEnd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A pointer that might cause a tap has contacted the screen at a particular
 | 
				
			||||||
 | 
					  /// location.
 | 
				
			||||||
 | 
					  final PhotoViewImageDragUpdateCallback? onDragUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A pointer that will trigger a scale has stopped contacting the screen at a
 | 
				
			||||||
 | 
					  /// particular location.
 | 
				
			||||||
 | 
					  final PhotoViewImageScaleEndCallback? onScaleEnd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// [HitTestBehavior] to be passed to the internal gesture detector.
 | 
				
			||||||
 | 
					  final HitTestBehavior? gestureDetectorBehavior;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Enables tight mode, making background container assume the size of the image/child.
 | 
				
			||||||
 | 
					  /// Useful when inside a [Dialog]
 | 
				
			||||||
 | 
					  final bool? tightMode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Quality levels for image filters.
 | 
				
			||||||
 | 
					  final FilterQuality? filterQuality;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Removes gesture detector if `true`.
 | 
				
			||||||
 | 
					  // Useful when custom gesture detector is used in child widget.
 | 
				
			||||||
 | 
					  final bool? disableGestures;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Enable pan the widget even if it's smaller than the hole parent widget.
 | 
				
			||||||
 | 
					  /// Useful when you want to drag a widget without restrictions.
 | 
				
			||||||
 | 
					  final bool? enablePanAlways;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get _isCustomChild {
 | 
				
			||||||
 | 
					    return child != null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<StatefulWidget> createState() {
 | 
				
			||||||
 | 
					    return _PhotoViewState();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PhotoViewState extends State<PhotoView>
 | 
				
			||||||
 | 
					    with AutomaticKeepAliveClientMixin {
 | 
				
			||||||
 | 
					  // image retrieval
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // controller
 | 
				
			||||||
 | 
					  late bool _controlledController;
 | 
				
			||||||
 | 
					  late PhotoViewControllerBase _controller;
 | 
				
			||||||
 | 
					  late bool _controlledScaleStateController;
 | 
				
			||||||
 | 
					  late PhotoViewScaleStateController _scaleStateController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (widget.controller == null) {
 | 
				
			||||||
 | 
					      _controlledController = true;
 | 
				
			||||||
 | 
					      _controller = PhotoViewController();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _controlledController = false;
 | 
				
			||||||
 | 
					      _controller = widget.controller!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (widget.scaleStateController == null) {
 | 
				
			||||||
 | 
					      _controlledScaleStateController = true;
 | 
				
			||||||
 | 
					      _scaleStateController = PhotoViewScaleStateController();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _controlledScaleStateController = false;
 | 
				
			||||||
 | 
					      _scaleStateController = widget.scaleStateController!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _scaleStateController.outputScaleStateStream.listen(scaleStateListener);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didUpdateWidget(PhotoView oldWidget) {
 | 
				
			||||||
 | 
					    if (widget.controller == null) {
 | 
				
			||||||
 | 
					      if (!_controlledController) {
 | 
				
			||||||
 | 
					        _controlledController = true;
 | 
				
			||||||
 | 
					        _controller = PhotoViewController();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _controlledController = false;
 | 
				
			||||||
 | 
					      _controller = widget.controller!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (widget.scaleStateController == null) {
 | 
				
			||||||
 | 
					      if (!_controlledScaleStateController) {
 | 
				
			||||||
 | 
					        _controlledScaleStateController = true;
 | 
				
			||||||
 | 
					        _scaleStateController = PhotoViewScaleStateController();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _controlledScaleStateController = false;
 | 
				
			||||||
 | 
					      _scaleStateController = widget.scaleStateController!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    super.didUpdateWidget(oldWidget);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    if (_controlledController) {
 | 
				
			||||||
 | 
					      _controller.dispose();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_controlledScaleStateController) {
 | 
				
			||||||
 | 
					      _scaleStateController.dispose();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void scaleStateListener(PhotoViewScaleState scaleState) {
 | 
				
			||||||
 | 
					    if (widget.scaleStateChangedCallback != null) {
 | 
				
			||||||
 | 
					      widget.scaleStateChangedCallback!(_scaleStateController.scaleState);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    super.build(context);
 | 
				
			||||||
 | 
					    return LayoutBuilder(
 | 
				
			||||||
 | 
					      builder: (
 | 
				
			||||||
 | 
					        BuildContext context,
 | 
				
			||||||
 | 
					        BoxConstraints constraints,
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        final computedOuterSize = widget.customSize ?? constraints.biggest;
 | 
				
			||||||
 | 
					        final backgroundDecoration = widget.backgroundDecoration ??
 | 
				
			||||||
 | 
					            const BoxDecoration(color: Colors.black);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return widget._isCustomChild
 | 
				
			||||||
 | 
					            ? CustomChildWrapper(
 | 
				
			||||||
 | 
					                childSize: widget.childSize,
 | 
				
			||||||
 | 
					                backgroundDecoration: backgroundDecoration,
 | 
				
			||||||
 | 
					                heroAttributes: widget.heroAttributes,
 | 
				
			||||||
 | 
					                scaleStateChangedCallback: widget.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					                enableRotation: widget.enableRotation,
 | 
				
			||||||
 | 
					                controller: _controller,
 | 
				
			||||||
 | 
					                scaleStateController: _scaleStateController,
 | 
				
			||||||
 | 
					                maxScale: widget.maxScale,
 | 
				
			||||||
 | 
					                minScale: widget.minScale,
 | 
				
			||||||
 | 
					                initialScale: widget.initialScale,
 | 
				
			||||||
 | 
					                basePosition: widget.basePosition,
 | 
				
			||||||
 | 
					                scaleStateCycle: widget.scaleStateCycle,
 | 
				
			||||||
 | 
					                onTapUp: widget.onTapUp,
 | 
				
			||||||
 | 
					                onTapDown: widget.onTapDown,
 | 
				
			||||||
 | 
					                onDragStart: widget.onDragStart,
 | 
				
			||||||
 | 
					                onDragEnd: widget.onDragEnd,
 | 
				
			||||||
 | 
					                onDragUpdate: widget.onDragUpdate,
 | 
				
			||||||
 | 
					                onScaleEnd: widget.onScaleEnd,
 | 
				
			||||||
 | 
					                outerSize: computedOuterSize,
 | 
				
			||||||
 | 
					                gestureDetectorBehavior: widget.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					                tightMode: widget.tightMode,
 | 
				
			||||||
 | 
					                filterQuality: widget.filterQuality,
 | 
				
			||||||
 | 
					                disableGestures: widget.disableGestures,
 | 
				
			||||||
 | 
					                enablePanAlways: widget.enablePanAlways,
 | 
				
			||||||
 | 
					                child: widget.child,
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            : ImageWrapper(
 | 
				
			||||||
 | 
					                imageProvider: widget.imageProvider!,
 | 
				
			||||||
 | 
					                loadingBuilder: widget.loadingBuilder,
 | 
				
			||||||
 | 
					                backgroundDecoration: backgroundDecoration,
 | 
				
			||||||
 | 
					                gaplessPlayback: widget.gaplessPlayback,
 | 
				
			||||||
 | 
					                heroAttributes: widget.heroAttributes,
 | 
				
			||||||
 | 
					                scaleStateChangedCallback: widget.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					                enableRotation: widget.enableRotation,
 | 
				
			||||||
 | 
					                controller: _controller,
 | 
				
			||||||
 | 
					                scaleStateController: _scaleStateController,
 | 
				
			||||||
 | 
					                maxScale: widget.maxScale,
 | 
				
			||||||
 | 
					                minScale: widget.minScale,
 | 
				
			||||||
 | 
					                initialScale: widget.initialScale,
 | 
				
			||||||
 | 
					                basePosition: widget.basePosition,
 | 
				
			||||||
 | 
					                scaleStateCycle: widget.scaleStateCycle,
 | 
				
			||||||
 | 
					                onTapUp: widget.onTapUp,
 | 
				
			||||||
 | 
					                onTapDown: widget.onTapDown,
 | 
				
			||||||
 | 
					                onDragStart: widget.onDragStart,
 | 
				
			||||||
 | 
					                onDragEnd: widget.onDragEnd,
 | 
				
			||||||
 | 
					                onDragUpdate: widget.onDragUpdate,
 | 
				
			||||||
 | 
					                onScaleEnd: widget.onScaleEnd,
 | 
				
			||||||
 | 
					                outerSize: computedOuterSize,
 | 
				
			||||||
 | 
					                gestureDetectorBehavior: widget.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					                tightMode: widget.tightMode,
 | 
				
			||||||
 | 
					                filterQuality: widget.filterQuality,
 | 
				
			||||||
 | 
					                disableGestures: widget.disableGestures,
 | 
				
			||||||
 | 
					                errorBuilder: widget.errorBuilder,
 | 
				
			||||||
 | 
					                enablePanAlways: widget.enablePanAlways,
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool get wantKeepAlive => widget.wantKeepAlive;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The default [ScaleStateCycle]
 | 
				
			||||||
 | 
					PhotoViewScaleState defaultScaleStateCycle(PhotoViewScaleState actual) {
 | 
				
			||||||
 | 
					  switch (actual) {
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.initial:
 | 
				
			||||||
 | 
					      return PhotoViewScaleState.covering;
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.covering:
 | 
				
			||||||
 | 
					      return PhotoViewScaleState.originalSize;
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.originalSize:
 | 
				
			||||||
 | 
					      return PhotoViewScaleState.initial;
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.zoomedIn:
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.zoomedOut:
 | 
				
			||||||
 | 
					      return PhotoViewScaleState.initial;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return PhotoViewScaleState.initial;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a [Function] that receives the actual [PhotoViewScaleState] and returns the next one
 | 
				
			||||||
 | 
					/// It is used internally to walk in the "doubletap gesture cycle".
 | 
				
			||||||
 | 
					/// It is passed to [PhotoView.scaleStateCycle]
 | 
				
			||||||
 | 
					typedef ScaleStateCycle = PhotoViewScaleState Function(
 | 
				
			||||||
 | 
					  PhotoViewScaleState actual,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a callback when the user taps up the photoview region
 | 
				
			||||||
 | 
					typedef PhotoViewImageTapUpCallback = Function(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  TapUpDetails details,
 | 
				
			||||||
 | 
					  PhotoViewControllerValue controllerValue,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a callback when the user taps down the photoview region
 | 
				
			||||||
 | 
					typedef PhotoViewImageTapDownCallback = Function(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  TapDownDetails details,
 | 
				
			||||||
 | 
					  PhotoViewControllerValue controllerValue,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a callback when the user drags up
 | 
				
			||||||
 | 
					typedef PhotoViewImageDragStartCallback = Function(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  DragStartDetails details,
 | 
				
			||||||
 | 
					  PhotoViewControllerValue controllerValue,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a callback when the user drags 
 | 
				
			||||||
 | 
					typedef PhotoViewImageDragUpdateCallback = Function(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  DragUpdateDetails details,
 | 
				
			||||||
 | 
					  PhotoViewControllerValue controllerValue,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a callback when the user taps down the photoview region
 | 
				
			||||||
 | 
					typedef PhotoViewImageDragEndCallback = Function(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  DragEndDetails details,
 | 
				
			||||||
 | 
					  PhotoViewControllerValue controllerValue,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a callback when a user finished scale
 | 
				
			||||||
 | 
					typedef PhotoViewImageScaleEndCallback = Function(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  ScaleEndDetails details,
 | 
				
			||||||
 | 
					  PhotoViewControllerValue controllerValue,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a callback to show a widget while the image is loading, a [ImageChunkEvent] is passed to inform progress
 | 
				
			||||||
 | 
					typedef LoadingBuilder = Widget Function(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  ImageChunkEvent? event,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										446
									
								
								mobile/lib/shared/ui/photo_view/photo_view_gallery.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										446
									
								
								mobile/lib/shared/ui/photo_view/photo_view_gallery.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,446 @@
 | 
				
			|||||||
 | 
					library photo_view_gallery;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/photo_view.dart'
 | 
				
			||||||
 | 
					    show
 | 
				
			||||||
 | 
					        LoadingBuilder,
 | 
				
			||||||
 | 
					        PhotoView,
 | 
				
			||||||
 | 
					        PhotoViewImageTapDownCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageTapUpCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageDragStartCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageDragEndCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageDragUpdateCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageScaleEndCallback,
 | 
				
			||||||
 | 
					        ScaleStateCycle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_controller.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_scalestate_controller.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/core/photo_view_gesture_detector.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a [Function] that receives a index after a page change in [PhotoViewGallery]
 | 
				
			||||||
 | 
					typedef PhotoViewGalleryPageChangedCallback = void Function(int index);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type definition for a [Function] that defines a page in [PhotoViewGallery.build]
 | 
				
			||||||
 | 
					typedef PhotoViewGalleryBuilder = PhotoViewGalleryPageOptions Function(
 | 
				
			||||||
 | 
					  BuildContext context, 
 | 
				
			||||||
 | 
					  int index,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A [StatefulWidget] that shows multiple [PhotoView] widgets in a [PageView]
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Some of [PhotoView] constructor options are passed direct to [PhotoViewGallery] constructor. Those options will affect the gallery in a whole.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Some of the options may be defined to each image individually, such as `initialScale` or `PhotoViewHeroAttributes`. Those must be passed via each [PhotoViewGalleryPageOptions].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Example of usage as a list of options:
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// PhotoViewGallery(
 | 
				
			||||||
 | 
					///   pageOptions: <PhotoViewGalleryPageOptions>[
 | 
				
			||||||
 | 
					///     PhotoViewGalleryPageOptions(
 | 
				
			||||||
 | 
					///       imageProvider: AssetImage("assets/gallery1.jpg"),
 | 
				
			||||||
 | 
					///       heroAttributes: const PhotoViewHeroAttributes(tag: "tag1"),
 | 
				
			||||||
 | 
					///     ),
 | 
				
			||||||
 | 
					///     PhotoViewGalleryPageOptions(
 | 
				
			||||||
 | 
					///       imageProvider: AssetImage("assets/gallery2.jpg"),
 | 
				
			||||||
 | 
					///       heroAttributes: const PhotoViewHeroAttributes(tag: "tag2"),
 | 
				
			||||||
 | 
					///       maxScale: PhotoViewComputedScale.contained * 0.3
 | 
				
			||||||
 | 
					///     ),
 | 
				
			||||||
 | 
					///     PhotoViewGalleryPageOptions(
 | 
				
			||||||
 | 
					///       imageProvider: AssetImage("assets/gallery3.jpg"),
 | 
				
			||||||
 | 
					///       minScale: PhotoViewComputedScale.contained * 0.8,
 | 
				
			||||||
 | 
					///       maxScale: PhotoViewComputedScale.covered * 1.1,
 | 
				
			||||||
 | 
					///       heroAttributes: const HeroAttributes(tag: "tag3"),
 | 
				
			||||||
 | 
					///     ),
 | 
				
			||||||
 | 
					///   ],
 | 
				
			||||||
 | 
					///   loadingBuilder: (context, progress) => Center(
 | 
				
			||||||
 | 
					///            child: Container(
 | 
				
			||||||
 | 
					///              width: 20.0,
 | 
				
			||||||
 | 
					///              height: 20.0,
 | 
				
			||||||
 | 
					///              child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					///                value: _progress == null
 | 
				
			||||||
 | 
					///                    ? null
 | 
				
			||||||
 | 
					///                    : _progress.cumulativeBytesLoaded /
 | 
				
			||||||
 | 
					///                        _progress.expectedTotalBytes,
 | 
				
			||||||
 | 
					///              ),
 | 
				
			||||||
 | 
					///            ),
 | 
				
			||||||
 | 
					///          ),
 | 
				
			||||||
 | 
					///   backgroundDecoration: widget.backgroundDecoration,
 | 
				
			||||||
 | 
					///   pageController: widget.pageController,
 | 
				
			||||||
 | 
					///   onPageChanged: onPageChanged,
 | 
				
			||||||
 | 
					/// )
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Example of usage with builder pattern:
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// PhotoViewGallery.builder(
 | 
				
			||||||
 | 
					///   scrollPhysics: const BouncingScrollPhysics(),
 | 
				
			||||||
 | 
					///   builder: (BuildContext context, int index) {
 | 
				
			||||||
 | 
					///     return PhotoViewGalleryPageOptions(
 | 
				
			||||||
 | 
					///       imageProvider: AssetImage(widget.galleryItems[index].image),
 | 
				
			||||||
 | 
					///       initialScale: PhotoViewComputedScale.contained * 0.8,
 | 
				
			||||||
 | 
					///       minScale: PhotoViewComputedScale.contained * 0.8,
 | 
				
			||||||
 | 
					///       maxScale: PhotoViewComputedScale.covered * 1.1,
 | 
				
			||||||
 | 
					///       heroAttributes: HeroAttributes(tag: galleryItems[index].id),
 | 
				
			||||||
 | 
					///     );
 | 
				
			||||||
 | 
					///   },
 | 
				
			||||||
 | 
					///   itemCount: galleryItems.length,
 | 
				
			||||||
 | 
					///   loadingBuilder: (context, progress) => Center(
 | 
				
			||||||
 | 
					///            child: Container(
 | 
				
			||||||
 | 
					///              width: 20.0,
 | 
				
			||||||
 | 
					///              height: 20.0,
 | 
				
			||||||
 | 
					///              child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					///                value: _progress == null
 | 
				
			||||||
 | 
					///                    ? null
 | 
				
			||||||
 | 
					///                    : _progress.cumulativeBytesLoaded /
 | 
				
			||||||
 | 
					///                        _progress.expectedTotalBytes,
 | 
				
			||||||
 | 
					///              ),
 | 
				
			||||||
 | 
					///            ),
 | 
				
			||||||
 | 
					///          ),
 | 
				
			||||||
 | 
					///   backgroundDecoration: widget.backgroundDecoration,
 | 
				
			||||||
 | 
					///   pageController: widget.pageController,
 | 
				
			||||||
 | 
					///   onPageChanged: onPageChanged,
 | 
				
			||||||
 | 
					/// )
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					class PhotoViewGallery extends StatefulWidget {
 | 
				
			||||||
 | 
					  /// Construct a gallery with static items through a list of [PhotoViewGalleryPageOptions].
 | 
				
			||||||
 | 
					  const PhotoViewGallery({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.pageOptions,
 | 
				
			||||||
 | 
					    this.loadingBuilder,
 | 
				
			||||||
 | 
					    this.backgroundDecoration,
 | 
				
			||||||
 | 
					    this.wantKeepAlive = false,
 | 
				
			||||||
 | 
					    this.gaplessPlayback = false,
 | 
				
			||||||
 | 
					    this.reverse = false,
 | 
				
			||||||
 | 
					    this.pageController,
 | 
				
			||||||
 | 
					    this.onPageChanged,
 | 
				
			||||||
 | 
					    this.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					    this.enableRotation = false,
 | 
				
			||||||
 | 
					    this.scrollPhysics,
 | 
				
			||||||
 | 
					    this.scrollDirection = Axis.horizontal,
 | 
				
			||||||
 | 
					    this.customSize,
 | 
				
			||||||
 | 
					    this.allowImplicitScrolling = false,
 | 
				
			||||||
 | 
					  })  : itemCount = null,
 | 
				
			||||||
 | 
					        builder = null,
 | 
				
			||||||
 | 
					        super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Construct a gallery with dynamic items.
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// The builder must return a [PhotoViewGalleryPageOptions].
 | 
				
			||||||
 | 
					  const PhotoViewGallery.builder({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.itemCount,
 | 
				
			||||||
 | 
					    required this.builder,
 | 
				
			||||||
 | 
					    this.loadingBuilder,
 | 
				
			||||||
 | 
					    this.backgroundDecoration,
 | 
				
			||||||
 | 
					    this.wantKeepAlive = false,
 | 
				
			||||||
 | 
					    this.gaplessPlayback = false,
 | 
				
			||||||
 | 
					    this.reverse = false,
 | 
				
			||||||
 | 
					    this.pageController,
 | 
				
			||||||
 | 
					    this.onPageChanged,
 | 
				
			||||||
 | 
					    this.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					    this.enableRotation = false,
 | 
				
			||||||
 | 
					    this.scrollPhysics,
 | 
				
			||||||
 | 
					    this.scrollDirection = Axis.horizontal,
 | 
				
			||||||
 | 
					    this.customSize,
 | 
				
			||||||
 | 
					    this.allowImplicitScrolling = false,
 | 
				
			||||||
 | 
					  })  : pageOptions = null,
 | 
				
			||||||
 | 
					        assert(itemCount != null),
 | 
				
			||||||
 | 
					        assert(builder != null),
 | 
				
			||||||
 | 
					        super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// A list of options to describe the items in the gallery
 | 
				
			||||||
 | 
					  final List<PhotoViewGalleryPageOptions>? pageOptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The count of items in the gallery, only used when constructed via [PhotoViewGallery.builder]
 | 
				
			||||||
 | 
					  final int? itemCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Called to build items for the gallery when using [PhotoViewGallery.builder]
 | 
				
			||||||
 | 
					  final PhotoViewGalleryBuilder? builder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// [ScrollPhysics] for the internal [PageView]
 | 
				
			||||||
 | 
					  final ScrollPhysics? scrollPhysics;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.loadingBuilder]
 | 
				
			||||||
 | 
					  final LoadingBuilder? loadingBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.backgroundDecoration]
 | 
				
			||||||
 | 
					  final BoxDecoration? backgroundDecoration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.wantKeepAlive]
 | 
				
			||||||
 | 
					  final bool wantKeepAlive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.gaplessPlayback]
 | 
				
			||||||
 | 
					  final bool gaplessPlayback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PageView.reverse]
 | 
				
			||||||
 | 
					  final bool reverse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// An object that controls the [PageView] inside [PhotoViewGallery]
 | 
				
			||||||
 | 
					  final PageController? pageController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// An callback to be called on a page change
 | 
				
			||||||
 | 
					  final PhotoViewGalleryPageChangedCallback? onPageChanged;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.scaleStateChangedCallback]
 | 
				
			||||||
 | 
					  final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.enableRotation]
 | 
				
			||||||
 | 
					  final bool enableRotation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.customSize]
 | 
				
			||||||
 | 
					  final Size? customSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The axis along which the [PageView] scrolls. Mirror to [PageView.scrollDirection]
 | 
				
			||||||
 | 
					  final Axis scrollDirection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// When user attempts to move it to the next element, focus will traverse to the next page in the page view.
 | 
				
			||||||
 | 
					  final bool allowImplicitScrolling;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get _isBuilder => builder != null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<StatefulWidget> createState() {
 | 
				
			||||||
 | 
					    return _PhotoViewGalleryState();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PhotoViewGalleryState extends State<PhotoViewGallery> {
 | 
				
			||||||
 | 
					  late final PageController _controller =
 | 
				
			||||||
 | 
					      widget.pageController ?? PageController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void scaleStateChangedCallback(PhotoViewScaleState scaleState) {
 | 
				
			||||||
 | 
					    if (widget.scaleStateChangedCallback != null) {
 | 
				
			||||||
 | 
					      widget.scaleStateChangedCallback!(scaleState);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int get actualPage {
 | 
				
			||||||
 | 
					    return _controller.hasClients ? _controller.page!.floor() : 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int get itemCount {
 | 
				
			||||||
 | 
					    if (widget._isBuilder) {
 | 
				
			||||||
 | 
					      return widget.itemCount!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return widget.pageOptions!.length;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    // Enable corner hit test
 | 
				
			||||||
 | 
					    return PhotoViewGestureDetectorScope(
 | 
				
			||||||
 | 
					      axis: widget.scrollDirection,
 | 
				
			||||||
 | 
					      child: PageView.builder(
 | 
				
			||||||
 | 
					        reverse: widget.reverse,
 | 
				
			||||||
 | 
					        controller: _controller,
 | 
				
			||||||
 | 
					        onPageChanged: widget.onPageChanged,
 | 
				
			||||||
 | 
					        itemCount: itemCount,
 | 
				
			||||||
 | 
					        itemBuilder: _buildItem,
 | 
				
			||||||
 | 
					        scrollDirection: widget.scrollDirection,
 | 
				
			||||||
 | 
					        physics: widget.scrollPhysics,
 | 
				
			||||||
 | 
					        allowImplicitScrolling: widget.allowImplicitScrolling,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildItem(BuildContext context, int index) {
 | 
				
			||||||
 | 
					    final pageOption = _buildPageOption(context, index);
 | 
				
			||||||
 | 
					    final isCustomChild = pageOption.child != null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final PhotoView photoView = isCustomChild
 | 
				
			||||||
 | 
					        ? PhotoView.customChild(
 | 
				
			||||||
 | 
					            key: ObjectKey(index),
 | 
				
			||||||
 | 
					            childSize: pageOption.childSize,
 | 
				
			||||||
 | 
					            backgroundDecoration: widget.backgroundDecoration,
 | 
				
			||||||
 | 
					            wantKeepAlive: widget.wantKeepAlive,
 | 
				
			||||||
 | 
					            controller: pageOption.controller,
 | 
				
			||||||
 | 
					            scaleStateController: pageOption.scaleStateController,
 | 
				
			||||||
 | 
					            customSize: widget.customSize,
 | 
				
			||||||
 | 
					            heroAttributes: pageOption.heroAttributes,
 | 
				
			||||||
 | 
					            scaleStateChangedCallback: scaleStateChangedCallback,
 | 
				
			||||||
 | 
					            enableRotation: widget.enableRotation,
 | 
				
			||||||
 | 
					            initialScale: pageOption.initialScale,
 | 
				
			||||||
 | 
					            minScale: pageOption.minScale,
 | 
				
			||||||
 | 
					            maxScale: pageOption.maxScale,
 | 
				
			||||||
 | 
					            scaleStateCycle: pageOption.scaleStateCycle,
 | 
				
			||||||
 | 
					            onTapUp: pageOption.onTapUp,
 | 
				
			||||||
 | 
					            onTapDown: pageOption.onTapDown,
 | 
				
			||||||
 | 
					            onDragStart: pageOption.onDragStart,
 | 
				
			||||||
 | 
					            onDragEnd: pageOption.onDragEnd,
 | 
				
			||||||
 | 
					            onDragUpdate: pageOption.onDragUpdate,
 | 
				
			||||||
 | 
					            onScaleEnd: pageOption.onScaleEnd,
 | 
				
			||||||
 | 
					            gestureDetectorBehavior: pageOption.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					            tightMode: pageOption.tightMode,
 | 
				
			||||||
 | 
					            filterQuality: pageOption.filterQuality,
 | 
				
			||||||
 | 
					            basePosition: pageOption.basePosition,
 | 
				
			||||||
 | 
					            disableGestures: pageOption.disableGestures,
 | 
				
			||||||
 | 
					            child: pageOption.child,
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        : PhotoView(
 | 
				
			||||||
 | 
					            key: ObjectKey(index),
 | 
				
			||||||
 | 
					            imageProvider: pageOption.imageProvider,
 | 
				
			||||||
 | 
					            loadingBuilder: widget.loadingBuilder,
 | 
				
			||||||
 | 
					            backgroundDecoration: widget.backgroundDecoration,
 | 
				
			||||||
 | 
					            wantKeepAlive: widget.wantKeepAlive,
 | 
				
			||||||
 | 
					            controller: pageOption.controller,
 | 
				
			||||||
 | 
					            scaleStateController: pageOption.scaleStateController,
 | 
				
			||||||
 | 
					            customSize: widget.customSize,
 | 
				
			||||||
 | 
					            gaplessPlayback: widget.gaplessPlayback,
 | 
				
			||||||
 | 
					            heroAttributes: pageOption.heroAttributes,
 | 
				
			||||||
 | 
					            scaleStateChangedCallback: scaleStateChangedCallback,
 | 
				
			||||||
 | 
					            enableRotation: widget.enableRotation,
 | 
				
			||||||
 | 
					            initialScale: pageOption.initialScale,
 | 
				
			||||||
 | 
					            minScale: pageOption.minScale,
 | 
				
			||||||
 | 
					            maxScale: pageOption.maxScale,
 | 
				
			||||||
 | 
					            scaleStateCycle: pageOption.scaleStateCycle,
 | 
				
			||||||
 | 
					            onTapUp: pageOption.onTapUp,
 | 
				
			||||||
 | 
					            onTapDown: pageOption.onTapDown,
 | 
				
			||||||
 | 
					            onDragStart: pageOption.onDragStart,
 | 
				
			||||||
 | 
					            onDragEnd: pageOption.onDragEnd,
 | 
				
			||||||
 | 
					            onDragUpdate: pageOption.onDragUpdate,
 | 
				
			||||||
 | 
					            onScaleEnd: pageOption.onScaleEnd,
 | 
				
			||||||
 | 
					            gestureDetectorBehavior: pageOption.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					            tightMode: pageOption.tightMode,
 | 
				
			||||||
 | 
					            filterQuality: pageOption.filterQuality,
 | 
				
			||||||
 | 
					            basePosition: pageOption.basePosition,
 | 
				
			||||||
 | 
					            disableGestures: pageOption.disableGestures,
 | 
				
			||||||
 | 
					            errorBuilder: pageOption.errorBuilder,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ClipRect(
 | 
				
			||||||
 | 
					      child: photoView,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PhotoViewGalleryPageOptions _buildPageOption(BuildContext context, int index) {
 | 
				
			||||||
 | 
					    if (widget._isBuilder) {
 | 
				
			||||||
 | 
					      return widget.builder!(context, index);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return widget.pageOptions![index];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A helper class that wraps individual options of a page in [PhotoViewGallery]
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The [maxScale], [minScale] and [initialScale] options may be [double] or a [PhotoViewComputedScale] constant
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					class PhotoViewGalleryPageOptions {
 | 
				
			||||||
 | 
					  PhotoViewGalleryPageOptions({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.imageProvider,
 | 
				
			||||||
 | 
					    this.heroAttributes,
 | 
				
			||||||
 | 
					    this.minScale,
 | 
				
			||||||
 | 
					    this.maxScale,
 | 
				
			||||||
 | 
					    this.initialScale,
 | 
				
			||||||
 | 
					    this.controller,
 | 
				
			||||||
 | 
					    this.scaleStateController,
 | 
				
			||||||
 | 
					    this.basePosition,
 | 
				
			||||||
 | 
					    this.scaleStateCycle,
 | 
				
			||||||
 | 
					    this.onTapUp,
 | 
				
			||||||
 | 
					    this.onTapDown,
 | 
				
			||||||
 | 
					    this.onDragStart,
 | 
				
			||||||
 | 
					    this.onDragEnd,
 | 
				
			||||||
 | 
					    this.onDragUpdate,
 | 
				
			||||||
 | 
					    this.onScaleEnd,
 | 
				
			||||||
 | 
					    this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    this.tightMode,
 | 
				
			||||||
 | 
					    this.filterQuality,
 | 
				
			||||||
 | 
					    this.disableGestures,
 | 
				
			||||||
 | 
					    this.errorBuilder,
 | 
				
			||||||
 | 
					  })  : child = null,
 | 
				
			||||||
 | 
					        childSize = null,
 | 
				
			||||||
 | 
					        assert(imageProvider != null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PhotoViewGalleryPageOptions.customChild({
 | 
				
			||||||
 | 
					    required this.child,
 | 
				
			||||||
 | 
					    this.childSize,
 | 
				
			||||||
 | 
					    this.heroAttributes,
 | 
				
			||||||
 | 
					    this.minScale,
 | 
				
			||||||
 | 
					    this.maxScale,
 | 
				
			||||||
 | 
					    this.initialScale,
 | 
				
			||||||
 | 
					    this.controller,
 | 
				
			||||||
 | 
					    this.scaleStateController,
 | 
				
			||||||
 | 
					    this.basePosition,
 | 
				
			||||||
 | 
					    this.scaleStateCycle,
 | 
				
			||||||
 | 
					    this.onTapUp,
 | 
				
			||||||
 | 
					    this.onTapDown,
 | 
				
			||||||
 | 
					    this.onDragStart,
 | 
				
			||||||
 | 
					    this.onDragEnd,
 | 
				
			||||||
 | 
					    this.onDragUpdate,
 | 
				
			||||||
 | 
					    this.onScaleEnd,
 | 
				
			||||||
 | 
					    this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    this.tightMode,
 | 
				
			||||||
 | 
					    this.filterQuality,
 | 
				
			||||||
 | 
					    this.disableGestures,
 | 
				
			||||||
 | 
					  })  : errorBuilder = null,
 | 
				
			||||||
 | 
					        imageProvider = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.imageProvider]
 | 
				
			||||||
 | 
					  final ImageProvider? imageProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.heroAttributes]
 | 
				
			||||||
 | 
					  final PhotoViewHeroAttributes? heroAttributes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.minScale]
 | 
				
			||||||
 | 
					  final dynamic minScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.maxScale]
 | 
				
			||||||
 | 
					  final dynamic maxScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.initialScale]
 | 
				
			||||||
 | 
					  final dynamic initialScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.controller]
 | 
				
			||||||
 | 
					  final PhotoViewController? controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.scaleStateController]
 | 
				
			||||||
 | 
					  final PhotoViewScaleStateController? scaleStateController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.basePosition]
 | 
				
			||||||
 | 
					  final Alignment? basePosition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.child]
 | 
				
			||||||
 | 
					  final Widget? child;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.childSize]
 | 
				
			||||||
 | 
					  final Size? childSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.scaleStateCycle]
 | 
				
			||||||
 | 
					  final ScaleStateCycle? scaleStateCycle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.onTapUp]
 | 
				
			||||||
 | 
					  final PhotoViewImageTapUpCallback? onTapUp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.onDragUp]
 | 
				
			||||||
 | 
					  final PhotoViewImageDragStartCallback? onDragStart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.onDragDown]
 | 
				
			||||||
 | 
					  final PhotoViewImageDragEndCallback? onDragEnd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.onDraUpdate]
 | 
				
			||||||
 | 
					  final PhotoViewImageDragUpdateCallback? onDragUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.onTapDown]
 | 
				
			||||||
 | 
					  final PhotoViewImageTapDownCallback? onTapDown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.onScaleEnd]
 | 
				
			||||||
 | 
					  final PhotoViewImageScaleEndCallback? onScaleEnd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.gestureDetectorBehavior]
 | 
				
			||||||
 | 
					  final HitTestBehavior? gestureDetectorBehavior;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.tightMode]
 | 
				
			||||||
 | 
					  final bool? tightMode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.disableGestures]
 | 
				
			||||||
 | 
					  final bool? disableGestures;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Quality levels for image filters.
 | 
				
			||||||
 | 
					  final FilterQuality? filterQuality;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [PhotoView.errorBuilder]
 | 
				
			||||||
 | 
					  final ImageErrorWidgetBuilder? errorBuilder;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,291 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/utils/ignorable_change_notifier.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The interface in which controllers will be implemented.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It concerns storing the state ([PhotoViewControllerValue]) and streaming its updates.
 | 
				
			||||||
 | 
					/// [PhotoViewImageWrapper] will respond to user gestures setting thew fields in the instance of a controller.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Any instance of a controller must be disposed after unmount. So if you instantiate a [PhotoViewController] or your custom implementation, do not forget to dispose it when not using it anymore.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The controller exposes value fields like [scale] or [rotationFocus]. Usually those fields will be only getters and setters serving as hooks to the internal [PhotoViewControllerValue].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The default implementation used by [PhotoView] is [PhotoViewController].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This was created to allow customization (you can create your own controller class)
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Previously it controlled `scaleState` as well, but duw to some [concerns](https://github.com/renancaraujo/photo_view/issues/127)
 | 
				
			||||||
 | 
					/// [ScaleStateListener is responsible for tat value now
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// As it is a controller, whoever instantiates it, should [dispose] it afterwards.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
 | 
				
			||||||
 | 
					  /// The output for state/value updates. Usually a broadcast [Stream]
 | 
				
			||||||
 | 
					  Stream<T> get outputStateStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The state value before the last change or the initial state if the state has not been changed.
 | 
				
			||||||
 | 
					  late T prevValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The actual state value
 | 
				
			||||||
 | 
					  late T value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Resets the state to the initial value;
 | 
				
			||||||
 | 
					  void reset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Closes streams and removes eventual listeners.
 | 
				
			||||||
 | 
					  void dispose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Add a listener that will ignore updates made internally
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Since it is made for internal use, it is not performatic to use more than one
 | 
				
			||||||
 | 
					  /// listener. Prefer [outputStateStream]
 | 
				
			||||||
 | 
					  void addIgnorableListener(VoidCallback callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Remove a listener that will ignore updates made internally
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Since it is made for internal use, it is not performatic to use more than one
 | 
				
			||||||
 | 
					  /// listener. Prefer [outputStateStream]
 | 
				
			||||||
 | 
					  void removeIgnorableListener(VoidCallback callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The position of the image in the screen given its offset after pan gestures.
 | 
				
			||||||
 | 
					  late Offset position;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The scale factor to transform the child (image or a customChild).
 | 
				
			||||||
 | 
					  late double? scale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Nevermind this method :D, look away
 | 
				
			||||||
 | 
					  void setScaleInvisibly(double? scale);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The rotation factor to transform the child (image or a customChild).
 | 
				
			||||||
 | 
					  late double rotation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The center of the rotation transformation. It is a coordinate referring to the absolute dimensions of the image.
 | 
				
			||||||
 | 
					  Offset? rotationFocusPoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Update multiple fields of the state with only one update streamed.
 | 
				
			||||||
 | 
					  void updateMultiple({
 | 
				
			||||||
 | 
					    Offset? position,
 | 
				
			||||||
 | 
					    double? scale,
 | 
				
			||||||
 | 
					    double? rotation,
 | 
				
			||||||
 | 
					    Offset? rotationFocusPoint,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The state value stored and streamed by [PhotoViewController].
 | 
				
			||||||
 | 
					@immutable
 | 
				
			||||||
 | 
					class PhotoViewControllerValue {
 | 
				
			||||||
 | 
					  const PhotoViewControllerValue({
 | 
				
			||||||
 | 
					    required this.position,
 | 
				
			||||||
 | 
					    required this.scale,
 | 
				
			||||||
 | 
					    required this.rotation,
 | 
				
			||||||
 | 
					    required this.rotationFocusPoint,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Offset position;
 | 
				
			||||||
 | 
					  final double? scale;
 | 
				
			||||||
 | 
					  final double rotation;
 | 
				
			||||||
 | 
					  final Offset? rotationFocusPoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) =>
 | 
				
			||||||
 | 
					      identical(this, other) ||
 | 
				
			||||||
 | 
					      other is PhotoViewControllerValue &&
 | 
				
			||||||
 | 
					          runtimeType == other.runtimeType &&
 | 
				
			||||||
 | 
					          position == other.position &&
 | 
				
			||||||
 | 
					          scale == other.scale &&
 | 
				
			||||||
 | 
					          rotation == other.rotation &&
 | 
				
			||||||
 | 
					          rotationFocusPoint == other.rotationFocusPoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					      position.hashCode ^
 | 
				
			||||||
 | 
					      scale.hashCode ^
 | 
				
			||||||
 | 
					      rotation.hashCode ^
 | 
				
			||||||
 | 
					      rotationFocusPoint.hashCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'PhotoViewControllerValue{position: $position, scale: $scale, rotation: $rotation, rotationFocusPoint: $rotationFocusPoint}';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The default implementation of [PhotoViewControllerBase].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Containing a [ValueNotifier] it stores the state in the [value] field and streams
 | 
				
			||||||
 | 
					/// updates via [outputStateStream].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// For details of fields and methods, check [PhotoViewControllerBase].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					class PhotoViewController
 | 
				
			||||||
 | 
					    implements PhotoViewControllerBase<PhotoViewControllerValue> {
 | 
				
			||||||
 | 
					  PhotoViewController({
 | 
				
			||||||
 | 
					    Offset initialPosition = Offset.zero,
 | 
				
			||||||
 | 
					    double initialRotation = 0.0,
 | 
				
			||||||
 | 
					    double? initialScale,
 | 
				
			||||||
 | 
					  })  : _valueNotifier = IgnorableValueNotifier(
 | 
				
			||||||
 | 
					          PhotoViewControllerValue(
 | 
				
			||||||
 | 
					            position: initialPosition,
 | 
				
			||||||
 | 
					            rotation: initialRotation,
 | 
				
			||||||
 | 
					            scale: initialScale,
 | 
				
			||||||
 | 
					            rotationFocusPoint: null,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        super() {
 | 
				
			||||||
 | 
					    initial = value;
 | 
				
			||||||
 | 
					    prevValue = initial;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _valueNotifier.addListener(_changeListener);
 | 
				
			||||||
 | 
					    _outputCtrl = StreamController<PhotoViewControllerValue>.broadcast();
 | 
				
			||||||
 | 
					    _outputCtrl.sink.add(initial);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final IgnorableValueNotifier<PhotoViewControllerValue> _valueNotifier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late PhotoViewControllerValue initial;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late StreamController<PhotoViewControllerValue> _outputCtrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  late PhotoViewControllerValue prevValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void reset() {
 | 
				
			||||||
 | 
					    value = initial;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _changeListener() {
 | 
				
			||||||
 | 
					    _outputCtrl.sink.add(value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void addIgnorableListener(VoidCallback callback) {
 | 
				
			||||||
 | 
					    _valueNotifier.addIgnorableListener(callback);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void removeIgnorableListener(VoidCallback callback) {
 | 
				
			||||||
 | 
					    _valueNotifier.removeIgnorableListener(callback);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _outputCtrl.close();
 | 
				
			||||||
 | 
					    _valueNotifier.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  set position(Offset position) {
 | 
				
			||||||
 | 
					    if (value.position == position) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    prevValue = value;
 | 
				
			||||||
 | 
					    value = PhotoViewControllerValue(
 | 
				
			||||||
 | 
					      position: position,
 | 
				
			||||||
 | 
					      scale: scale,
 | 
				
			||||||
 | 
					      rotation: rotation,
 | 
				
			||||||
 | 
					      rotationFocusPoint: rotationFocusPoint,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Offset get position => value.position;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  set scale(double? scale) {
 | 
				
			||||||
 | 
					    if (value.scale == scale) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    prevValue = value;
 | 
				
			||||||
 | 
					    value = PhotoViewControllerValue(
 | 
				
			||||||
 | 
					      position: position,
 | 
				
			||||||
 | 
					      scale: scale,
 | 
				
			||||||
 | 
					      rotation: rotation,
 | 
				
			||||||
 | 
					      rotationFocusPoint: rotationFocusPoint,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  double? get scale => value.scale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void setScaleInvisibly(double? scale) {
 | 
				
			||||||
 | 
					    if (value.scale == scale) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    prevValue = value;
 | 
				
			||||||
 | 
					    _valueNotifier.updateIgnoring(
 | 
				
			||||||
 | 
					      PhotoViewControllerValue(
 | 
				
			||||||
 | 
					        position: position,
 | 
				
			||||||
 | 
					        scale: scale,
 | 
				
			||||||
 | 
					        rotation: rotation,
 | 
				
			||||||
 | 
					        rotationFocusPoint: rotationFocusPoint,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  set rotation(double rotation) {
 | 
				
			||||||
 | 
					    if (value.rotation == rotation) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    prevValue = value;
 | 
				
			||||||
 | 
					    value = PhotoViewControllerValue(
 | 
				
			||||||
 | 
					      position: position,
 | 
				
			||||||
 | 
					      scale: scale,
 | 
				
			||||||
 | 
					      rotation: rotation,
 | 
				
			||||||
 | 
					      rotationFocusPoint: rotationFocusPoint,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  double get rotation => value.rotation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  set rotationFocusPoint(Offset? rotationFocusPoint) {
 | 
				
			||||||
 | 
					    if (value.rotationFocusPoint == rotationFocusPoint) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    prevValue = value;
 | 
				
			||||||
 | 
					    value = PhotoViewControllerValue(
 | 
				
			||||||
 | 
					      position: position,
 | 
				
			||||||
 | 
					      scale: scale,
 | 
				
			||||||
 | 
					      rotation: rotation,
 | 
				
			||||||
 | 
					      rotationFocusPoint: rotationFocusPoint,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Offset? get rotationFocusPoint => value.rotationFocusPoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void updateMultiple({
 | 
				
			||||||
 | 
					    Offset? position,
 | 
				
			||||||
 | 
					    double? scale,
 | 
				
			||||||
 | 
					    double? rotation,
 | 
				
			||||||
 | 
					    Offset? rotationFocusPoint,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    prevValue = value;
 | 
				
			||||||
 | 
					    value = PhotoViewControllerValue(
 | 
				
			||||||
 | 
					      position: position ?? value.position,
 | 
				
			||||||
 | 
					      scale: scale ?? value.scale,
 | 
				
			||||||
 | 
					      rotation: rotation ?? value.rotation,
 | 
				
			||||||
 | 
					      rotationFocusPoint: rotationFocusPoint ?? value.rotationFocusPoint,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  PhotoViewControllerValue get value => _valueNotifier.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  set value(PhotoViewControllerValue newValue) {
 | 
				
			||||||
 | 
					    if (_valueNotifier.value == newValue) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _valueNotifier.value = newValue;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,214 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/photo_view.dart'
 | 
				
			||||||
 | 
					    show
 | 
				
			||||||
 | 
					        PhotoViewControllerBase,
 | 
				
			||||||
 | 
					        PhotoViewScaleState,
 | 
				
			||||||
 | 
					        PhotoViewScaleStateController,
 | 
				
			||||||
 | 
					        ScaleStateCycle;
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/core/photo_view_core.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_utils.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A  class to hold internal layout logic to sync both controller states
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It reacts to layout changes (eg: enter landscape or widget resize) and syncs the two controllers.
 | 
				
			||||||
 | 
					mixin PhotoViewControllerDelegate on State<PhotoViewCore> {
 | 
				
			||||||
 | 
					  PhotoViewControllerBase get controller => widget.controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PhotoViewScaleStateController get scaleStateController =>
 | 
				
			||||||
 | 
					      widget.scaleStateController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ScaleBoundaries get scaleBoundaries => widget.scaleBoundaries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ScaleStateCycle get scaleStateCycle => widget.scaleStateCycle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Alignment get basePosition => widget.basePosition;
 | 
				
			||||||
 | 
					  Function(double prevScale, double nextScale)? _animateScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mark if scale need recalculation, useful for scale boundaries changes.
 | 
				
			||||||
 | 
					  bool markNeedsScaleRecalc = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void initDelegate() {
 | 
				
			||||||
 | 
					    controller.addIgnorableListener(_blindScaleListener);
 | 
				
			||||||
 | 
					    scaleStateController.addIgnorableListener(_blindScaleStateListener);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _blindScaleStateListener() {
 | 
				
			||||||
 | 
					    if (!scaleStateController.hasChanged) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_animateScale == null || scaleStateController.isZooming) {
 | 
				
			||||||
 | 
					      controller.setScaleInvisibly(scale);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final double prevScale = controller.scale ??
 | 
				
			||||||
 | 
					        getScaleForScaleState(
 | 
				
			||||||
 | 
					          scaleStateController.prevScaleState,
 | 
				
			||||||
 | 
					          scaleBoundaries,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double nextScale = getScaleForScaleState(
 | 
				
			||||||
 | 
					      scaleStateController.scaleState,
 | 
				
			||||||
 | 
					      scaleBoundaries,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _animateScale!(prevScale, nextScale);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void addAnimateOnScaleStateUpdate(
 | 
				
			||||||
 | 
					    void Function(double prevScale, double nextScale) animateScale,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    _animateScale = animateScale;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _blindScaleListener() {
 | 
				
			||||||
 | 
					    if (!widget.enablePanAlways) {
 | 
				
			||||||
 | 
					      controller.position = clampPosition();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (controller.scale == controller.prevValue.scale) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final PhotoViewScaleState newScaleState =
 | 
				
			||||||
 | 
					        (scale > scaleBoundaries.initialScale)
 | 
				
			||||||
 | 
					            ? PhotoViewScaleState.zoomedIn
 | 
				
			||||||
 | 
					            : PhotoViewScaleState.zoomedOut;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    scaleStateController.setInvisibly(newScaleState);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Offset get position => controller.position;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double get scale {
 | 
				
			||||||
 | 
					    // for figuring out initial scale
 | 
				
			||||||
 | 
					    final needsRecalc = markNeedsScaleRecalc &&
 | 
				
			||||||
 | 
					        !scaleStateController.scaleState.isScaleStateZooming;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final scaleExistsOnController = controller.scale != null;
 | 
				
			||||||
 | 
					    if (needsRecalc || !scaleExistsOnController) {
 | 
				
			||||||
 | 
					      final newScale = getScaleForScaleState(
 | 
				
			||||||
 | 
					        scaleStateController.scaleState,
 | 
				
			||||||
 | 
					        scaleBoundaries,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      markNeedsScaleRecalc = false;
 | 
				
			||||||
 | 
					      scale = newScale;
 | 
				
			||||||
 | 
					      return newScale;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return controller.scale!;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set scale(double scale) => controller.setScaleInvisibly(scale);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void updateMultiple({
 | 
				
			||||||
 | 
					    Offset? position,
 | 
				
			||||||
 | 
					    double? scale,
 | 
				
			||||||
 | 
					    double? rotation,
 | 
				
			||||||
 | 
					    Offset? rotationFocusPoint,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    controller.updateMultiple(
 | 
				
			||||||
 | 
					      position: position,
 | 
				
			||||||
 | 
					      scale: scale,
 | 
				
			||||||
 | 
					      rotation: rotation,
 | 
				
			||||||
 | 
					      rotationFocusPoint: rotationFocusPoint,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void updateScaleStateFromNewScale(double newScale) {
 | 
				
			||||||
 | 
					    PhotoViewScaleState newScaleState = PhotoViewScaleState.initial;
 | 
				
			||||||
 | 
					    if (scale != scaleBoundaries.initialScale) {
 | 
				
			||||||
 | 
					      newScaleState = (newScale > scaleBoundaries.initialScale)
 | 
				
			||||||
 | 
					          ? PhotoViewScaleState.zoomedIn
 | 
				
			||||||
 | 
					          : PhotoViewScaleState.zoomedOut;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    scaleStateController.setInvisibly(newScaleState);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void nextScaleState() {
 | 
				
			||||||
 | 
					    final PhotoViewScaleState scaleState = scaleStateController.scaleState;
 | 
				
			||||||
 | 
					    if (scaleState == PhotoViewScaleState.zoomedIn ||
 | 
				
			||||||
 | 
					        scaleState == PhotoViewScaleState.zoomedOut) {
 | 
				
			||||||
 | 
					      scaleStateController.scaleState = scaleStateCycle(scaleState);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final double originalScale = getScaleForScaleState(
 | 
				
			||||||
 | 
					      scaleState,
 | 
				
			||||||
 | 
					      scaleBoundaries,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    double prevScale = originalScale;
 | 
				
			||||||
 | 
					    PhotoViewScaleState prevScaleState = scaleState;
 | 
				
			||||||
 | 
					    double nextScale = originalScale;
 | 
				
			||||||
 | 
					    PhotoViewScaleState nextScaleState = scaleState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    do {
 | 
				
			||||||
 | 
					      prevScale = nextScale;
 | 
				
			||||||
 | 
					      prevScaleState = nextScaleState;
 | 
				
			||||||
 | 
					      nextScaleState = scaleStateCycle(prevScaleState);
 | 
				
			||||||
 | 
					      nextScale = getScaleForScaleState(nextScaleState, scaleBoundaries);
 | 
				
			||||||
 | 
					    } while (prevScale == nextScale && scaleState != nextScaleState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (originalScale == nextScale) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    scaleStateController.scaleState = nextScaleState;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CornersRange cornersX({double? scale}) {
 | 
				
			||||||
 | 
					    final double s = scale ?? this.scale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double computedWidth = scaleBoundaries.childSize.width * s;
 | 
				
			||||||
 | 
					    final double screenWidth = scaleBoundaries.outerSize.width;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double positionX = basePosition.x;
 | 
				
			||||||
 | 
					    final double widthDiff = computedWidth - screenWidth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double minX = ((positionX - 1).abs() / 2) * widthDiff * -1;
 | 
				
			||||||
 | 
					    final double maxX = ((positionX + 1).abs() / 2) * widthDiff;
 | 
				
			||||||
 | 
					    return CornersRange(minX, maxX);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CornersRange cornersY({double? scale}) {
 | 
				
			||||||
 | 
					    final double s = scale ?? this.scale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double computedHeight = scaleBoundaries.childSize.height * s;
 | 
				
			||||||
 | 
					    final double screenHeight = scaleBoundaries.outerSize.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double positionY = basePosition.y;
 | 
				
			||||||
 | 
					    final double heightDiff = computedHeight - screenHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double minY = ((positionY - 1).abs() / 2) * heightDiff * -1;
 | 
				
			||||||
 | 
					    final double maxY = ((positionY + 1).abs() / 2) * heightDiff;
 | 
				
			||||||
 | 
					    return CornersRange(minY, maxY);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Offset clampPosition({Offset? position, double? scale}) {
 | 
				
			||||||
 | 
					    final double s = scale ?? this.scale;
 | 
				
			||||||
 | 
					    final Offset p = position ?? this.position;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double computedWidth = scaleBoundaries.childSize.width * s;
 | 
				
			||||||
 | 
					    final double computedHeight = scaleBoundaries.childSize.height * s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double screenWidth = scaleBoundaries.outerSize.width;
 | 
				
			||||||
 | 
					    final double screenHeight = scaleBoundaries.outerSize.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    double finalX = 0.0;
 | 
				
			||||||
 | 
					    if (screenWidth < computedWidth) {
 | 
				
			||||||
 | 
					      final cornersX = this.cornersX(scale: s);
 | 
				
			||||||
 | 
					      finalX = p.dx.clamp(cornersX.min, cornersX.max);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    double finalY = 0.0;
 | 
				
			||||||
 | 
					    if (screenHeight < computedHeight) {
 | 
				
			||||||
 | 
					      final cornersY = this.cornersY(scale: s);
 | 
				
			||||||
 | 
					      finalY = p.dy.clamp(cornersY.min, cornersY.max);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Offset(finalX, finalY);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _animateScale = null;
 | 
				
			||||||
 | 
					    controller.removeIgnorableListener(_blindScaleListener);
 | 
				
			||||||
 | 
					    scaleStateController.removeIgnorableListener(_blindScaleStateListener);
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/widgets.dart' show VoidCallback;
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/utils/ignorable_change_notifier.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef ScaleStateListener = void Function(double prevScale, double nextScale);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A controller responsible only by [scaleState].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Scale state is a common value with represents the step in which the [PhotoView.scaleStateCycle] is.
 | 
				
			||||||
 | 
					/// This cycle is triggered by the "doubleTap" gesture.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Any change in its [scaleState] should animate the scale of image/content.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// As it is a controller, whoever instantiates it, should [dispose] it afterwards.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The updates should be done via [scaleState] setter and the updated listened via [outputScaleStateStream]
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					class PhotoViewScaleStateController {
 | 
				
			||||||
 | 
					  late final IgnorableValueNotifier<PhotoViewScaleState> _scaleStateNotifier =
 | 
				
			||||||
 | 
					      IgnorableValueNotifier(PhotoViewScaleState.initial)
 | 
				
			||||||
 | 
					        ..addListener(_scaleStateChangeListener);
 | 
				
			||||||
 | 
					  final StreamController<PhotoViewScaleState> _outputScaleStateCtrl =
 | 
				
			||||||
 | 
					      StreamController<PhotoViewScaleState>.broadcast()
 | 
				
			||||||
 | 
					        ..sink.add(PhotoViewScaleState.initial);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The output for state/value updates
 | 
				
			||||||
 | 
					  Stream<PhotoViewScaleState> get outputScaleStateStream =>
 | 
				
			||||||
 | 
					      _outputScaleStateCtrl.stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The state value before the last change or the initial state if the state has not been changed.
 | 
				
			||||||
 | 
					  PhotoViewScaleState prevScaleState = PhotoViewScaleState.initial;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The actual state value
 | 
				
			||||||
 | 
					  PhotoViewScaleState get scaleState => _scaleStateNotifier.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Updates scaleState and notify all listeners (and the stream)
 | 
				
			||||||
 | 
					  set scaleState(PhotoViewScaleState newValue) {
 | 
				
			||||||
 | 
					    if (_scaleStateNotifier.value == newValue) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prevScaleState = _scaleStateNotifier.value;
 | 
				
			||||||
 | 
					    _scaleStateNotifier.value = newValue;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Checks if its actual value is different than previousValue
 | 
				
			||||||
 | 
					  bool get hasChanged => prevScaleState != scaleState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Check if is `zoomedIn` & `zoomedOut`
 | 
				
			||||||
 | 
					  bool get isZooming =>
 | 
				
			||||||
 | 
					      scaleState == PhotoViewScaleState.zoomedIn ||
 | 
				
			||||||
 | 
					      scaleState == PhotoViewScaleState.zoomedOut;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Resets the state to the initial value;
 | 
				
			||||||
 | 
					  void reset() {
 | 
				
			||||||
 | 
					    prevScaleState = scaleState;
 | 
				
			||||||
 | 
					    scaleState = PhotoViewScaleState.initial;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Closes streams and removes eventual listeners
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _outputScaleStateCtrl.close();
 | 
				
			||||||
 | 
					    _scaleStateNotifier.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Nevermind this method :D, look away
 | 
				
			||||||
 | 
					  /// Seriously: It is used to change scale state without trigging updates on the []
 | 
				
			||||||
 | 
					  void setInvisibly(PhotoViewScaleState newValue) {
 | 
				
			||||||
 | 
					    if (_scaleStateNotifier.value == newValue) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    prevScaleState = _scaleStateNotifier.value;
 | 
				
			||||||
 | 
					    _scaleStateNotifier.updateIgnoring(newValue);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _scaleStateChangeListener() {
 | 
				
			||||||
 | 
					    _outputScaleStateCtrl.sink.add(scaleState);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Add a listener that will ignore updates made internally
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Since it is made for internal use, it is not performatic to use more than one
 | 
				
			||||||
 | 
					  /// listener. Prefer [outputScaleStateStream]
 | 
				
			||||||
 | 
					  void addIgnorableListener(VoidCallback callback) {
 | 
				
			||||||
 | 
					    _scaleStateNotifier.addIgnorableListener(callback);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Remove a listener that will ignore updates made internally
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Since it is made for internal use, it is not performatic to use more than one
 | 
				
			||||||
 | 
					  /// listener. Prefer [outputScaleStateStream]
 | 
				
			||||||
 | 
					  void removeIgnorableListener(VoidCallback callback) {
 | 
				
			||||||
 | 
					    _scaleStateNotifier.removeIgnorableListener(callback);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										461
									
								
								mobile/lib/shared/ui/photo_view/src/core/photo_view_core.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										461
									
								
								mobile/lib/shared/ui/photo_view/src/core/photo_view_core.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,461 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/photo_view.dart'
 | 
				
			||||||
 | 
					    show
 | 
				
			||||||
 | 
					        PhotoViewScaleState,
 | 
				
			||||||
 | 
					        PhotoViewHeroAttributes,
 | 
				
			||||||
 | 
					        PhotoViewImageTapDownCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageTapUpCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageScaleEndCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageDragEndCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageDragStartCallback,
 | 
				
			||||||
 | 
					        PhotoViewImageDragUpdateCallback,
 | 
				
			||||||
 | 
					        ScaleStateCycle;
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_controller.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_controller_delegate.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_scalestate_controller.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/core/photo_view_gesture_detector.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/core/photo_view_hit_corners.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_utils.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _defaultDecoration = BoxDecoration(
 | 
				
			||||||
 | 
					  color: Color.fromRGBO(0, 0, 0, 1.0),
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Internal widget in which controls all animations lifecycle, core responses
 | 
				
			||||||
 | 
					/// to user gestures, updates to  the controller state and mounts the entire PhotoView Layout
 | 
				
			||||||
 | 
					class PhotoViewCore extends StatefulWidget {
 | 
				
			||||||
 | 
					  const PhotoViewCore({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.imageProvider,
 | 
				
			||||||
 | 
					    required this.backgroundDecoration,
 | 
				
			||||||
 | 
					    required this.gaplessPlayback,
 | 
				
			||||||
 | 
					    required this.heroAttributes,
 | 
				
			||||||
 | 
					    required this.enableRotation,
 | 
				
			||||||
 | 
					    required this.onTapUp,
 | 
				
			||||||
 | 
					    required this.onTapDown,
 | 
				
			||||||
 | 
					    required this.onDragStart,
 | 
				
			||||||
 | 
					    required this.onDragEnd,
 | 
				
			||||||
 | 
					    required this.onDragUpdate,
 | 
				
			||||||
 | 
					    required this.onScaleEnd,
 | 
				
			||||||
 | 
					    required this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    required this.controller,
 | 
				
			||||||
 | 
					    required this.scaleBoundaries,
 | 
				
			||||||
 | 
					    required this.scaleStateCycle,
 | 
				
			||||||
 | 
					    required this.scaleStateController,
 | 
				
			||||||
 | 
					    required this.basePosition,
 | 
				
			||||||
 | 
					    required this.tightMode,
 | 
				
			||||||
 | 
					    required this.filterQuality,
 | 
				
			||||||
 | 
					    required this.disableGestures,
 | 
				
			||||||
 | 
					    required this.enablePanAlways,
 | 
				
			||||||
 | 
					  })  : customChild = null,
 | 
				
			||||||
 | 
					        super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const PhotoViewCore.customChild({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.customChild,
 | 
				
			||||||
 | 
					    required this.backgroundDecoration,
 | 
				
			||||||
 | 
					    this.heroAttributes,
 | 
				
			||||||
 | 
					    required this.enableRotation,
 | 
				
			||||||
 | 
					    this.onTapUp,
 | 
				
			||||||
 | 
					    this.onTapDown,
 | 
				
			||||||
 | 
					    this.onDragStart,
 | 
				
			||||||
 | 
					    this.onDragEnd,
 | 
				
			||||||
 | 
					    this.onDragUpdate,
 | 
				
			||||||
 | 
					    this.onScaleEnd,
 | 
				
			||||||
 | 
					    this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    required this.controller,
 | 
				
			||||||
 | 
					    required this.scaleBoundaries,
 | 
				
			||||||
 | 
					    required this.scaleStateCycle,
 | 
				
			||||||
 | 
					    required this.scaleStateController,
 | 
				
			||||||
 | 
					    required this.basePosition,
 | 
				
			||||||
 | 
					    required this.tightMode,
 | 
				
			||||||
 | 
					    required this.filterQuality,
 | 
				
			||||||
 | 
					    required this.disableGestures,
 | 
				
			||||||
 | 
					    required this.enablePanAlways,
 | 
				
			||||||
 | 
					  })  : imageProvider = null,
 | 
				
			||||||
 | 
					        gaplessPlayback = false,
 | 
				
			||||||
 | 
					        super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Decoration? backgroundDecoration;
 | 
				
			||||||
 | 
					  final ImageProvider? imageProvider;
 | 
				
			||||||
 | 
					  final bool? gaplessPlayback;
 | 
				
			||||||
 | 
					  final PhotoViewHeroAttributes? heroAttributes;
 | 
				
			||||||
 | 
					  final bool enableRotation;
 | 
				
			||||||
 | 
					  final Widget? customChild;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final PhotoViewControllerBase controller;
 | 
				
			||||||
 | 
					  final PhotoViewScaleStateController scaleStateController;
 | 
				
			||||||
 | 
					  final ScaleBoundaries scaleBoundaries;
 | 
				
			||||||
 | 
					  final ScaleStateCycle scaleStateCycle;
 | 
				
			||||||
 | 
					  final Alignment basePosition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final PhotoViewImageTapUpCallback? onTapUp;
 | 
				
			||||||
 | 
					  final PhotoViewImageTapDownCallback? onTapDown;
 | 
				
			||||||
 | 
					  final PhotoViewImageScaleEndCallback? onScaleEnd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final PhotoViewImageDragStartCallback? onDragStart;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragEndCallback? onDragEnd;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragUpdateCallback? onDragUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final HitTestBehavior? gestureDetectorBehavior;
 | 
				
			||||||
 | 
					  final bool tightMode;
 | 
				
			||||||
 | 
					  final bool disableGestures;
 | 
				
			||||||
 | 
					  final bool enablePanAlways;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final FilterQuality filterQuality;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<StatefulWidget> createState() {
 | 
				
			||||||
 | 
					    return PhotoViewCoreState();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get hasCustomChild => customChild != null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PhotoViewCoreState extends State<PhotoViewCore>
 | 
				
			||||||
 | 
					    with
 | 
				
			||||||
 | 
					        TickerProviderStateMixin,
 | 
				
			||||||
 | 
					        PhotoViewControllerDelegate,
 | 
				
			||||||
 | 
					        HitCornersDetector {
 | 
				
			||||||
 | 
					  Offset? _normalizedPosition;
 | 
				
			||||||
 | 
					  double? _scaleBefore;
 | 
				
			||||||
 | 
					  double? _rotationBefore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late final AnimationController _scaleAnimationController;
 | 
				
			||||||
 | 
					  Animation<double>? _scaleAnimation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late final AnimationController _positionAnimationController;
 | 
				
			||||||
 | 
					  Animation<Offset>? _positionAnimation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late final AnimationController _rotationAnimationController =
 | 
				
			||||||
 | 
					      AnimationController(vsync: this)..addListener(handleRotationAnimation);
 | 
				
			||||||
 | 
					  Animation<double>? _rotationAnimation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PhotoViewHeroAttributes? get heroAttributes => widget.heroAttributes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  late ScaleBoundaries cachedScaleBoundaries = widget.scaleBoundaries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void handleScaleAnimation() {
 | 
				
			||||||
 | 
					    scale = _scaleAnimation!.value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void handlePositionAnimate() {
 | 
				
			||||||
 | 
					    controller.position = _positionAnimation!.value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void handleRotationAnimation() {
 | 
				
			||||||
 | 
					    controller.rotation = _rotationAnimation!.value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void onScaleStart(ScaleStartDetails details) {
 | 
				
			||||||
 | 
					    _rotationBefore = controller.rotation;
 | 
				
			||||||
 | 
					    _scaleBefore = scale;
 | 
				
			||||||
 | 
					    _normalizedPosition = details.focalPoint - controller.position;
 | 
				
			||||||
 | 
					    _scaleAnimationController.stop();
 | 
				
			||||||
 | 
					    _positionAnimationController.stop();
 | 
				
			||||||
 | 
					    _rotationAnimationController.stop();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void onScaleUpdate(ScaleUpdateDetails details) {
 | 
				
			||||||
 | 
					    final double newScale = _scaleBefore! * details.scale;
 | 
				
			||||||
 | 
					    final Offset delta = details.focalPoint - _normalizedPosition!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    updateScaleStateFromNewScale(newScale);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    updateMultiple(
 | 
				
			||||||
 | 
					      scale: newScale,
 | 
				
			||||||
 | 
					      position: widget.enablePanAlways
 | 
				
			||||||
 | 
					          ? delta
 | 
				
			||||||
 | 
					          : clampPosition(position: delta * details.scale),
 | 
				
			||||||
 | 
					      rotation:
 | 
				
			||||||
 | 
					          widget.enableRotation ? _rotationBefore! + details.rotation : null,
 | 
				
			||||||
 | 
					      rotationFocusPoint: widget.enableRotation ? details.focalPoint : null,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void onScaleEnd(ScaleEndDetails details) {
 | 
				
			||||||
 | 
					    final double s = scale;
 | 
				
			||||||
 | 
					    final Offset p = controller.position;
 | 
				
			||||||
 | 
					    final double maxScale = scaleBoundaries.maxScale;
 | 
				
			||||||
 | 
					    final double minScale = scaleBoundaries.minScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    widget.onScaleEnd?.call(context, details, controller.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //animate back to maxScale if gesture exceeded the maxScale specified
 | 
				
			||||||
 | 
					    if (s > maxScale) {
 | 
				
			||||||
 | 
					      final double scaleComebackRatio = maxScale / s;
 | 
				
			||||||
 | 
					      animateScale(s, maxScale);
 | 
				
			||||||
 | 
					      final Offset clampedPosition = clampPosition(
 | 
				
			||||||
 | 
					        position: p * scaleComebackRatio,
 | 
				
			||||||
 | 
					        scale: maxScale,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      animatePosition(p, clampedPosition);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //animate back to minScale if gesture fell smaller than the minScale specified
 | 
				
			||||||
 | 
					    if (s < minScale) {
 | 
				
			||||||
 | 
					      final double scaleComebackRatio = minScale / s;
 | 
				
			||||||
 | 
					      animateScale(s, minScale);
 | 
				
			||||||
 | 
					      animatePosition(
 | 
				
			||||||
 | 
					        p,
 | 
				
			||||||
 | 
					        clampPosition(
 | 
				
			||||||
 | 
					          position: p * scaleComebackRatio,
 | 
				
			||||||
 | 
					          scale: minScale,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // get magnitude from gesture velocity
 | 
				
			||||||
 | 
					    final double magnitude = details.velocity.pixelsPerSecond.distance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // animate velocity only if there is no scale change and a significant magnitude
 | 
				
			||||||
 | 
					    if (_scaleBefore! / s == 1.0 && magnitude >= 400.0) {
 | 
				
			||||||
 | 
					      final Offset direction = details.velocity.pixelsPerSecond / magnitude;
 | 
				
			||||||
 | 
					      animatePosition(
 | 
				
			||||||
 | 
					        p,
 | 
				
			||||||
 | 
					        clampPosition(position: p + direction * 100.0),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void onDoubleTap() {
 | 
				
			||||||
 | 
					    nextScaleState();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void animateScale(double from, double to) {
 | 
				
			||||||
 | 
					    _scaleAnimation = Tween<double>(
 | 
				
			||||||
 | 
					      begin: from,
 | 
				
			||||||
 | 
					      end: to,
 | 
				
			||||||
 | 
					    ).animate(_scaleAnimationController);
 | 
				
			||||||
 | 
					    _scaleAnimationController
 | 
				
			||||||
 | 
					      ..value = 0.0
 | 
				
			||||||
 | 
					      ..fling(velocity: 0.4);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void animatePosition(Offset from, Offset to) {
 | 
				
			||||||
 | 
					    _positionAnimation = Tween<Offset>(begin: from, end: to)
 | 
				
			||||||
 | 
					        .animate(_positionAnimationController);
 | 
				
			||||||
 | 
					    _positionAnimationController
 | 
				
			||||||
 | 
					      ..value = 0.0
 | 
				
			||||||
 | 
					      ..fling(velocity: 0.4);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void animateRotation(double from, double to) {
 | 
				
			||||||
 | 
					    _rotationAnimation = Tween<double>(begin: from, end: to)
 | 
				
			||||||
 | 
					        .animate(_rotationAnimationController);
 | 
				
			||||||
 | 
					    _rotationAnimationController
 | 
				
			||||||
 | 
					      ..value = 0.0
 | 
				
			||||||
 | 
					      ..fling(velocity: 0.4);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void onAnimationStatus(AnimationStatus status) {
 | 
				
			||||||
 | 
					    if (status == AnimationStatus.completed) {
 | 
				
			||||||
 | 
					      onAnimationStatusCompleted();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Check if scale is equal to initial after scale animation update
 | 
				
			||||||
 | 
					  void onAnimationStatusCompleted() {
 | 
				
			||||||
 | 
					    if (scaleStateController.scaleState != PhotoViewScaleState.initial &&
 | 
				
			||||||
 | 
					        scale == scaleBoundaries.initialScale) {
 | 
				
			||||||
 | 
					      scaleStateController.setInvisibly(PhotoViewScaleState.initial);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    initDelegate();
 | 
				
			||||||
 | 
					    addAnimateOnScaleStateUpdate(animateOnScaleStateUpdate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cachedScaleBoundaries = widget.scaleBoundaries;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _scaleAnimationController = AnimationController(vsync: this)
 | 
				
			||||||
 | 
					      ..addListener(handleScaleAnimation)
 | 
				
			||||||
 | 
					      ..addStatusListener(onAnimationStatus);
 | 
				
			||||||
 | 
					    _positionAnimationController = AnimationController(vsync: this)
 | 
				
			||||||
 | 
					      ..addListener(handlePositionAnimate);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void animateOnScaleStateUpdate(double prevScale, double nextScale) {
 | 
				
			||||||
 | 
					    animateScale(prevScale, nextScale);
 | 
				
			||||||
 | 
					    animatePosition(controller.position, Offset.zero);
 | 
				
			||||||
 | 
					    animateRotation(controller.rotation, 0.0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _scaleAnimationController.removeStatusListener(onAnimationStatus);
 | 
				
			||||||
 | 
					    _scaleAnimationController.dispose();
 | 
				
			||||||
 | 
					    _positionAnimationController.dispose();
 | 
				
			||||||
 | 
					    _rotationAnimationController.dispose();
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void onTapUp(TapUpDetails details) {
 | 
				
			||||||
 | 
					    widget.onTapUp?.call(context, details, controller.value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void onTapDown(TapDownDetails details) {
 | 
				
			||||||
 | 
					    widget.onTapDown?.call(context, details, controller.value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    // Check if we need a recalc on the scale
 | 
				
			||||||
 | 
					    if (widget.scaleBoundaries != cachedScaleBoundaries) {
 | 
				
			||||||
 | 
					      markNeedsScaleRecalc = true;
 | 
				
			||||||
 | 
					      cachedScaleBoundaries = widget.scaleBoundaries;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return StreamBuilder(
 | 
				
			||||||
 | 
					      stream: controller.outputStateStream,
 | 
				
			||||||
 | 
					      initialData: controller.prevValue,
 | 
				
			||||||
 | 
					      builder: (
 | 
				
			||||||
 | 
					        BuildContext context,
 | 
				
			||||||
 | 
					        AsyncSnapshot<PhotoViewControllerValue> snapshot,
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        if (snapshot.hasData) {
 | 
				
			||||||
 | 
					          final PhotoViewControllerValue value = snapshot.data!;
 | 
				
			||||||
 | 
					          final useImageScale = widget.filterQuality != FilterQuality.none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          final computedScale = useImageScale ? 1.0 : scale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          final matrix = Matrix4.identity()
 | 
				
			||||||
 | 
					            ..translate(value.position.dx, value.position.dy)
 | 
				
			||||||
 | 
					            ..scale(computedScale)
 | 
				
			||||||
 | 
					            ..rotateZ(value.rotation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          final Widget customChildLayout = CustomSingleChildLayout(
 | 
				
			||||||
 | 
					            delegate: _CenterWithOriginalSizeDelegate(
 | 
				
			||||||
 | 
					              scaleBoundaries.childSize,
 | 
				
			||||||
 | 
					              basePosition,
 | 
				
			||||||
 | 
					              useImageScale,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            child: _buildHero(),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          final child = Container(
 | 
				
			||||||
 | 
					            constraints: widget.tightMode
 | 
				
			||||||
 | 
					                ? BoxConstraints.tight(scaleBoundaries.childSize * scale)
 | 
				
			||||||
 | 
					                : null,
 | 
				
			||||||
 | 
					            decoration: widget.backgroundDecoration ?? _defaultDecoration,
 | 
				
			||||||
 | 
					            child: Center(
 | 
				
			||||||
 | 
					              child: Transform(
 | 
				
			||||||
 | 
					                transform: matrix,
 | 
				
			||||||
 | 
					                alignment: basePosition,
 | 
				
			||||||
 | 
					                child: customChildLayout,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (widget.disableGestures) {
 | 
				
			||||||
 | 
					            return child;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return PhotoViewGestureDetector(
 | 
				
			||||||
 | 
					            onDoubleTap: nextScaleState,
 | 
				
			||||||
 | 
					            onScaleStart: onScaleStart,
 | 
				
			||||||
 | 
					            onScaleUpdate: onScaleUpdate,
 | 
				
			||||||
 | 
					            onScaleEnd: onScaleEnd,
 | 
				
			||||||
 | 
					            onDragStart:  widget.onDragStart != null 
 | 
				
			||||||
 | 
					               ? (details) => widget.onDragStart!(context, details, value)
 | 
				
			||||||
 | 
					               : null,
 | 
				
			||||||
 | 
					            onDragEnd:  widget.onDragEnd != null 
 | 
				
			||||||
 | 
					               ? (details) => widget.onDragEnd!(context, details, value)
 | 
				
			||||||
 | 
					               : null,
 | 
				
			||||||
 | 
					            onDragUpdate: widget.onDragUpdate != null 
 | 
				
			||||||
 | 
					               ? (details) => widget.onDragUpdate!(context, details, value)
 | 
				
			||||||
 | 
					               : null,
 | 
				
			||||||
 | 
					            hitDetector: this,
 | 
				
			||||||
 | 
					            onTapUp: widget.onTapUp != null
 | 
				
			||||||
 | 
					                ? (details) => widget.onTapUp!(context, details, value)
 | 
				
			||||||
 | 
					                : null,
 | 
				
			||||||
 | 
					            onTapDown: widget.onTapDown != null
 | 
				
			||||||
 | 
					                ? (details) => widget.onTapDown!(context, details, value)
 | 
				
			||||||
 | 
					                : null,
 | 
				
			||||||
 | 
					            child: child,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          return Container();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildHero() {
 | 
				
			||||||
 | 
					    return heroAttributes != null
 | 
				
			||||||
 | 
					        ? Hero(
 | 
				
			||||||
 | 
					            tag: heroAttributes!.tag,
 | 
				
			||||||
 | 
					            createRectTween: heroAttributes!.createRectTween,
 | 
				
			||||||
 | 
					            flightShuttleBuilder: heroAttributes!.flightShuttleBuilder,
 | 
				
			||||||
 | 
					            placeholderBuilder: heroAttributes!.placeholderBuilder,
 | 
				
			||||||
 | 
					            transitionOnUserGestures: heroAttributes!.transitionOnUserGestures,
 | 
				
			||||||
 | 
					            child: _buildChild(),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        : _buildChild();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildChild() {
 | 
				
			||||||
 | 
					    return widget.hasCustomChild
 | 
				
			||||||
 | 
					        ? widget.customChild!
 | 
				
			||||||
 | 
					        : Image(
 | 
				
			||||||
 | 
					            image: widget.imageProvider!,
 | 
				
			||||||
 | 
					            gaplessPlayback: widget.gaplessPlayback ?? false,
 | 
				
			||||||
 | 
					            filterQuality: widget.filterQuality,
 | 
				
			||||||
 | 
					            width: scaleBoundaries.childSize.width * scale,
 | 
				
			||||||
 | 
					            fit: BoxFit.contain,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _CenterWithOriginalSizeDelegate extends SingleChildLayoutDelegate {
 | 
				
			||||||
 | 
					  const _CenterWithOriginalSizeDelegate(
 | 
				
			||||||
 | 
					    this.subjectSize,
 | 
				
			||||||
 | 
					    this.basePosition,
 | 
				
			||||||
 | 
					    this.useImageScale,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Size subjectSize;
 | 
				
			||||||
 | 
					  final Alignment basePosition;
 | 
				
			||||||
 | 
					  final bool useImageScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Offset getPositionForChild(Size size, Size childSize) {
 | 
				
			||||||
 | 
					    final childWidth = useImageScale ? childSize.width : subjectSize.width;
 | 
				
			||||||
 | 
					    final childHeight = useImageScale ? childSize.height : subjectSize.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final halfWidth = (size.width - childWidth) / 2;
 | 
				
			||||||
 | 
					    final halfHeight = (size.height - childHeight) / 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final double offsetX = halfWidth * (basePosition.x + 1);
 | 
				
			||||||
 | 
					    final double offsetY = halfHeight * (basePosition.y + 1);
 | 
				
			||||||
 | 
					    return Offset(offsetX, offsetY);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
 | 
				
			||||||
 | 
					    return useImageScale
 | 
				
			||||||
 | 
					        ? const BoxConstraints()
 | 
				
			||||||
 | 
					        : BoxConstraints.tight(subjectSize);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool shouldRelayout(_CenterWithOriginalSizeDelegate oldDelegate) {
 | 
				
			||||||
 | 
					    return oldDelegate != this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) =>
 | 
				
			||||||
 | 
					      identical(this, other) ||
 | 
				
			||||||
 | 
					      other is _CenterWithOriginalSizeDelegate &&
 | 
				
			||||||
 | 
					          runtimeType == other.runtimeType &&
 | 
				
			||||||
 | 
					          subjectSize == other.subjectSize &&
 | 
				
			||||||
 | 
					          basePosition == other.basePosition &&
 | 
				
			||||||
 | 
					          useImageScale == other.useImageScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					      subjectSize.hashCode ^ basePosition.hashCode ^ useImageScale.hashCode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,293 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/gestures.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'photo_view_hit_corners.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Credit to [eduribas](https://github.com/eduribas/photo_view/commit/508d9b77dafbcf88045b4a7fee737eed4064ea2c)
 | 
				
			||||||
 | 
					/// for the gist
 | 
				
			||||||
 | 
					class PhotoViewGestureDetector extends StatelessWidget {
 | 
				
			||||||
 | 
					  const PhotoViewGestureDetector({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    this.hitDetector,
 | 
				
			||||||
 | 
					    this.onScaleStart,
 | 
				
			||||||
 | 
					    this.onScaleUpdate,
 | 
				
			||||||
 | 
					    this.onScaleEnd,
 | 
				
			||||||
 | 
					    this.onDoubleTap,
 | 
				
			||||||
 | 
					    this.onDragStart,
 | 
				
			||||||
 | 
					    this.onDragEnd,
 | 
				
			||||||
 | 
					    this.onDragUpdate,
 | 
				
			||||||
 | 
					    this.child,
 | 
				
			||||||
 | 
					    this.onTapUp,
 | 
				
			||||||
 | 
					    this.onTapDown,
 | 
				
			||||||
 | 
					    this.behavior,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final GestureDoubleTapCallback? onDoubleTap;
 | 
				
			||||||
 | 
					  final HitCornersDetector? hitDetector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final GestureScaleStartCallback? onScaleStart;
 | 
				
			||||||
 | 
					  final GestureScaleUpdateCallback? onScaleUpdate;
 | 
				
			||||||
 | 
					  final GestureScaleEndCallback? onScaleEnd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final GestureDragEndCallback? onDragEnd;
 | 
				
			||||||
 | 
					  final GestureDragStartCallback? onDragStart;
 | 
				
			||||||
 | 
					  final GestureDragUpdateCallback? onDragUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final GestureTapUpCallback? onTapUp;
 | 
				
			||||||
 | 
					  final GestureTapDownCallback? onTapDown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Widget? child;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final HitTestBehavior? behavior;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final scope = PhotoViewGestureDetectorScope.of(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Axis? axis = scope?.axis;
 | 
				
			||||||
 | 
					    final touchSlopFactor = scope?.touchSlopFactor ?? 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Map<Type, GestureRecognizerFactory> gestures =
 | 
				
			||||||
 | 
					        <Type, GestureRecognizerFactory>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (onTapDown != null || onTapUp != null) {
 | 
				
			||||||
 | 
					      gestures[TapGestureRecognizer] =
 | 
				
			||||||
 | 
					          GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
 | 
				
			||||||
 | 
					        () => TapGestureRecognizer(debugOwner: this),
 | 
				
			||||||
 | 
					        (TapGestureRecognizer instance) {
 | 
				
			||||||
 | 
					          instance
 | 
				
			||||||
 | 
					            ..onTapDown = onTapDown
 | 
				
			||||||
 | 
					            ..onTapUp = onTapUp;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (onDragStart != null || onDragEnd != null || onDragUpdate != null) {
 | 
				
			||||||
 | 
					      gestures[VerticalDragGestureRecognizer] = 
 | 
				
			||||||
 | 
					          GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
 | 
				
			||||||
 | 
					        () => VerticalDragGestureRecognizer(debugOwner: this),
 | 
				
			||||||
 | 
					        (VerticalDragGestureRecognizer instance) {
 | 
				
			||||||
 | 
					          instance
 | 
				
			||||||
 | 
					              ..onStart = onDragStart
 | 
				
			||||||
 | 
					              ..onUpdate = onDragUpdate
 | 
				
			||||||
 | 
					              ..onEnd = onDragEnd;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    gestures[DoubleTapGestureRecognizer] =
 | 
				
			||||||
 | 
					        GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
 | 
				
			||||||
 | 
					      () => DoubleTapGestureRecognizer(debugOwner: this),
 | 
				
			||||||
 | 
					      (DoubleTapGestureRecognizer instance) {
 | 
				
			||||||
 | 
					        instance.onDoubleTap = onDoubleTap;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    gestures[PhotoViewGestureRecognizer] =
 | 
				
			||||||
 | 
					        GestureRecognizerFactoryWithHandlers<PhotoViewGestureRecognizer>(
 | 
				
			||||||
 | 
					      () => PhotoViewGestureRecognizer(
 | 
				
			||||||
 | 
					          hitDetector: hitDetector,
 | 
				
			||||||
 | 
					          debugOwner: this,
 | 
				
			||||||
 | 
					          validateAxis: axis,
 | 
				
			||||||
 | 
					          touchSlopFactor: touchSlopFactor,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      (PhotoViewGestureRecognizer instance) {
 | 
				
			||||||
 | 
					        instance
 | 
				
			||||||
 | 
					          ..onStart = onScaleStart
 | 
				
			||||||
 | 
					          ..onUpdate = onScaleUpdate
 | 
				
			||||||
 | 
					          ..onEnd = onScaleEnd;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return RawGestureDetector(
 | 
				
			||||||
 | 
					      behavior: behavior,
 | 
				
			||||||
 | 
					      gestures: gestures,
 | 
				
			||||||
 | 
					      child: child,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PhotoViewGestureRecognizer extends ScaleGestureRecognizer {
 | 
				
			||||||
 | 
					  PhotoViewGestureRecognizer({
 | 
				
			||||||
 | 
					    this.hitDetector,
 | 
				
			||||||
 | 
					    Object? debugOwner,
 | 
				
			||||||
 | 
					    this.validateAxis,
 | 
				
			||||||
 | 
					    this.touchSlopFactor = 1,
 | 
				
			||||||
 | 
					    PointerDeviceKind? kind,
 | 
				
			||||||
 | 
					  }) : super(debugOwner: debugOwner, supportedDevices: null);
 | 
				
			||||||
 | 
					  final HitCornersDetector? hitDetector;
 | 
				
			||||||
 | 
					  final Axis? validateAxis;
 | 
				
			||||||
 | 
					  final double touchSlopFactor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<int, Offset> _pointerLocations = <int, Offset>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Offset? _initialFocalPoint;
 | 
				
			||||||
 | 
					  Offset? _currentFocalPoint;
 | 
				
			||||||
 | 
					  double? _initialSpan;
 | 
				
			||||||
 | 
					  double? _currentSpan;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool ready = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void addAllowedPointer(PointerDownEvent event) {
 | 
				
			||||||
 | 
					    if (ready) {
 | 
				
			||||||
 | 
					      ready = false;
 | 
				
			||||||
 | 
					      _pointerLocations = <int, Offset>{};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    super.addAllowedPointer(event);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didStopTrackingLastPointer(int pointer) {
 | 
				
			||||||
 | 
					    ready = true;
 | 
				
			||||||
 | 
					    super.didStopTrackingLastPointer(pointer);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void handleEvent(PointerEvent event) {
 | 
				
			||||||
 | 
					    if (validateAxis != null) {
 | 
				
			||||||
 | 
					      bool didChangeConfiguration = false;
 | 
				
			||||||
 | 
					      if (event is PointerMoveEvent) {
 | 
				
			||||||
 | 
					        if (!event.synthesized) {
 | 
				
			||||||
 | 
					          _pointerLocations[event.pointer] = event.position;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (event is PointerDownEvent) {
 | 
				
			||||||
 | 
					        _pointerLocations[event.pointer] = event.position;
 | 
				
			||||||
 | 
					        didChangeConfiguration = true;
 | 
				
			||||||
 | 
					      } else if (event is PointerUpEvent || event is PointerCancelEvent) {
 | 
				
			||||||
 | 
					        _pointerLocations.remove(event.pointer);
 | 
				
			||||||
 | 
					        didChangeConfiguration = true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _updateDistances();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (didChangeConfiguration) {
 | 
				
			||||||
 | 
					        // cf super._reconfigure
 | 
				
			||||||
 | 
					        _initialFocalPoint = _currentFocalPoint;
 | 
				
			||||||
 | 
					        _initialSpan = _currentSpan;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _decideIfWeAcceptEvent(event);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    super.handleEvent(event);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _updateDistances() {
 | 
				
			||||||
 | 
					    // cf super._update
 | 
				
			||||||
 | 
					    final int count = _pointerLocations.keys.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Compute the focal point
 | 
				
			||||||
 | 
					    Offset focalPoint = Offset.zero;
 | 
				
			||||||
 | 
					    for (final int pointer in _pointerLocations.keys) {
 | 
				
			||||||
 | 
					      focalPoint += _pointerLocations[pointer]!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _currentFocalPoint =
 | 
				
			||||||
 | 
					        count > 0 ? focalPoint / count.toDouble() : Offset.zero;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Span is the average deviation from focal point. Horizontal and vertical
 | 
				
			||||||
 | 
					    // spans are the average deviations from the focal point's horizontal and
 | 
				
			||||||
 | 
					    // vertical coordinates, respectively.
 | 
				
			||||||
 | 
					    double totalDeviation = 0.0;
 | 
				
			||||||
 | 
					    for (final int pointer in _pointerLocations.keys) {
 | 
				
			||||||
 | 
					      totalDeviation +=
 | 
				
			||||||
 | 
					          (_currentFocalPoint! - _pointerLocations[pointer]!).distance;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _decideIfWeAcceptEvent(PointerEvent event) {
 | 
				
			||||||
 | 
					    final move = _initialFocalPoint! - _currentFocalPoint!;
 | 
				
			||||||
 | 
					    final bool shouldMove = validateAxis == Axis.vertical
 | 
				
			||||||
 | 
					        ? hitDetector!.shouldMove(move, Axis.vertical)
 | 
				
			||||||
 | 
					        : hitDetector!.shouldMove(move, Axis.horizontal);
 | 
				
			||||||
 | 
					    if (shouldMove || _pointerLocations.keys.length > 1) {
 | 
				
			||||||
 | 
					      final double spanDelta = (_currentSpan! - _initialSpan!).abs();
 | 
				
			||||||
 | 
					      final double focalPointDelta =
 | 
				
			||||||
 | 
					          (_currentFocalPoint! - _initialFocalPoint!).distance;
 | 
				
			||||||
 | 
					      // warning: do not compare `focalPointDelta` to `kPanSlop`
 | 
				
			||||||
 | 
					      // `ScaleGestureRecognizer` uses `kPanSlop`, but `HorizontalDragGestureRecognizer` uses `kTouchSlop`
 | 
				
			||||||
 | 
					      // and PhotoView recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView`
 | 
				
			||||||
 | 
					      // setting `touchSlopFactor` to 2 restores default `ScaleGestureRecognizer` behaviour as `kPanSlop = kTouchSlop * 2.0`
 | 
				
			||||||
 | 
					      // setting `touchSlopFactor` in [0, 1] will allow this recognizer to accept the gesture before the one from `PageView`
 | 
				
			||||||
 | 
					      if (spanDelta > kScaleSlop ||
 | 
				
			||||||
 | 
					          focalPointDelta > kTouchSlop * touchSlopFactor) {
 | 
				
			||||||
 | 
					        acceptGesture(event.pointer);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An [InheritedWidget] responsible to give a axis aware scope to [PhotoViewGestureRecognizer].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// When using this, PhotoView will test if the content zoomed has hit edge every time user pinches,
 | 
				
			||||||
 | 
					/// if so, it will let parent gesture detectors win the gesture arena
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Useful when placing PhotoView inside a gesture sensitive context,
 | 
				
			||||||
 | 
					/// such as [PageView], [Dismissible], [BottomSheet].
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Usage example:
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// PhotoViewGestureDetectorScope(
 | 
				
			||||||
 | 
					///   axis: Axis.vertical,
 | 
				
			||||||
 | 
					///   child: PhotoView(
 | 
				
			||||||
 | 
					///     imageProvider: AssetImage("assets/pudim.jpg"),
 | 
				
			||||||
 | 
					///   ),
 | 
				
			||||||
 | 
					/// );
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					class PhotoViewGestureDetectorScope extends InheritedWidget {
 | 
				
			||||||
 | 
					  const PhotoViewGestureDetectorScope({
 | 
				
			||||||
 | 
					    super.key, 
 | 
				
			||||||
 | 
					    this.axis,
 | 
				
			||||||
 | 
					    this.touchSlopFactor = .2,
 | 
				
			||||||
 | 
					    required Widget child,
 | 
				
			||||||
 | 
					  }) : super(child: child);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static PhotoViewGestureDetectorScope? of(BuildContext context) {
 | 
				
			||||||
 | 
					    final PhotoViewGestureDetectorScope? scope = context
 | 
				
			||||||
 | 
					        .dependOnInheritedWidgetOfExactType<PhotoViewGestureDetectorScope>();
 | 
				
			||||||
 | 
					    return scope;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Axis? axis;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // in [0, 1[
 | 
				
			||||||
 | 
					  // 0: most reactive but will not let tap recognizers accept gestures
 | 
				
			||||||
 | 
					  // <1: less reactive but gives the most leeway to other recognizers
 | 
				
			||||||
 | 
					  // 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree
 | 
				
			||||||
 | 
					  final double touchSlopFactor;  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool updateShouldNotify(PhotoViewGestureDetectorScope oldWidget) {
 | 
				
			||||||
 | 
					    return axis != oldWidget.axis && touchSlopFactor != oldWidget.touchSlopFactor;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// `PageView` contains a `Scrollable` which sets up a `HorizontalDragGestureRecognizer`
 | 
				
			||||||
 | 
					// this recognizer will win in the gesture arena when the drag distance reaches `kTouchSlop`
 | 
				
			||||||
 | 
					// we cannot change that, but we can prevent the scrollable from panning until this threshold is reached
 | 
				
			||||||
 | 
					// and let other recognizers accept the gesture instead
 | 
				
			||||||
 | 
					class PhotoViewPageViewScrollPhysics extends ScrollPhysics {
 | 
				
			||||||
 | 
					  const PhotoViewPageViewScrollPhysics({
 | 
				
			||||||
 | 
					    this.touchSlopFactor = 0.1,
 | 
				
			||||||
 | 
					    ScrollPhysics? parent,
 | 
				
			||||||
 | 
					  }) : super(parent: parent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // in [0, 1]
 | 
				
			||||||
 | 
					  // 0: most reactive but will not let PhotoView recognizers accept gestures
 | 
				
			||||||
 | 
					  // 1: less reactive but gives the most leeway to PhotoView recognizers
 | 
				
			||||||
 | 
					  final double touchSlopFactor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  PhotoViewPageViewScrollPhysics applyTo(ScrollPhysics? ancestor) {
 | 
				
			||||||
 | 
					    return PhotoViewPageViewScrollPhysics(
 | 
				
			||||||
 | 
					      touchSlopFactor: touchSlopFactor,
 | 
				
			||||||
 | 
					      parent: buildParent(ancestor),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  double get dragStartDistanceMotionThreshold => kTouchSlop * touchSlopFactor;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/controller/photo_view_controller_delegate.dart'
 | 
				
			||||||
 | 
					    show PhotoViewControllerDelegate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mixin HitCornersDetector on PhotoViewControllerDelegate {
 | 
				
			||||||
 | 
					  HitCorners _hitCornersX() {
 | 
				
			||||||
 | 
					    final double childWidth = scaleBoundaries.childSize.width * scale;
 | 
				
			||||||
 | 
					    final double screenWidth = scaleBoundaries.outerSize.width;
 | 
				
			||||||
 | 
					    if (screenWidth >= childWidth) {
 | 
				
			||||||
 | 
					      return const HitCorners(true, true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final x = -position.dx;
 | 
				
			||||||
 | 
					    final cornersX = this.cornersX();
 | 
				
			||||||
 | 
					    return HitCorners(x <= cornersX.min, x >= cornersX.max);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  HitCorners _hitCornersY() {
 | 
				
			||||||
 | 
					    final double childHeight = scaleBoundaries.childSize.height * scale;
 | 
				
			||||||
 | 
					    final double screenHeight = scaleBoundaries.outerSize.height;
 | 
				
			||||||
 | 
					    if (screenHeight >= childHeight) {
 | 
				
			||||||
 | 
					      return const HitCorners(true, true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final y = -position.dy;
 | 
				
			||||||
 | 
					    final cornersY = this.cornersY();
 | 
				
			||||||
 | 
					    return HitCorners(y <= cornersY.min, y >= cornersY.max);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _shouldMoveAxis(HitCorners hitCorners, double mainAxisMove, double crossAxisMove) {
 | 
				
			||||||
 | 
					    if (mainAxisMove == 0) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!hitCorners.hasHitAny) {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final axisBlocked = hitCorners.hasHitBoth ||
 | 
				
			||||||
 | 
					        (hitCorners.hasHitMax ? mainAxisMove > 0 : mainAxisMove < 0);
 | 
				
			||||||
 | 
					    if (axisBlocked) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _shouldMoveX(Offset move) {
 | 
				
			||||||
 | 
					    final hitCornersX = _hitCornersX();
 | 
				
			||||||
 | 
					    final mainAxisMove = move.dx;
 | 
				
			||||||
 | 
					    final crossAxisMove = move.dy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return _shouldMoveAxis(hitCornersX, mainAxisMove, crossAxisMove);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _shouldMoveY(Offset move) {
 | 
				
			||||||
 | 
					    final hitCornersY = _hitCornersY();
 | 
				
			||||||
 | 
					    final mainAxisMove = move.dy;
 | 
				
			||||||
 | 
					    final crossAxisMove = move.dx;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return _shouldMoveAxis(hitCornersY, mainAxisMove, crossAxisMove);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool shouldMove(Offset move, Axis mainAxis) {
 | 
				
			||||||
 | 
					    if (mainAxis == Axis.vertical) {
 | 
				
			||||||
 | 
					      return _shouldMoveY(move);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return _shouldMoveX(move);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HitCorners {
 | 
				
			||||||
 | 
					  const HitCorners(this.hasHitMin, this.hasHitMax);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final bool hasHitMin;
 | 
				
			||||||
 | 
					  final bool hasHitMax;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get hasHitAny => hasHitMin || hasHitMax;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get hasHitBoth => hasHitMin && hasHitMax;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					/// A class that work as a enum. It overloads the operator `*` saving the double as a multiplier.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// PhotoViewComputedScale.contained * 2
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					class PhotoViewComputedScale {
 | 
				
			||||||
 | 
					  const PhotoViewComputedScale._internal(this._value, [this.multiplier = 1.0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String _value;
 | 
				
			||||||
 | 
					  final double multiplier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'Enum.$_value';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const contained = PhotoViewComputedScale._internal('contained');
 | 
				
			||||||
 | 
					  static const covered = PhotoViewComputedScale._internal('covered');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PhotoViewComputedScale operator *(double multiplier) {
 | 
				
			||||||
 | 
					    return PhotoViewComputedScale._internal(_value, multiplier);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PhotoViewComputedScale operator /(double divider) {
 | 
				
			||||||
 | 
					    return PhotoViewComputedScale._internal(_value, 1 / divider);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) =>
 | 
				
			||||||
 | 
					      identical(this, other) ||
 | 
				
			||||||
 | 
					      other is PhotoViewComputedScale &&
 | 
				
			||||||
 | 
					          runtimeType == other.runtimeType &&
 | 
				
			||||||
 | 
					          _value == other._value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode => _value.hashCode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PhotoViewDefaultError extends StatelessWidget {
 | 
				
			||||||
 | 
					  const PhotoViewDefaultError({Key? key, required this.decoration})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final BoxDecoration decoration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return DecoratedBox(
 | 
				
			||||||
 | 
					      decoration: decoration,
 | 
				
			||||||
 | 
					      child: Center(
 | 
				
			||||||
 | 
					        child: Icon(
 | 
				
			||||||
 | 
					          Icons.broken_image,
 | 
				
			||||||
 | 
					          color: Colors.grey[400],
 | 
				
			||||||
 | 
					          size: 40.0,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PhotoViewDefaultLoading extends StatelessWidget {
 | 
				
			||||||
 | 
					  const PhotoViewDefaultLoading({Key? key, this.event}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ImageChunkEvent? event;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final expectedBytes = event?.expectedTotalBytes;
 | 
				
			||||||
 | 
					    final loadedBytes = event?.cumulativeBytesLoaded;
 | 
				
			||||||
 | 
					    final value = loadedBytes != null && expectedBytes != null
 | 
				
			||||||
 | 
					        ? loadedBytes / expectedBytes
 | 
				
			||||||
 | 
					        : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Center(
 | 
				
			||||||
 | 
					      child: SizedBox(
 | 
				
			||||||
 | 
					        width: 20.0,
 | 
				
			||||||
 | 
					        height: 20.0,
 | 
				
			||||||
 | 
					        child: CircularProgressIndicator(value: value),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					/// A way to represent the step of the "doubletap gesture cycle" in which PhotoView is.
 | 
				
			||||||
 | 
					enum PhotoViewScaleState {
 | 
				
			||||||
 | 
					  initial,
 | 
				
			||||||
 | 
					  covering,
 | 
				
			||||||
 | 
					  originalSize,
 | 
				
			||||||
 | 
					  zoomedIn,
 | 
				
			||||||
 | 
					  zoomedOut;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get isScaleStateZooming =>
 | 
				
			||||||
 | 
					      this == PhotoViewScaleState.zoomedIn ||
 | 
				
			||||||
 | 
					      this == PhotoViewScaleState.zoomedOut;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										327
									
								
								mobile/lib/shared/ui/photo_view/src/photo_view_wrappers.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								mobile/lib/shared/ui/photo_view/src/photo_view_wrappers.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,327 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '../photo_view.dart';
 | 
				
			||||||
 | 
					import 'core/photo_view_core.dart';
 | 
				
			||||||
 | 
					import 'photo_view_default_widgets.dart';
 | 
				
			||||||
 | 
					import 'utils/photo_view_utils.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImageWrapper extends StatefulWidget {
 | 
				
			||||||
 | 
					  const ImageWrapper({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    required this.imageProvider,
 | 
				
			||||||
 | 
					    required this.loadingBuilder,
 | 
				
			||||||
 | 
					    required this.backgroundDecoration,
 | 
				
			||||||
 | 
					    required this.gaplessPlayback,
 | 
				
			||||||
 | 
					    required this.heroAttributes,
 | 
				
			||||||
 | 
					    required this.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					    required this.enableRotation,
 | 
				
			||||||
 | 
					    required this.controller,
 | 
				
			||||||
 | 
					    required this.scaleStateController,
 | 
				
			||||||
 | 
					    required this.maxScale,
 | 
				
			||||||
 | 
					    required this.minScale,
 | 
				
			||||||
 | 
					    required this.initialScale,
 | 
				
			||||||
 | 
					    required this.basePosition,
 | 
				
			||||||
 | 
					    required this.scaleStateCycle,
 | 
				
			||||||
 | 
					    required this.onTapUp,
 | 
				
			||||||
 | 
					    required this.onTapDown,
 | 
				
			||||||
 | 
					    required this.onDragStart,
 | 
				
			||||||
 | 
					    required this.onDragEnd,
 | 
				
			||||||
 | 
					    required this.onDragUpdate,
 | 
				
			||||||
 | 
					    required this.onScaleEnd,
 | 
				
			||||||
 | 
					    required this.outerSize,
 | 
				
			||||||
 | 
					    required this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    required this.tightMode,
 | 
				
			||||||
 | 
					    required this.filterQuality,
 | 
				
			||||||
 | 
					    required this.disableGestures,
 | 
				
			||||||
 | 
					    required this.errorBuilder,
 | 
				
			||||||
 | 
					    required this.enablePanAlways,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ImageProvider imageProvider;
 | 
				
			||||||
 | 
					  final LoadingBuilder? loadingBuilder;
 | 
				
			||||||
 | 
					  final ImageErrorWidgetBuilder? errorBuilder;
 | 
				
			||||||
 | 
					  final BoxDecoration backgroundDecoration;
 | 
				
			||||||
 | 
					  final bool gaplessPlayback;
 | 
				
			||||||
 | 
					  final PhotoViewHeroAttributes? heroAttributes;
 | 
				
			||||||
 | 
					  final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
 | 
				
			||||||
 | 
					  final bool enableRotation;
 | 
				
			||||||
 | 
					  final dynamic maxScale;
 | 
				
			||||||
 | 
					  final dynamic minScale;
 | 
				
			||||||
 | 
					  final dynamic initialScale;
 | 
				
			||||||
 | 
					  final PhotoViewControllerBase controller;
 | 
				
			||||||
 | 
					  final PhotoViewScaleStateController scaleStateController;
 | 
				
			||||||
 | 
					  final Alignment? basePosition;
 | 
				
			||||||
 | 
					  final ScaleStateCycle? scaleStateCycle;
 | 
				
			||||||
 | 
					  final PhotoViewImageTapUpCallback? onTapUp;
 | 
				
			||||||
 | 
					  final PhotoViewImageTapDownCallback? onTapDown;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragStartCallback? onDragStart;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragEndCallback? onDragEnd;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragUpdateCallback? onDragUpdate;
 | 
				
			||||||
 | 
					  final PhotoViewImageScaleEndCallback? onScaleEnd;
 | 
				
			||||||
 | 
					  final Size outerSize;
 | 
				
			||||||
 | 
					  final HitTestBehavior? gestureDetectorBehavior;
 | 
				
			||||||
 | 
					  final bool? tightMode;
 | 
				
			||||||
 | 
					  final FilterQuality? filterQuality;
 | 
				
			||||||
 | 
					  final bool? disableGestures;
 | 
				
			||||||
 | 
					  final bool? enablePanAlways;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  createState() => _ImageWrapperState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ImageWrapperState extends State<ImageWrapper> {
 | 
				
			||||||
 | 
					  ImageStreamListener? _imageStreamListener;
 | 
				
			||||||
 | 
					  ImageStream? _imageStream;
 | 
				
			||||||
 | 
					  ImageChunkEvent? _loadingProgress;
 | 
				
			||||||
 | 
					  ImageInfo? _imageInfo;
 | 
				
			||||||
 | 
					  bool _loading = true;
 | 
				
			||||||
 | 
					  Size? _imageSize;
 | 
				
			||||||
 | 
					  Object? _lastException;
 | 
				
			||||||
 | 
					  StackTrace? _lastStack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					    _stopImageStream();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didChangeDependencies() {
 | 
				
			||||||
 | 
					    _resolveImage();
 | 
				
			||||||
 | 
					    super.didChangeDependencies();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didUpdateWidget(ImageWrapper oldWidget) {
 | 
				
			||||||
 | 
					    super.didUpdateWidget(oldWidget);
 | 
				
			||||||
 | 
					    if (widget.imageProvider != oldWidget.imageProvider) {
 | 
				
			||||||
 | 
					      _resolveImage();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // retrieve image from the provider
 | 
				
			||||||
 | 
					  void _resolveImage() {
 | 
				
			||||||
 | 
					    final ImageStream newStream = widget.imageProvider.resolve(
 | 
				
			||||||
 | 
					      const ImageConfiguration(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    _updateSourceStream(newStream);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ImageStreamListener _getOrCreateListener() {
 | 
				
			||||||
 | 
					    void handleImageChunk(ImageChunkEvent event) {
 | 
				
			||||||
 | 
					      setState(() {
 | 
				
			||||||
 | 
					        _loadingProgress = event;
 | 
				
			||||||
 | 
					        _lastException = null;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void handleImageFrame(ImageInfo info, bool synchronousCall) {
 | 
				
			||||||
 | 
					      setupCB() {
 | 
				
			||||||
 | 
					        _imageSize = Size(
 | 
				
			||||||
 | 
					          info.image.width.toDouble(),
 | 
				
			||||||
 | 
					          info.image.height.toDouble(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        _loading = false;
 | 
				
			||||||
 | 
					        _imageInfo = _imageInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _loadingProgress = null;
 | 
				
			||||||
 | 
					        _lastException = null;
 | 
				
			||||||
 | 
					        _lastStack = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      synchronousCall ? setupCB() : setState(setupCB);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void handleError(dynamic error, StackTrace? stackTrace) {
 | 
				
			||||||
 | 
					      setState(() {
 | 
				
			||||||
 | 
					        _loading = false;
 | 
				
			||||||
 | 
					        _lastException = error;
 | 
				
			||||||
 | 
					        _lastStack = stackTrace;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        if (widget.errorBuilder == null) {
 | 
				
			||||||
 | 
					          throw error;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _imageStreamListener = ImageStreamListener(
 | 
				
			||||||
 | 
					      handleImageFrame,
 | 
				
			||||||
 | 
					      onChunk: handleImageChunk,
 | 
				
			||||||
 | 
					      onError: handleError,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return _imageStreamListener!;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _updateSourceStream(ImageStream newStream) {
 | 
				
			||||||
 | 
					    if (_imageStream?.key == newStream.key) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _imageStream?.removeListener(_imageStreamListener!);
 | 
				
			||||||
 | 
					    _imageStream = newStream;
 | 
				
			||||||
 | 
					    _imageStream!.addListener(_getOrCreateListener());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _stopImageStream() {
 | 
				
			||||||
 | 
					    _imageStream?.removeListener(_imageStreamListener!);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    if (_loading) {
 | 
				
			||||||
 | 
					      return _buildLoading(context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (_lastException != null) {
 | 
				
			||||||
 | 
					      return _buildError(context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final scaleBoundaries = ScaleBoundaries(
 | 
				
			||||||
 | 
					      widget.minScale ?? 0.0,
 | 
				
			||||||
 | 
					      widget.maxScale ?? double.infinity,
 | 
				
			||||||
 | 
					      widget.initialScale ?? PhotoViewComputedScale.contained,
 | 
				
			||||||
 | 
					      widget.outerSize,
 | 
				
			||||||
 | 
					      _imageSize!,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return PhotoViewCore(
 | 
				
			||||||
 | 
					      imageProvider: widget.imageProvider,
 | 
				
			||||||
 | 
					      backgroundDecoration: widget.backgroundDecoration,
 | 
				
			||||||
 | 
					      gaplessPlayback: widget.gaplessPlayback,
 | 
				
			||||||
 | 
					      enableRotation: widget.enableRotation,
 | 
				
			||||||
 | 
					      heroAttributes: widget.heroAttributes,
 | 
				
			||||||
 | 
					      basePosition: widget.basePosition ?? Alignment.center,
 | 
				
			||||||
 | 
					      controller: widget.controller,
 | 
				
			||||||
 | 
					      scaleStateController: widget.scaleStateController,
 | 
				
			||||||
 | 
					      scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle,
 | 
				
			||||||
 | 
					      scaleBoundaries: scaleBoundaries,
 | 
				
			||||||
 | 
					      onTapUp: widget.onTapUp,
 | 
				
			||||||
 | 
					      onTapDown: widget.onTapDown,
 | 
				
			||||||
 | 
					      onDragStart: widget.onDragStart,
 | 
				
			||||||
 | 
					      onDragEnd: widget.onDragEnd,
 | 
				
			||||||
 | 
					      onDragUpdate: widget.onDragUpdate,
 | 
				
			||||||
 | 
					      onScaleEnd: widget.onScaleEnd,
 | 
				
			||||||
 | 
					      gestureDetectorBehavior: widget.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					      tightMode: widget.tightMode ?? false,
 | 
				
			||||||
 | 
					      filterQuality: widget.filterQuality ?? FilterQuality.none,
 | 
				
			||||||
 | 
					      disableGestures: widget.disableGestures ?? false,
 | 
				
			||||||
 | 
					      enablePanAlways: widget.enablePanAlways ?? false,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildLoading(BuildContext context) {
 | 
				
			||||||
 | 
					    if (widget.loadingBuilder != null) {
 | 
				
			||||||
 | 
					      return widget.loadingBuilder!(context, _loadingProgress);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return PhotoViewDefaultLoading(
 | 
				
			||||||
 | 
					      event: _loadingProgress,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildError(
 | 
				
			||||||
 | 
					    BuildContext context,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    if (widget.errorBuilder != null) {
 | 
				
			||||||
 | 
					      return widget.errorBuilder!(context, _lastException!, _lastStack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return PhotoViewDefaultError(
 | 
				
			||||||
 | 
					      decoration: widget.backgroundDecoration,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CustomChildWrapper extends StatelessWidget {
 | 
				
			||||||
 | 
					  const CustomChildWrapper({
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					    this.child,
 | 
				
			||||||
 | 
					    required this.childSize,
 | 
				
			||||||
 | 
					    required this.backgroundDecoration,
 | 
				
			||||||
 | 
					    this.heroAttributes,
 | 
				
			||||||
 | 
					    this.scaleStateChangedCallback,
 | 
				
			||||||
 | 
					    required this.enableRotation,
 | 
				
			||||||
 | 
					    required this.controller,
 | 
				
			||||||
 | 
					    required this.scaleStateController,
 | 
				
			||||||
 | 
					    required this.maxScale,
 | 
				
			||||||
 | 
					    required this.minScale,
 | 
				
			||||||
 | 
					    required this.initialScale,
 | 
				
			||||||
 | 
					    required this.basePosition,
 | 
				
			||||||
 | 
					    required this.scaleStateCycle,
 | 
				
			||||||
 | 
					    this.onTapUp,
 | 
				
			||||||
 | 
					    this.onTapDown,
 | 
				
			||||||
 | 
					    this.onDragStart,
 | 
				
			||||||
 | 
					    this.onDragEnd,
 | 
				
			||||||
 | 
					    this.onDragUpdate,
 | 
				
			||||||
 | 
					    this.onScaleEnd,
 | 
				
			||||||
 | 
					    required this.outerSize,
 | 
				
			||||||
 | 
					    this.gestureDetectorBehavior,
 | 
				
			||||||
 | 
					    required this.tightMode,
 | 
				
			||||||
 | 
					    required this.filterQuality,
 | 
				
			||||||
 | 
					    required this.disableGestures,
 | 
				
			||||||
 | 
					    required this.enablePanAlways,
 | 
				
			||||||
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Widget? child;
 | 
				
			||||||
 | 
					  final Size? childSize;
 | 
				
			||||||
 | 
					  final Decoration backgroundDecoration;
 | 
				
			||||||
 | 
					  final PhotoViewHeroAttributes? heroAttributes;
 | 
				
			||||||
 | 
					  final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
 | 
				
			||||||
 | 
					  final bool enableRotation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final PhotoViewControllerBase controller;
 | 
				
			||||||
 | 
					  final PhotoViewScaleStateController scaleStateController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final dynamic maxScale;
 | 
				
			||||||
 | 
					  final dynamic minScale;
 | 
				
			||||||
 | 
					  final dynamic initialScale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Alignment? basePosition;
 | 
				
			||||||
 | 
					  final ScaleStateCycle? scaleStateCycle;
 | 
				
			||||||
 | 
					  final PhotoViewImageTapUpCallback? onTapUp;
 | 
				
			||||||
 | 
					  final PhotoViewImageTapDownCallback? onTapDown;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragStartCallback? onDragStart;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragEndCallback? onDragEnd;
 | 
				
			||||||
 | 
					  final PhotoViewImageDragUpdateCallback? onDragUpdate;
 | 
				
			||||||
 | 
					  final PhotoViewImageScaleEndCallback? onScaleEnd;
 | 
				
			||||||
 | 
					  final Size outerSize;
 | 
				
			||||||
 | 
					  final HitTestBehavior? gestureDetectorBehavior;
 | 
				
			||||||
 | 
					  final bool? tightMode;
 | 
				
			||||||
 | 
					  final FilterQuality? filterQuality;
 | 
				
			||||||
 | 
					  final bool? disableGestures;
 | 
				
			||||||
 | 
					  final bool? enablePanAlways;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final scaleBoundaries = ScaleBoundaries(
 | 
				
			||||||
 | 
					      minScale ?? 0.0,
 | 
				
			||||||
 | 
					      maxScale ?? double.infinity,
 | 
				
			||||||
 | 
					      initialScale ?? PhotoViewComputedScale.contained,
 | 
				
			||||||
 | 
					      outerSize,
 | 
				
			||||||
 | 
					      childSize ?? outerSize,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return PhotoViewCore.customChild(
 | 
				
			||||||
 | 
					      customChild: child,
 | 
				
			||||||
 | 
					      backgroundDecoration: backgroundDecoration,
 | 
				
			||||||
 | 
					      enableRotation: enableRotation,
 | 
				
			||||||
 | 
					      heroAttributes: heroAttributes,
 | 
				
			||||||
 | 
					      controller: controller,
 | 
				
			||||||
 | 
					      scaleStateController: scaleStateController,
 | 
				
			||||||
 | 
					      scaleStateCycle: scaleStateCycle ?? defaultScaleStateCycle,
 | 
				
			||||||
 | 
					      basePosition: basePosition ?? Alignment.center,
 | 
				
			||||||
 | 
					      scaleBoundaries: scaleBoundaries,
 | 
				
			||||||
 | 
					      onTapUp: onTapUp,
 | 
				
			||||||
 | 
					      onTapDown: onTapDown,
 | 
				
			||||||
 | 
					      onDragStart: onDragStart,
 | 
				
			||||||
 | 
					      onDragEnd: onDragEnd,
 | 
				
			||||||
 | 
					      onDragUpdate: onDragUpdate,
 | 
				
			||||||
 | 
					      onScaleEnd: onScaleEnd,
 | 
				
			||||||
 | 
					      gestureDetectorBehavior: gestureDetectorBehavior,
 | 
				
			||||||
 | 
					      tightMode: tightMode ?? false,
 | 
				
			||||||
 | 
					      filterQuality: filterQuality ?? FilterQuality.none,
 | 
				
			||||||
 | 
					      disableGestures: disableGestures ?? false,
 | 
				
			||||||
 | 
					      enablePanAlways: enablePanAlways ?? false,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A [ChangeNotifier] that has a second collection of listeners: the ignorable ones
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Those listeners will be fired when [notifyListeners] fires and will be ignored
 | 
				
			||||||
 | 
					/// when [notifySomeListeners] fires.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The common collection of listeners inherited from [ChangeNotifier] will be fired
 | 
				
			||||||
 | 
					/// every time.
 | 
				
			||||||
 | 
					class IgnorableChangeNotifier extends ChangeNotifier {
 | 
				
			||||||
 | 
					  ObserverList<VoidCallback>? _ignorableListeners =
 | 
				
			||||||
 | 
					      ObserverList<VoidCallback>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _debugAssertNotDisposed() {
 | 
				
			||||||
 | 
					    assert(() {
 | 
				
			||||||
 | 
					      if (_ignorableListeners == null) {
 | 
				
			||||||
 | 
					        AssertionError([
 | 
				
			||||||
 | 
					          'A $runtimeType was used after being disposed.',
 | 
				
			||||||
 | 
					          'Once you have called dispose() on a $runtimeType, it can no longer be used.'
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }());
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool get hasListeners {
 | 
				
			||||||
 | 
					    return super.hasListeners || (_ignorableListeners?.isNotEmpty ?? false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void addIgnorableListener(listener) {
 | 
				
			||||||
 | 
					    assert(_debugAssertNotDisposed());
 | 
				
			||||||
 | 
					    _ignorableListeners!.add(listener);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void removeIgnorableListener(listener) {
 | 
				
			||||||
 | 
					    assert(_debugAssertNotDisposed());
 | 
				
			||||||
 | 
					    _ignorableListeners!.remove(listener);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _ignorableListeners = null;
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @protected
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  @visibleForTesting
 | 
				
			||||||
 | 
					  void notifyListeners() {
 | 
				
			||||||
 | 
					    super.notifyListeners();
 | 
				
			||||||
 | 
					    if (_ignorableListeners != null) {
 | 
				
			||||||
 | 
					      final List<VoidCallback> localListeners =
 | 
				
			||||||
 | 
					          List<VoidCallback>.from(_ignorableListeners!);
 | 
				
			||||||
 | 
					      for (VoidCallback listener in localListeners) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          if (_ignorableListeners!.contains(listener)) {
 | 
				
			||||||
 | 
					            listener();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (exception, stack) {
 | 
				
			||||||
 | 
					          FlutterError.reportError(
 | 
				
			||||||
 | 
					            FlutterErrorDetails(
 | 
				
			||||||
 | 
					              exception: exception,
 | 
				
			||||||
 | 
					              stack: stack,
 | 
				
			||||||
 | 
					              library: 'Photoview library',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Ignores the ignoreables
 | 
				
			||||||
 | 
					  @protected
 | 
				
			||||||
 | 
					  void notifySomeListeners() {
 | 
				
			||||||
 | 
					    super.notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Just like [ValueNotifier] except it extends [IgnorableChangeNotifier] which has
 | 
				
			||||||
 | 
					/// listeners that wont fire when [updateIgnoring] is called.
 | 
				
			||||||
 | 
					class IgnorableValueNotifier<T> extends IgnorableChangeNotifier
 | 
				
			||||||
 | 
					    implements ValueListenable<T> {
 | 
				
			||||||
 | 
					  IgnorableValueNotifier(this._value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  T get value => _value;
 | 
				
			||||||
 | 
					  T _value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set value(T newValue) {
 | 
				
			||||||
 | 
					    if (_value == newValue) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _value = newValue;
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void updateIgnoring(T newValue) {
 | 
				
			||||||
 | 
					    if (_value == newValue) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _value = newValue;
 | 
				
			||||||
 | 
					    notifySomeListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => '${describeIdentity(this)}($value)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Data class that holds the attributes that are going to be passed to
 | 
				
			||||||
 | 
					/// [PhotoViewImageWrapper]'s [Hero].
 | 
				
			||||||
 | 
					class PhotoViewHeroAttributes {
 | 
				
			||||||
 | 
					  const PhotoViewHeroAttributes({
 | 
				
			||||||
 | 
					    required this.tag,
 | 
				
			||||||
 | 
					    this.createRectTween,
 | 
				
			||||||
 | 
					    this.flightShuttleBuilder,
 | 
				
			||||||
 | 
					    this.placeholderBuilder,
 | 
				
			||||||
 | 
					    this.transitionOnUserGestures = false,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [Hero.tag]
 | 
				
			||||||
 | 
					  final Object tag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [Hero.createRectTween]
 | 
				
			||||||
 | 
					  final CreateRectTween? createRectTween;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [Hero.flightShuttleBuilder]
 | 
				
			||||||
 | 
					  final HeroFlightShuttleBuilder? flightShuttleBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [Hero.placeholderBuilder]
 | 
				
			||||||
 | 
					  final HeroPlaceholderBuilder? placeholderBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Mirror to [Hero.transitionOnUserGestures]
 | 
				
			||||||
 | 
					  final bool transitionOnUserGestures;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										145
									
								
								mobile/lib/shared/ui/photo_view/src/utils/photo_view_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								mobile/lib/shared/ui/photo_view/src/utils/photo_view_utils.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
				
			|||||||
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					import 'dart:ui' show Size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart";
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Given a [PhotoViewScaleState], returns a scale value considering [scaleBoundaries].
 | 
				
			||||||
 | 
					double getScaleForScaleState(
 | 
				
			||||||
 | 
					  PhotoViewScaleState scaleState,
 | 
				
			||||||
 | 
					  ScaleBoundaries scaleBoundaries,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  switch (scaleState) {
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.initial:
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.zoomedIn:
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.zoomedOut:
 | 
				
			||||||
 | 
					      return _clampSize(scaleBoundaries.initialScale, scaleBoundaries);
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.covering:
 | 
				
			||||||
 | 
					      return _clampSize(
 | 
				
			||||||
 | 
					        _scaleForCovering(
 | 
				
			||||||
 | 
					          scaleBoundaries.outerSize, 
 | 
				
			||||||
 | 
					          scaleBoundaries.childSize,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        scaleBoundaries,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    case PhotoViewScaleState.originalSize:
 | 
				
			||||||
 | 
					      return _clampSize(1.0, scaleBoundaries);
 | 
				
			||||||
 | 
					    // Will never be reached
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Internal class to wraps custom scale boundaries (min, max and initial)
 | 
				
			||||||
 | 
					/// Also, stores values regarding the two sizes: the container and teh child.
 | 
				
			||||||
 | 
					class ScaleBoundaries {
 | 
				
			||||||
 | 
					  const ScaleBoundaries(
 | 
				
			||||||
 | 
					    this._minScale,
 | 
				
			||||||
 | 
					    this._maxScale,
 | 
				
			||||||
 | 
					    this._initialScale,
 | 
				
			||||||
 | 
					    this.outerSize,
 | 
				
			||||||
 | 
					    this.childSize,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final dynamic _minScale;
 | 
				
			||||||
 | 
					  final dynamic _maxScale;
 | 
				
			||||||
 | 
					  final dynamic _initialScale;
 | 
				
			||||||
 | 
					  final Size outerSize;
 | 
				
			||||||
 | 
					  final Size childSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double get minScale {
 | 
				
			||||||
 | 
					    assert(_minScale is double || _minScale is PhotoViewComputedScale);
 | 
				
			||||||
 | 
					    if (_minScale == PhotoViewComputedScale.contained) {
 | 
				
			||||||
 | 
					      return _scaleForContained(outerSize, childSize) *
 | 
				
			||||||
 | 
					          (_minScale as PhotoViewComputedScale).multiplier; // ignore: avoid_as
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_minScale == PhotoViewComputedScale.covered) {
 | 
				
			||||||
 | 
					      return _scaleForCovering(outerSize, childSize) *
 | 
				
			||||||
 | 
					          (_minScale as PhotoViewComputedScale).multiplier; // ignore: avoid_as
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    assert(_minScale >= 0.0);
 | 
				
			||||||
 | 
					    return _minScale;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double get maxScale {
 | 
				
			||||||
 | 
					    assert(_maxScale is double || _maxScale is PhotoViewComputedScale);
 | 
				
			||||||
 | 
					    if (_maxScale == PhotoViewComputedScale.contained) {
 | 
				
			||||||
 | 
					      return (_scaleForContained(outerSize, childSize) *
 | 
				
			||||||
 | 
					              (_maxScale as PhotoViewComputedScale) // ignore: avoid_as
 | 
				
			||||||
 | 
					                  .multiplier)
 | 
				
			||||||
 | 
					          .clamp(minScale, double.infinity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_maxScale == PhotoViewComputedScale.covered) {
 | 
				
			||||||
 | 
					      return (_scaleForCovering(outerSize, childSize) *
 | 
				
			||||||
 | 
					              (_maxScale as PhotoViewComputedScale) // ignore: avoid_as
 | 
				
			||||||
 | 
					                  .multiplier)
 | 
				
			||||||
 | 
					          .clamp(minScale, double.infinity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return _maxScale.clamp(minScale, double.infinity);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  double get initialScale {
 | 
				
			||||||
 | 
					    assert(_initialScale is double || _initialScale is PhotoViewComputedScale);
 | 
				
			||||||
 | 
					    if (_initialScale == PhotoViewComputedScale.contained) {
 | 
				
			||||||
 | 
					      return _scaleForContained(outerSize, childSize) *
 | 
				
			||||||
 | 
					          (_initialScale as PhotoViewComputedScale) // ignore: avoid_as
 | 
				
			||||||
 | 
					              .multiplier;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (_initialScale == PhotoViewComputedScale.covered) {
 | 
				
			||||||
 | 
					      return _scaleForCovering(outerSize, childSize) *
 | 
				
			||||||
 | 
					          (_initialScale as PhotoViewComputedScale) // ignore: avoid_as
 | 
				
			||||||
 | 
					              .multiplier;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return _initialScale.clamp(minScale, maxScale);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) =>
 | 
				
			||||||
 | 
					      identical(this, other) ||
 | 
				
			||||||
 | 
					      other is ScaleBoundaries &&
 | 
				
			||||||
 | 
					          runtimeType == other.runtimeType &&
 | 
				
			||||||
 | 
					          _minScale == other._minScale &&
 | 
				
			||||||
 | 
					          _maxScale == other._maxScale &&
 | 
				
			||||||
 | 
					          _initialScale == other._initialScale &&
 | 
				
			||||||
 | 
					          outerSize == other.outerSize &&
 | 
				
			||||||
 | 
					          childSize == other.childSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					      _minScale.hashCode ^
 | 
				
			||||||
 | 
					      _maxScale.hashCode ^
 | 
				
			||||||
 | 
					      _initialScale.hashCode ^
 | 
				
			||||||
 | 
					      outerSize.hashCode ^
 | 
				
			||||||
 | 
					      childSize.hashCode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					double _scaleForContained(Size size, Size childSize) {
 | 
				
			||||||
 | 
					  final double imageWidth = childSize.width;
 | 
				
			||||||
 | 
					  final double imageHeight = childSize.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final double screenWidth = size.width;
 | 
				
			||||||
 | 
					  final double screenHeight = size.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return math.min(screenWidth / imageWidth, screenHeight / imageHeight);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					double _scaleForCovering(Size size, Size childSize) {
 | 
				
			||||||
 | 
					  final double imageWidth = childSize.width;
 | 
				
			||||||
 | 
					  final double imageHeight = childSize.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final double screenWidth = size.width;
 | 
				
			||||||
 | 
					  final double screenHeight = size.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return math.max(screenWidth / imageWidth, screenHeight / imageHeight);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					double _clampSize(double size, ScaleBoundaries scaleBoundaries) {
 | 
				
			||||||
 | 
					  return size.clamp(scaleBoundaries.minScale, scaleBoundaries.maxScale);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Simple class to store a min and a max value
 | 
				
			||||||
 | 
					class CornersRange {
 | 
				
			||||||
 | 
					  const CornersRange(this.min, this.max);
 | 
				
			||||||
 | 
					  final double min;
 | 
				
			||||||
 | 
					  final double max;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -239,6 +239,13 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.2.3"
 | 
					    version: "2.2.3"
 | 
				
			||||||
 | 
					  easy_image_viewer:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: easy_image_viewer
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.2.0"
 | 
				
			||||||
  easy_localization:
 | 
					  easy_localization:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -757,13 +764,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.5.0"
 | 
					    version: "2.5.0"
 | 
				
			||||||
  photo_view:
 | 
					 | 
				
			||||||
    dependency: "direct main"
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: photo_view
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "0.14.0"
 | 
					 | 
				
			||||||
  platform:
 | 
					  platform:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,6 @@ dependencies:
 | 
				
			|||||||
  video_player: ^2.2.18
 | 
					  video_player: ^2.2.18
 | 
				
			||||||
  chewie: ^1.3.5
 | 
					  chewie: ^1.3.5
 | 
				
			||||||
  badges: ^2.0.2
 | 
					  badges: ^2.0.2
 | 
				
			||||||
  photo_view: ^0.14.0
 | 
					 | 
				
			||||||
  socket_io_client: ^2.0.0-beta.4-nullsafety.0
 | 
					  socket_io_client: ^2.0.0-beta.4-nullsafety.0
 | 
				
			||||||
  flutter_map: ^0.14.0
 | 
					  flutter_map: ^0.14.0
 | 
				
			||||||
  flutter_udid: ^2.0.0
 | 
					  flutter_udid: ^2.0.0
 | 
				
			||||||
@@ -41,6 +40,7 @@ dependencies:
 | 
				
			|||||||
  collection: ^1.16.0
 | 
					  collection: ^1.16.0
 | 
				
			||||||
  http_parser: ^4.0.1
 | 
					  http_parser: ^4.0.1
 | 
				
			||||||
  flutter_web_auth: ^0.5.0
 | 
					  flutter_web_auth: ^0.5.0
 | 
				
			||||||
 | 
					  easy_image_viewer: ^1.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  openapi:
 | 
					  openapi:
 | 
				
			||||||
    path: openapi
 | 
					    path: openapi
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user