mirror of
https://github.com/immich-app/immich.git
synced 2025-01-02 12:48:35 +02:00
Allow zooming in image viewer (#227)
* Allow zooming in image viewer * Use thumbnailProvider as initial provider * Set maximum zoom level to 100% * Implement custom swipe listener in remote_photo_view * Dart format * Disable swipe gestures when zoomed in (prevents panning)
This commit is contained in:
parent
8840911f22
commit
34657f820f
114
mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart
Normal file
114
mobile/lib/modules/asset_viewer/ui/remote_photo_view.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
enum _RemoteImageStatus { empty, thumbnail, full }
|
||||||
|
|
||||||
|
class _RemotePhotoViewState extends State<RemotePhotoView> {
|
||||||
|
late CachedNetworkImageProvider _imageProvider;
|
||||||
|
_RemoteImageStatus _status = _RemoteImageStatus.empty;
|
||||||
|
bool _zoomedIn = false;
|
||||||
|
|
||||||
|
static const int swipeThreshold = 100;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool allowMoving = _status == _RemoteImageStatus.full;
|
||||||
|
|
||||||
|
return PhotoView(
|
||||||
|
imageProvider: _imageProvider,
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
||||||
|
enablePanAlways: true,
|
||||||
|
scaleStateChangedCallback: _scaleStateChanged,
|
||||||
|
onScaleEnd: _onScaleListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScaleListener(BuildContext context, ScaleEndDetails details,
|
||||||
|
PhotoViewControllerValue controllerValue) {
|
||||||
|
// Disable swipe events when zoomed in
|
||||||
|
if (_zoomedIn) return;
|
||||||
|
|
||||||
|
if (controllerValue.position.dy > swipeThreshold) {
|
||||||
|
widget.onSwipeDown();
|
||||||
|
} else if (controllerValue.position.dy < -swipeThreshold) {
|
||||||
|
widget.onSwipeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scaleStateChanged(PhotoViewScaleState state) {
|
||||||
|
_zoomedIn = state == PhotoViewScaleState.zoomedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
||||||
|
return CachedNetworkImageProvider(url,
|
||||||
|
headers: {"Authorization": widget.authToken}, cacheKey: url);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _performStateTransition(
|
||||||
|
_RemoteImageStatus newStatus, CachedNetworkImageProvider provider) {
|
||||||
|
// Transition to same status is forbidden
|
||||||
|
if (_status == newStatus) return;
|
||||||
|
// Transition full -> thumbnail is forbidden
|
||||||
|
if (_status == _RemoteImageStatus.full &&
|
||||||
|
newStatus == _RemoteImageStatus.thumbnail) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_status = newStatus;
|
||||||
|
_imageProvider = provider;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadImages() {
|
||||||
|
CachedNetworkImageProvider thumbnailProvider =
|
||||||
|
_authorizedImageProvider(widget.thumbnailUrl);
|
||||||
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
|
thumbnailProvider
|
||||||
|
.resolve(const ImageConfiguration())
|
||||||
|
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
|
_performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider);
|
||||||
|
}));
|
||||||
|
|
||||||
|
CachedNetworkImageProvider fullProvider =
|
||||||
|
_authorizedImageProvider(widget.imageUrl);
|
||||||
|
fullProvider
|
||||||
|
.resolve(const ImageConfiguration())
|
||||||
|
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
|
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_loadImages();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemotePhotoView extends StatefulWidget {
|
||||||
|
const RemotePhotoView(
|
||||||
|
{Key? key,
|
||||||
|
required this.thumbnailUrl,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.authToken,
|
||||||
|
required this.onSwipeDown,
|
||||||
|
required this.onSwipeUp})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
final String thumbnailUrl;
|
||||||
|
final String imageUrl;
|
||||||
|
final String authToken;
|
||||||
|
|
||||||
|
final void Function() onSwipeDown;
|
||||||
|
final void Function() onSwipeUp;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _RemotePhotoViewState();
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
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_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
@ -10,6 +8,7 @@ import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_stat
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.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/remote_photo_view.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/home/services/asset.service.dart';
|
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
@ -63,64 +62,19 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SwipeDetector(
|
body: SafeArea(
|
||||||
onSwipeDown: (_) {
|
|
||||||
AutoRouter.of(context).pop();
|
|
||||||
},
|
|
||||||
onSwipeUp: (_) {
|
|
||||||
showInfo();
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: CachedNetworkImage(
|
child: RemotePhotoView(
|
||||||
fit: BoxFit.cover,
|
thumbnailUrl: thumbnailUrl,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
authToken: "Bearer ${box.get(accessTokenKey)}",
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
errorWidget: (context, url, error) => ConstrainedBox(
|
onSwipeUp: () => showInfo(),
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
)
|
||||||
child: Wrap(
|
|
||||||
spacing: 32,
|
|
||||||
runSpacing: 32,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"Failed To Render Image - Possibly Corrupted Data",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
|
||||||
),
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Text(
|
|
||||||
error.toString(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
placeholder: (context, url) {
|
|
||||||
return CachedNetworkImage(
|
|
||||||
cacheKey: thumbnailUrl,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
imageUrl: thumbnailUrl,
|
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
|
||||||
placeholderFadeInDuration: const Duration(milliseconds: 0),
|
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
|
|
||||||
scale: 0.2,
|
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
|
||||||
),
|
|
||||||
errorWidget: (context, url, error) => Icon(
|
|
||||||
Icons.error,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
@ -130,7 +84,6 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -757,7 +757,7 @@ packages:
|
|||||||
name: photo_view
|
name: photo_view
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.14.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -30,7 +30,7 @@ dependencies:
|
|||||||
chewie: ^1.2.2
|
chewie: ^1.2.2
|
||||||
sliver_tools: ^0.2.5
|
sliver_tools: ^0.2.5
|
||||||
badges: ^2.0.2
|
badges: ^2.0.2
|
||||||
photo_view: ^0.13.0
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user