Compare commits

...

7 commits

7 changed files with 198 additions and 68 deletions

View file

@ -244,7 +244,7 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
void handleSwipeUpDown(DragUpdateDetails details) {
void handleDragUpdate(Asset asset, DragUpdateDetails details) {
int sensitivity = 15;
int dxThreshold = 50;
@ -373,6 +373,33 @@ class GalleryViewerPage extends HookConsumerWidget {
);
}
/// The image provider to use for the given asset
ImageProvider imageProvider(Asset asset) {
// Local assets
if (asset.isLocal) {
return localImageProvider(asset);
}
// Original assets
if (isLoadOriginal.value) {
return originalImageProvider(asset);
}
// Preview uses remote JPEG
if (isLoadPreview.value) {
return remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.JPEG,
);
}
// Otherwise, use the thumbnail preview
return remoteThumbnailImageProvider(
asset,
api.ThumbnailFormat.WEBP,
);
}
return Scaffold(
backgroundColor: Colors.black,
body: WillPopScope(
@ -466,31 +493,15 @@ class GalleryViewerPage extends HookConsumerWidget {
: null,
builder: (context, index) {
getAssetExif();
// Create image provider
final ImageProvider provider = imageProvider(assetList[index]);
if (assetList[index].isImage && !isPlayingMotionVideo.value) {
// Show photo
final ImageProvider provider;
if (assetList[index].isLocal) {
provider = localImageProvider(assetList[index]);
} else {
if (isLoadOriginal.value) {
provider = originalImageProvider(assetList[index]);
} else if (isLoadPreview.value) {
provider = remoteThumbnailImageProvider(
assetList[index],
api.ThumbnailFormat.JPEG,
);
} else {
provider = remoteThumbnailImageProvider(
assetList[index],
api.ThumbnailFormat.WEBP,
);
}
}
return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) =>
localPosition = details.localPosition,
onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details),
handleDragUpdate(assetList[index], details),
onTapDown: (_, __, ___) =>
showAppBar.value = !showAppBar.value,
imageProvider: provider,
@ -510,7 +521,7 @@ class GalleryViewerPage extends HookConsumerWidget {
onDragStart: (_, details, __) =>
localPosition = details.localPosition,
onDragUpdate: (_, details, __) =>
handleSwipeUpDown(details),
handleDragUpdate(assetList[index], details),
heroAttributes: PhotoViewHeroAttributes(
tag: assetList[index].id,
),
@ -518,23 +529,47 @@ class GalleryViewerPage extends HookConsumerWidget {
maxScale: 1.0,
minScale: 1.0,
basePosition: Alignment.bottomCenter,
child: SafeArea(
child: VideoViewerPage(
onPlaying: () => isPlayingVideo.value = true,
onPaused: () => isPlayingVideo.value = false,
asset: assetList[index],
isMotionVideo: isPlayingMotionVideo.value,
onVideoEnded: () {
if (isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
child: VideoViewerPage(
onPlaying: () => isPlayingVideo.value = true,
onPaused: () => isPlayingVideo.value = false,
asset: assetList[index],
isMotionVideo: isPlayingMotionVideo.value,
placeholder: Image(
image: provider,
fit: BoxFit.fitWidth,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
),
onVideoEnded: () {
if (isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
),
);
}
},
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onLongPressStart: (_) {
// If we are a motion video and we are not playing, start playing
if (assetList[indexOfAsset.value].livePhotoVideoId != null &&
!isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = true;
}
},
onLongPressEnd: (_) {
// Stop playing the motion video if this is a video and we are
// playing
if (assetList[indexOfAsset.value].livePhotoVideoId != null &&
isPlayingMotionVideo.value) {
isPlayingMotionVideo.value = false;
}
},
child: const SizedBox.expand(),
),
Positioned(
top: 0,
left: 0,

View file

@ -15,6 +15,7 @@ import 'package:video_player/video_player.dart';
class VideoViewerPage extends HookConsumerWidget {
final Asset asset;
final bool isMotionVideo;
final Widget? placeholder;
final VoidCallback onVideoEnded;
final VoidCallback? onPlaying;
final VoidCallback? onPaused;
@ -26,6 +27,7 @@ class VideoViewerPage extends HookConsumerWidget {
required this.onVideoEnded,
this.onPlaying,
this.onPaused,
this.placeholder,
}) : super(key: key);
@override
@ -66,6 +68,7 @@ class VideoViewerPage extends HookConsumerWidget {
onVideoEnded: onVideoEnded,
onPaused: onPaused,
onPlaying: onPlaying,
placeholder: placeholder,
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
@ -95,6 +98,10 @@ class VideoPlayer extends StatefulWidget {
final Function()? onPlaying;
final Function()? onPaused;
/// The placeholder to show while the video is loading usually, a thumbnail
/// of the video
final Widget? placeholder;
const VideoPlayer({
Key? key,
this.url,
@ -104,6 +111,7 @@ class VideoPlayer extends StatefulWidget {
required this.isMotionVideo,
this.onPlaying,
this.onPaused,
this.placeholder,
}) : super(key: key);
@override
@ -186,12 +194,33 @@ class _VideoPlayerState extends State<VideoPlayer> {
),
);
} else {
return const Center(
child: SizedBox(
width: 75,
height: 75,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
return SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Center(
child: Stack(
children: [
if (widget.placeholder != null) widget.placeholder!,
FutureBuilder(
future: Future.delayed(const Duration(seconds: 1)),
builder: (context, snapshot) {
// If we have to wait for over 1 second to start the video
// then show the loading indicator
final loading =
(snapshot.connectionState == ConnectionState.done)
? const Center(
child: ImmichLoadingIndicator(),
)
: Container();
// Fade in the loading animation
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: loading,
);
},
),
],
),
),
);

View file

@ -253,6 +253,8 @@ class PhotoView extends StatefulWidget {
this.onTapDown,
this.onDragStart,
this.onDragEnd,
this.onLongPressStart,
this.onLongPressEnd,
this.onDragUpdate,
this.onScaleEnd,
this.customSize,
@ -293,6 +295,8 @@ class PhotoView extends StatefulWidget {
this.onDragStart,
this.onDragEnd,
this.onDragUpdate,
this.onLongPressStart,
this.onLongPressEnd,
this.onScaleEnd,
this.customSize,
this.gestureDetectorBehavior,
@ -397,6 +401,12 @@ class PhotoView extends StatefulWidget {
/// location.
final PhotoViewImageDragUpdateCallback? onDragUpdate;
/// A long press start event
final PhotoViewImageLongPressStartCallback? onLongPressStart;
/// A long press end event
final PhotoViewImageLongPressEndCallback? onLongPressEnd;
/// A pointer that will trigger a scale has stopped contacting the screen at a
/// particular location.
final PhotoViewImageScaleEndCallback? onScaleEnd;
@ -534,6 +544,8 @@ class _PhotoViewState extends State<PhotoView>
onDragStart: widget.onDragStart,
onDragEnd: widget.onDragEnd,
onDragUpdate: widget.onDragUpdate,
onLongPressStart: widget.onLongPressStart,
onLongPressEnd: widget.onLongPressEnd,
onScaleEnd: widget.onScaleEnd,
outerSize: computedOuterSize,
gestureDetectorBehavior: widget.gestureDetectorBehavior,
@ -563,6 +575,8 @@ class _PhotoViewState extends State<PhotoView>
onDragStart: widget.onDragStart,
onDragEnd: widget.onDragEnd,
onDragUpdate: widget.onDragUpdate,
onLongPressStart: widget.onLongPressStart,
onLongPressEnd: widget.onLongPressEnd,
onScaleEnd: widget.onScaleEnd,
outerSize: computedOuterSize,
gestureDetectorBehavior: widget.gestureDetectorBehavior,
@ -625,7 +639,7 @@ typedef PhotoViewImageDragStartCallback = Function(
PhotoViewControllerValue controllerValue,
);
/// A type definition for a callback when the user drags
/// A type definition for a callback when the user drags
typedef PhotoViewImageDragUpdateCallback = Function(
BuildContext context,
DragUpdateDetails details,
@ -646,6 +660,20 @@ typedef PhotoViewImageScaleEndCallback = Function(
PhotoViewControllerValue controllerValue,
);
/// A type definition for a callback when the user starts a long press
typedef PhotoViewImageLongPressStartCallback = Function(
BuildContext context,
LongPressStartDetails details,
PhotoViewControllerValue controllerValue,
);
/// A type definition for a callback when the user starts a long press
typedef PhotoViewImageLongPressEndCallback = Function(
BuildContext context,
LongPressEndDetails 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,

View file

@ -3,16 +3,7 @@ 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;
show LoadingBuilder, PhotoView, PhotoViewImageDragEndCallback, PhotoViewImageDragStartCallback, PhotoViewImageDragUpdateCallback, PhotoViewImageLongPressEndCallback, PhotoViewImageLongPressStartCallback, PhotoViewImageScaleEndCallback, PhotoViewImageTapDownCallback, PhotoViewImageTapUpCallback, 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';
@ -25,7 +16,7 @@ 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,
BuildContext context,
int index,
);
@ -270,6 +261,8 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
onDragStart: pageOption.onDragStart,
onDragEnd: pageOption.onDragEnd,
onDragUpdate: pageOption.onDragUpdate,
onLongPressStart: pageOption.onLongPressStart,
onLongPressEnd: pageOption.onLongPressEnd,
onScaleEnd: pageOption.onScaleEnd,
gestureDetectorBehavior: pageOption.gestureDetectorBehavior,
tightMode: pageOption.tightMode,
@ -299,6 +292,8 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
onDragStart: pageOption.onDragStart,
onDragEnd: pageOption.onDragEnd,
onDragUpdate: pageOption.onDragUpdate,
onLongPressStart: pageOption.onLongPressStart,
onLongPressEnd: pageOption.onLongPressEnd,
onScaleEnd: pageOption.onScaleEnd,
gestureDetectorBehavior: pageOption.gestureDetectorBehavior,
tightMode: pageOption.tightMode,
@ -355,6 +350,8 @@ class PhotoViewGalleryPageOptions {
this.onDragStart,
this.onDragEnd,
this.onDragUpdate,
this.onLongPressStart,
this.onLongPressEnd,
this.onScaleEnd,
this.gestureDetectorBehavior,
this.tightMode,
@ -381,6 +378,8 @@ class PhotoViewGalleryPageOptions {
this.onDragStart,
this.onDragEnd,
this.onDragUpdate,
this.onLongPressStart,
this.onLongPressEnd,
this.onScaleEnd,
this.gestureDetectorBehavior,
this.tightMode,
@ -431,9 +430,15 @@ class PhotoViewGalleryPageOptions {
/// Mirror to [PhotoView.onDragDown]
final PhotoViewImageDragEndCallback? onDragEnd;
/// Mirror to [PhotoView.onDraUpdate]
/// Mirror to [PhotoView.onDragUpdate]
final PhotoViewImageDragUpdateCallback? onDragUpdate;
/// Mirror to [PhotoView.onLongPressStart]
final PhotoViewImageLongPressStartCallback? onLongPressStart;
/// Mirror to [PhotoView.onLongPressStart]
final PhotoViewImageLongPressEndCallback? onLongPressEnd;
/// Mirror to [PhotoView.onTapDown]
final PhotoViewImageTapDownCallback? onTapDown;

View file

@ -1,15 +1,6 @@
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;
show PhotoViewHeroAttributes, PhotoViewImageDragEndCallback, PhotoViewImageDragStartCallback, PhotoViewImageDragUpdateCallback, PhotoViewImageLongPressEndCallback, PhotoViewImageLongPressStartCallback, PhotoViewImageScaleEndCallback, PhotoViewImageTapDownCallback, PhotoViewImageTapUpCallback, PhotoViewScaleState, 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';
@ -36,6 +27,8 @@ class PhotoViewCore extends StatefulWidget {
required this.onDragStart,
required this.onDragEnd,
required this.onDragUpdate,
required this.onLongPressStart,
required this.onLongPressEnd,
required this.onScaleEnd,
required this.gestureDetectorBehavior,
required this.controller,
@ -61,6 +54,8 @@ class PhotoViewCore extends StatefulWidget {
this.onDragStart,
this.onDragEnd,
this.onDragUpdate,
this.onLongPressStart,
this.onLongPressEnd,
this.onScaleEnd,
this.gestureDetectorBehavior,
required this.controller,
@ -97,6 +92,9 @@ class PhotoViewCore extends StatefulWidget {
final PhotoViewImageDragEndCallback? onDragEnd;
final PhotoViewImageDragUpdateCallback? onDragUpdate;
final PhotoViewImageLongPressStartCallback? onLongPressStart;
final PhotoViewImageLongPressEndCallback? onLongPressEnd;
final HitTestBehavior? gestureDetectorBehavior;
final bool tightMode;
final bool disableGestures;
@ -359,13 +357,13 @@ class PhotoViewCoreState extends State<PhotoViewCore>
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onDragStart: widget.onDragStart != null
onDragStart: widget.onDragStart != null
? (details) => widget.onDragStart!(context, details, value)
: null,
onDragEnd: widget.onDragEnd != null
onDragEnd: widget.onDragEnd != null
? (details) => widget.onDragEnd!(context, details, value)
: null,
onDragUpdate: widget.onDragUpdate != null
onDragUpdate: widget.onDragUpdate != null
? (details) => widget.onDragUpdate!(context, details, value)
: null,
hitDetector: this,
@ -375,6 +373,12 @@ class PhotoViewCoreState extends State<PhotoViewCore>
onTapDown: widget.onTapDown != null
? (details) => widget.onTapDown!(context, details, value)
: null,
onLongPressStart: widget.onLongPressStart != null
? (details) => widget.onLongPressStart!(context, details, value)
: null,
onLongPressEnd: widget.onLongPressEnd != null
? (details) => widget.onLongPressEnd!(context, details, value)
: null,
child: child,
);
} else {

View file

@ -16,6 +16,8 @@ class PhotoViewGestureDetector extends StatelessWidget {
this.onDragStart,
this.onDragEnd,
this.onDragUpdate,
this.onLongPressStart,
this.onLongPressEnd,
this.child,
this.onTapUp,
this.onTapDown,
@ -36,6 +38,9 @@ class PhotoViewGestureDetector extends StatelessWidget {
final GestureTapUpCallback? onTapUp;
final GestureTapDownCallback? onTapDown;
final GestureLongPressStartCallback? onLongPressStart;
final GestureLongPressEndCallback? onLongPressEnd;
final Widget? child;
final HitTestBehavior? behavior;
@ -63,7 +68,7 @@ class PhotoViewGestureDetector extends StatelessWidget {
}
if (onDragStart != null || onDragEnd != null || onDragUpdate != null) {
gestures[VerticalDragGestureRecognizer] =
gestures[VerticalDragGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(debugOwner: this),
(VerticalDragGestureRecognizer instance) {
@ -75,6 +80,18 @@ class PhotoViewGestureDetector extends StatelessWidget {
);
}
if (onLongPressStart != null || onLongPressEnd != null) {
gestures[LongPressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPressStart = onLongPressStart
..onLongPressEnd = onLongPressEnd;
},
);
}
gestures[DoubleTapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(debugOwner: this),
@ -236,7 +253,7 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer {
/// ```
class PhotoViewGestureDetectorScope extends InheritedWidget {
const PhotoViewGestureDetectorScope({
super.key,
super.key,
this.axis,
this.touchSlopFactor = .2,
required Widget child,
@ -254,7 +271,7 @@ class PhotoViewGestureDetectorScope extends InheritedWidget {
// 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;
final double touchSlopFactor;
@override
bool updateShouldNotify(PhotoViewGestureDetectorScope oldWidget) {

View file

@ -27,6 +27,8 @@ class ImageWrapper extends StatefulWidget {
required this.onDragStart,
required this.onDragEnd,
required this.onDragUpdate,
required this.onLongPressStart,
required this.onLongPressEnd,
required this.onScaleEnd,
required this.outerSize,
required this.gestureDetectorBehavior,
@ -57,6 +59,8 @@ class ImageWrapper extends StatefulWidget {
final PhotoViewImageDragStartCallback? onDragStart;
final PhotoViewImageDragEndCallback? onDragEnd;
final PhotoViewImageDragUpdateCallback? onDragUpdate;
final PhotoViewImageLongPressStartCallback? onLongPressStart;
final PhotoViewImageLongPressEndCallback? onLongPressEnd;
final PhotoViewImageScaleEndCallback? onScaleEnd;
final Size outerSize;
final HitTestBehavior? gestureDetectorBehavior;
@ -201,6 +205,8 @@ class _ImageWrapperState extends State<ImageWrapper> {
onDragStart: widget.onDragStart,
onDragEnd: widget.onDragEnd,
onDragUpdate: widget.onDragUpdate,
onLongPressStart: widget.onLongPressStart,
onLongPressEnd: widget.onLongPressEnd,
onScaleEnd: widget.onScaleEnd,
gestureDetectorBehavior: widget.gestureDetectorBehavior,
tightMode: widget.tightMode ?? false,
@ -253,6 +259,8 @@ class CustomChildWrapper extends StatelessWidget {
this.onDragStart,
this.onDragEnd,
this.onDragUpdate,
this.onLongPressStart,
this.onLongPressEnd,
this.onScaleEnd,
required this.outerSize,
this.gestureDetectorBehavior,
@ -283,6 +291,8 @@ class CustomChildWrapper extends StatelessWidget {
final PhotoViewImageDragStartCallback? onDragStart;
final PhotoViewImageDragEndCallback? onDragEnd;
final PhotoViewImageDragUpdateCallback? onDragUpdate;
final PhotoViewImageLongPressStartCallback? onLongPressStart;
final PhotoViewImageLongPressEndCallback? onLongPressEnd;
final PhotoViewImageScaleEndCallback? onScaleEnd;
final Size outerSize;
final HitTestBehavior? gestureDetectorBehavior;
@ -316,6 +326,8 @@ class CustomChildWrapper extends StatelessWidget {
onDragStart: onDragStart,
onDragEnd: onDragEnd,
onDragUpdate: onDragUpdate,
onLongPressEnd: onLongPressEnd,
onLongPressStart: onLongPressStart,
onScaleEnd: onScaleEnd,
gestureDetectorBehavior: gestureDetectorBehavior,
tightMode: tightMode ?? false,