mirror of
https://github.com/immich-app/immich.git
synced 2026-05-14 11:32:15 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed06dc40d1 | |||
| 14193b82df | |||
| 5271863291 | |||
| 6b3e07dc52 |
@@ -4,7 +4,6 @@ enum Setting<T> {
|
||||
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
|
||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
|
||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
||||
loadPreview<bool>(StoreKey.loadPreview, true),
|
||||
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
|
||||
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
|
||||
|
||||
@@ -49,6 +49,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
|
||||
bool _showingDetails = false;
|
||||
bool _isZoomed = false;
|
||||
// Frozen during dismiss drag + settle to prevent widget tree swap mid-animation.
|
||||
bool _frozenMotionPlaying = false;
|
||||
bool _dismissSettling = false;
|
||||
|
||||
final _scrollController = SnapScrollController();
|
||||
double _snapOffset = 0.0;
|
||||
@@ -136,6 +139,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
> 0 => _DragIntent.dismiss,
|
||||
_ => _DragIntent.none,
|
||||
};
|
||||
if (_dragIntent == _DragIntent.dismiss) {
|
||||
_frozenMotionPlaying = ref.read(isPlayingMotionVideoProvider);
|
||||
}
|
||||
}
|
||||
|
||||
switch (_dragIntent) {
|
||||
@@ -173,12 +179,18 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
context.maybePop();
|
||||
return;
|
||||
}
|
||||
_viewController?.animateMultiple(
|
||||
position: _initialPhotoViewState.position,
|
||||
scale: _viewController?.initialScale ?? _initialPhotoViewState.scale,
|
||||
rotation: _initialPhotoViewState.rotation,
|
||||
);
|
||||
_viewer.setOpacity(1.0);
|
||||
_dismissSettling = true;
|
||||
_viewController
|
||||
?.animateMultiple(
|
||||
position: _initialPhotoViewState.position,
|
||||
scale: _viewController?.initialScale ?? _initialPhotoViewState.scale,
|
||||
rotation: _initialPhotoViewState.rotation,
|
||||
)
|
||||
.whenComplete(() {
|
||||
if (!mounted) return;
|
||||
setState(() => _dismissSettling = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +368,10 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
final currentHeroTag = ref.watch(assetViewerProvider.select((s) => s.currentAsset?.heroTag));
|
||||
_showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
|
||||
final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex));
|
||||
final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider);
|
||||
final liveMotionPlaying = ref.watch(isPlayingMotionVideoProvider);
|
||||
final isPlayingMotionVideo = (_dragIntent == _DragIntent.dismiss || _dismissSettling)
|
||||
? _frozenMotionPlaying
|
||||
: liveMotionPlaying;
|
||||
|
||||
final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index);
|
||||
if (asset == null) {
|
||||
|
||||
@@ -45,6 +45,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
|
||||
completer.operation.valueOrCancellation().whenComplete(() {
|
||||
cachedStream.removeListener(listener);
|
||||
cachedOperation = null;
|
||||
});
|
||||
cachedOperation = completer.operation;
|
||||
return null;
|
||||
@@ -105,33 +106,18 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
|
||||
}
|
||||
}
|
||||
|
||||
Stream<ImageInfo> initialImageStream({required bool isFinal}) async* {
|
||||
Stream<ImageInfo> initialImageStream() async* {
|
||||
final cachedOperation = this.cachedOperation;
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
if (cachedOperation == null) {
|
||||
// image resolved synchronously
|
||||
isFinished = isFinal;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final cachedImage = await cachedOperation.valueOrCancellation();
|
||||
if (isCancelled || cachedImage == null) {
|
||||
return;
|
||||
if (cachedImage != null && !isCancelled) {
|
||||
yield cachedImage;
|
||||
}
|
||||
isFinished = isFinal;
|
||||
yield cachedImage;
|
||||
} catch (e, stack) {
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
if (isFinal) {
|
||||
isFinished = true;
|
||||
PaintingBinding.instance.imageCache.evict(this);
|
||||
rethrow;
|
||||
}
|
||||
_log.severe('Error loading initial image', e, stack);
|
||||
} finally {
|
||||
this.cachedOperation = null;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
@@ -97,55 +97,51 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
||||
}
|
||||
|
||||
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
final loadOriginal = AppSetting.get(Setting.loadOriginal);
|
||||
final loadPreview = AppSetting.get(Setting.loadPreview);
|
||||
yield* initialImageStream(isFinal: !loadOriginal && !loadPreview);
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadPreview) {
|
||||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
final previewRequest = request = LocalImageRequest(
|
||||
localId: key.id,
|
||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||
assetType: key.assetType,
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
final loadOriginal = Store.get(StoreKey.loadOriginal, false);
|
||||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
var request = this.request = LocalImageRequest(
|
||||
localId: key.id,
|
||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||
assetType: key.assetType,
|
||||
);
|
||||
yield* loadRequest(request, decode, isFinal: !loadOriginal);
|
||||
|
||||
if (!loadOriginal) {
|
||||
return;
|
||||
}
|
||||
|
||||
final originalRequest = request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
|
||||
yield* loadRequest(originalRequest, decode, isFinal: true);
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
|
||||
|
||||
yield* loadRequest(request, decode, isFinal: true);
|
||||
}
|
||||
|
||||
Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
yield* initialImageStream(isFinal: false);
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppSetting.get(Setting.loadPreview)) {
|
||||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
final previewRequest = request = LocalImageRequest(
|
||||
localId: key.id,
|
||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||
assetType: key.assetType,
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: false);
|
||||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
final previewRequest = request = LocalImageRequest(
|
||||
localId: key.id,
|
||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||
assetType: key.assetType,
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: false);
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// always try original for animated, since previews don't support animation
|
||||
|
||||
@@ -107,35 +107,31 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
}
|
||||
|
||||
Stream<ImageInfo> _codec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
final isImage = assetType == AssetType.image;
|
||||
final loadOriginal = isImage && AppSetting.get(Setting.loadOriginal);
|
||||
final loadPreview = isImage && AppSetting.get(Setting.loadPreview);
|
||||
yield* initialImageStream(isFinal: !loadOriginal && !loadPreview);
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadPreview) {
|
||||
final previewRequest = request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
type: AssetMediaSize.preview,
|
||||
thumbhash: key.thumbhash,
|
||||
edited: key.edited,
|
||||
),
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
final previewRequest = request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
type: AssetMediaSize.preview,
|
||||
thumbhash: key.thumbhash,
|
||||
edited: key.edited,
|
||||
),
|
||||
);
|
||||
final loadOriginal = assetType == AssetType.image && AppSetting.get(Setting.loadOriginal);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
|
||||
|
||||
if (!loadOriginal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final originalRequest = request = RemoteImageRequest(
|
||||
uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited),
|
||||
);
|
||||
@@ -143,26 +139,24 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
|
||||
}
|
||||
|
||||
Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
yield* initialImageStream(isFinal: false);
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppSetting.get(Setting.loadPreview)) {
|
||||
final previewRequest = request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
type: AssetMediaSize.preview,
|
||||
thumbhash: key.thumbhash,
|
||||
edited: key.edited,
|
||||
),
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: false);
|
||||
final previewRequest = request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
type: AssetMediaSize.preview,
|
||||
thumbhash: key.thumbhash,
|
||||
edited: key.edited,
|
||||
),
|
||||
);
|
||||
yield* loadRequest(previewRequest, decode, isFinal: false);
|
||||
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// always try original for animated, since previews don't support animation
|
||||
|
||||
@@ -38,12 +38,13 @@ abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
|
||||
/// Closes streams and removes eventual listeners.
|
||||
void dispose();
|
||||
|
||||
void positionAnimationBuilder(void Function(Offset)? value);
|
||||
void scaleAnimationBuilder(void Function(double)? value);
|
||||
void rotationAnimationBuilder(void Function(double)? value);
|
||||
void positionAnimationBuilder(Future<void> Function(Offset)? value);
|
||||
void scaleAnimationBuilder(Future<void> Function(double)? value);
|
||||
void rotationAnimationBuilder(Future<void> Function(double)? value);
|
||||
|
||||
/// Animates multiple fields of the state
|
||||
void animateMultiple({Offset? position, double? scale, double? rotation});
|
||||
/// Animates multiple fields of the state. The returned future completes
|
||||
/// when all underlying animations have settled.
|
||||
Future<void> animateMultiple({Offset? position, double? scale, double? rotation});
|
||||
|
||||
/// Add a listener that will ignore updates made internally
|
||||
///
|
||||
@@ -148,9 +149,9 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
|
||||
@override
|
||||
ScaleBoundaries? scaleBoundaries;
|
||||
|
||||
late void Function(Offset)? _animatePosition;
|
||||
late void Function(double)? _animateScale;
|
||||
late void Function(double)? _animateRotation;
|
||||
late Future<void> Function(Offset)? _animatePosition;
|
||||
late Future<void> Function(double)? _animateScale;
|
||||
late Future<void> Function(double)? _animateRotation;
|
||||
|
||||
@override
|
||||
Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream;
|
||||
@@ -159,17 +160,17 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
|
||||
late PhotoViewControllerValue prevValue;
|
||||
|
||||
@override
|
||||
void positionAnimationBuilder(void Function(Offset)? value) {
|
||||
void positionAnimationBuilder(Future<void> Function(Offset)? value) {
|
||||
_animatePosition = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void scaleAnimationBuilder(void Function(double)? value) {
|
||||
void scaleAnimationBuilder(Future<void> Function(double)? value) {
|
||||
_animateScale = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void rotationAnimationBuilder(void Function(double)? value) {
|
||||
void rotationAnimationBuilder(Future<void> Function(double)? value) {
|
||||
_animateRotation = value;
|
||||
}
|
||||
|
||||
@@ -193,18 +194,18 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
|
||||
}
|
||||
|
||||
@override
|
||||
void animateMultiple({Offset? position, double? scale, double? rotation}) {
|
||||
Future<void> animateMultiple({Offset? position, double? scale, double? rotation}) {
|
||||
final futures = <Future<void>>[];
|
||||
if (position != null && _animatePosition != null) {
|
||||
_animatePosition!(position);
|
||||
futures.add(_animatePosition!(position));
|
||||
}
|
||||
|
||||
if (scale != null && _animateScale != null) {
|
||||
_animateScale!(scale);
|
||||
futures.add(_animateScale!(scale));
|
||||
}
|
||||
|
||||
if (rotation != null && _animateRotation != null) {
|
||||
_animateRotation!(rotation);
|
||||
futures.add(_animateRotation!(rotation));
|
||||
}
|
||||
return Future.wait(futures);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -237,34 +237,31 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||
nextScaleState();
|
||||
}
|
||||
|
||||
void animateScale(double from, double to) {
|
||||
Future<void> animateScale(double from, double to) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
return Future.value();
|
||||
}
|
||||
_scaleAnimation = Tween<double>(begin: from, end: to).animate(_scaleAnimationController);
|
||||
_scaleAnimationController
|
||||
..value = 0.0
|
||||
..fling(velocity: 0.4);
|
||||
_scaleAnimationController.value = 0.0;
|
||||
return _scaleAnimationController.fling(velocity: 0.4);
|
||||
}
|
||||
|
||||
void animatePosition(Offset from, Offset to) {
|
||||
Future<void> animatePosition(Offset from, Offset to) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
return Future.value();
|
||||
}
|
||||
_positionAnimation = Tween<Offset>(begin: from, end: to).animate(_positionAnimationController);
|
||||
_positionAnimationController
|
||||
..value = 0.0
|
||||
..fling(velocity: 0.4);
|
||||
_positionAnimationController.value = 0.0;
|
||||
return _positionAnimationController.fling(velocity: 0.4);
|
||||
}
|
||||
|
||||
void animateRotation(double from, double to) {
|
||||
Future<void> animateRotation(double from, double to) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
return Future.value();
|
||||
}
|
||||
_rotationAnimation = Tween<double>(begin: from, end: to).animate(_rotationAnimationController);
|
||||
_rotationAnimationController
|
||||
..value = 0.0
|
||||
..fling(velocity: 0.4);
|
||||
_rotationAnimationController.value = 0.0;
|
||||
return _rotationAnimationController.fling(velocity: 0.4);
|
||||
}
|
||||
|
||||
void onAnimationStatus(AnimationStatus status) {
|
||||
@@ -280,18 +277,19 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
||||
}
|
||||
}
|
||||
|
||||
void _animateControllerPosition(Offset position) {
|
||||
animatePosition(controller.position, position);
|
||||
Future<void> _animateControllerPosition(Offset position) {
|
||||
return animatePosition(controller.position, position);
|
||||
}
|
||||
|
||||
void _animateControllerScale(double scale) {
|
||||
Future<void> _animateControllerScale(double scale) {
|
||||
if (controller.scale != null) {
|
||||
animateScale(controller.scale!, scale);
|
||||
return animateScale(controller.scale!, scale);
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
void _animateControllerRotation(double rotation) {
|
||||
animateRotation(controller.rotation, rotation);
|
||||
Future<void> _animateControllerRotation(double rotation) {
|
||||
return animateRotation(controller.rotation, rotation);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user