mirror of
https://github.com/immich-app/immich.git
synced 2025-08-11 09:16:31 -04:00
buttery hero animation for remote assets
This commit is contained in:
parent
abb6f671fe
commit
663f684a4b
@ -473,10 +473,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
if (stackChildren != null && stackChildren.isNotEmpty) {
|
if (stackChildren != null && stackChildren.isNotEmpty) {
|
||||||
asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
|
asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
|
||||||
}
|
}
|
||||||
return Hero(
|
return Thumbnail.fromBaseAsset(asset: asset, fit: BoxFit.contain);
|
||||||
tag: '${asset.heroTag}_$heroOffset',
|
|
||||||
child: Thumbnail.fromBaseAsset(asset: asset, fit: BoxFit.contain),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
void _onScaleStateChanged(PhotoViewScaleState scaleState) {
|
||||||
|
@ -57,3 +57,25 @@ ImageProvider getThumbnailImageProvider({
|
|||||||
|
|
||||||
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
||||||
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
||||||
|
|
||||||
|
ImageInfo? getCachedImage(ImageProvider key) {
|
||||||
|
ImageInfo? thumbnail;
|
||||||
|
final ImageStreamCompleter? stream = PaintingBinding.instance.imageCache.putIfAbsent(
|
||||||
|
key,
|
||||||
|
() => throw Exception(), // don't bother loading if it isn't cacched
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stream != null) {
|
||||||
|
void listener(ImageInfo info, bool synchronousCall) {
|
||||||
|
thumbnail = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream.addListener(ImageStreamListener(listener));
|
||||||
|
} finally {
|
||||||
|
stream.removeListener(ImageStreamListener(listener));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnail;
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.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/images/one_frame_multi_image_stream_completer.dart';
|
||||||
|
|
||||||
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
||||||
@ -65,29 +66,9 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
ImageInfo? thumbnail;
|
|
||||||
final thumbnailProvider = LocalThumbProvider(id: key.id);
|
|
||||||
|
|
||||||
final ImageStreamCompleter? stream = PaintingBinding.instance.imageCache.putIfAbsent(
|
|
||||||
thumbnailProvider,
|
|
||||||
() => throw Exception(), // don't bother loading the thumbnail if it isn't cacched
|
|
||||||
);
|
|
||||||
|
|
||||||
if (stream != null) {
|
|
||||||
void listener(ImageInfo info, bool synchronousCall) {
|
|
||||||
thumbnail = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
stream.addListener(ImageStreamListener(listener));
|
|
||||||
} finally {
|
|
||||||
stream.removeListener(ImageStreamListener(listener));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return OneFramePlaceholderImageStreamCompleter(
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
_codec(key, decode),
|
_codec(key, decode),
|
||||||
initialImage: thumbnail,
|
initialImage: getCachedImage(LocalThumbProvider(id: key.id)),
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
DiagnosticsProperty<String>('Id', key.id),
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
@ -96,14 +77,14 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async {
|
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||||
final codec = await _assetMediaRepository.getLocalThumbnail(
|
final codec = await _assetMediaRepository.getLocalThumbnail(
|
||||||
key.id,
|
key.id,
|
||||||
Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||||
);
|
);
|
||||||
final frame = await codec.getNextFrame();
|
final frame = await codec.getNextFrame();
|
||||||
return ImageInfo(image: frame.image, scale: 1.0);
|
yield ImageInfo(image: frame.image, scale: 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -15,12 +15,12 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
|||||||
/// should be the primary image to display. The [initialImage] is an optional
|
/// should be the primary image to display. The [initialImage] is an optional
|
||||||
/// image that will be emitted synchronously, useful as a thumbnail or placeholder.
|
/// image that will be emitted synchronously, useful as a thumbnail or placeholder.
|
||||||
OneFramePlaceholderImageStreamCompleter(
|
OneFramePlaceholderImageStreamCompleter(
|
||||||
Future<ImageInfo> image, {
|
Stream<ImageInfo> image, {
|
||||||
ImageInfo? initialImage,
|
ImageInfo? initialImage,
|
||||||
InformationCollector? informationCollector,
|
InformationCollector? informationCollector,
|
||||||
}) {
|
}) {
|
||||||
_initialImage = initialImage;
|
_initialImage = initialImage;
|
||||||
image.then<void>(
|
image.listen(
|
||||||
setImage,
|
setImage,
|
||||||
onError: (Object error, StackTrace stack) {
|
onError: (Object error, StackTrace stack) {
|
||||||
reportError(
|
reportError(
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.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/services/setting.service.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/providers/image/cache/image_loader.dart';
|
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
||||||
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
@ -25,11 +26,9 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
|||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(RemoteThumbProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(RemoteThumbProvider key, ImageDecoderCallback decode) {
|
||||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||||
final chunkController = StreamController<ImageChunkEvent>();
|
|
||||||
return MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
codec: _codec(key, cache, decode, chunkController),
|
codec: _codec(key, cache, decode),
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
chunkEvents: chunkController.stream,
|
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||||
@ -37,20 +36,10 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Codec> _codec(
|
Future<Codec> _codec(RemoteThumbProvider key, CacheManager cache, ImageDecoderCallback decode) async {
|
||||||
RemoteThumbProvider key,
|
|
||||||
CacheManager cache,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
StreamController<ImageChunkEvent> chunkController,
|
|
||||||
) async {
|
|
||||||
final preview = getThumbnailUrlForRemoteId(key.assetId);
|
final preview = getThumbnailUrlForRemoteId(key.assetId);
|
||||||
|
|
||||||
return ImageLoader.loadImageFromCache(
|
return ImageLoader.loadImageFromCache(preview, cache: cache, decode: decode);
|
||||||
preview,
|
|
||||||
cache: cache,
|
|
||||||
decode: decode,
|
|
||||||
chunkEvents: chunkController,
|
|
||||||
).whenComplete(chunkController.close);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -81,36 +70,39 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
|
|||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
return MultiImageStreamCompleter(
|
_codec(key, cache, decode),
|
||||||
codec: _codec(key, cache, decode, chunkEvents),
|
initialImage: getCachedImage(RemoteThumbProvider(assetId: assetId)),
|
||||||
scale: 1.0,
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
chunkEvents: chunkEvents.stream,
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Codec> _codec(
|
Stream<ImageInfo> _codec(RemoteFullImageProvider key, CacheManager cache, ImageDecoderCallback decode) async* {
|
||||||
RemoteFullImageProvider key,
|
ImageInfo? imageInfo;
|
||||||
CacheManager cache,
|
final originalImageFuture = AppSetting.get(Setting.loadOriginal)
|
||||||
ImageDecoderCallback decode,
|
? ImageLoader.loadImageFromCache(
|
||||||
StreamController<ImageChunkEvent> chunkController,
|
getOriginalUrlForRemoteId(key.assetId),
|
||||||
) async* {
|
cache: cache,
|
||||||
yield await ImageLoader.loadImageFromCache(
|
decode: decode,
|
||||||
getPreviewUrlForRemoteId(key.assetId),
|
).then((image) => image.getNextFrame()).then((frame) => imageInfo = ImageInfo(image: frame.image, scale: 1.0))
|
||||||
cache: cache,
|
: null;
|
||||||
decode: decode,
|
|
||||||
chunkEvents: chunkController,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (AppSetting.get(Setting.loadOriginal)) {
|
final previewImageFuture =
|
||||||
yield await ImageLoader.loadImageFromCache(
|
ImageLoader.loadImageFromCache(getPreviewUrlForRemoteId(key.assetId), cache: cache, decode: decode)
|
||||||
getOriginalUrlForRemoteId(key.assetId),
|
.then((image) async => imageInfo == null ? await image.getNextFrame() : null)
|
||||||
cache: cache,
|
.then((frame) => imageInfo == null ? ImageInfo(image: frame!.image, scale: 1.0) : null);
|
||||||
decode: decode,
|
|
||||||
chunkEvents: chunkController,
|
final previewImage = await previewImageFuture;
|
||||||
);
|
if (previewImage != null) {
|
||||||
|
yield previewImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalImageFuture != null) {
|
||||||
|
yield await originalImageFuture;
|
||||||
}
|
}
|
||||||
await chunkController.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user