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/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'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; class LocalThumbProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String id; final Size size; final AssetType assetType; LocalThumbProvider({required this.id, required this.assetType, this.size = kThumbnailResolution}); @override Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); } @override ImageStreamCompleter loadImage(LocalThumbProvider key, ImageDecoderCallback decode) { return OneFramePlaceholderImageStreamCompleter( _codec(key, decode), informationCollector: () => [ DiagnosticsProperty('Id', key.id), DiagnosticsProperty('Size', key.size), ], onLastListenerRemoved: cancel, ); } Stream _codec(LocalThumbProvider key, ImageDecoderCallback decode) { final request = this.request = LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType); return loadRequest(request, decode); } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is LocalThumbProvider) { return id == other.id; } return false; } @override int get hashCode => id.hashCode; } class LocalFullImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { final String id; final Size size; final AssetType assetType; final bool isAnimated; LocalFullImageProvider({required this.id, required this.assetType, required this.size, required this.isAnimated}); @override Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); } @override ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) { if (key.isAnimated) { return AnimatedImageStreamCompleter( stream: _animatedCodec(key, decode), scale: 1.0, initialImage: getInitialImage(LocalThumbProvider(id: key.id, assetType: key.assetType)), informationCollector: () => [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Id', key.id), DiagnosticsProperty('Size', key.size), DiagnosticsProperty('isAnimated', key.isAnimated), ], onLastListenerRemoved: cancel, ); } return OneFramePlaceholderImageStreamCompleter( _codec(key, decode), initialImage: getInitialImage(LocalThumbProvider(id: key.id, assetType: key.assetType)), informationCollector: () => [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Id', key.id), DiagnosticsProperty('Size', key.size), DiagnosticsProperty('isAnimated', key.isAnimated), ], onLastListenerRemoved: cancel, ); } Stream _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { yield* initialImageStream(); if (isCancelled) { PaintingBinding.instance.imageCache.evict(this); return; } 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); if (!Store.get(StoreKey.loadOriginal, false)) { return; } if (isCancelled) { PaintingBinding.instance.imageCache.evict(this); return; } request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero); yield* loadRequest(request, decode); } Stream _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { yield* initialImageStream(); if (isCancelled) { PaintingBinding.instance.imageCache.evict(this); return; } 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); if (isCancelled) { PaintingBinding.instance.imageCache.evict(this); return; } // always try original for animated, since previews don't support animation final originalRequest = request = LocalImageRequest(localId: key.id, size: Size.zero, assetType: key.assetType); final codec = await loadCodecRequest(originalRequest); if (codec == null) { throw StateError('Failed to load animated codec for local asset ${key.id}'); } yield codec; } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is LocalFullImageProvider) { return id == other.id && size == other.size && isAnimated == other.isAnimated; } return false; } @override int get hashCode => id.hashCode ^ size.hashCode ^ isAnimated.hashCode; }