feat(mobile): platform image providers (#20927)
* platform image providers * use key * fix cache manager * more logs, cancel on dispose instead * split into separate files * fix saving to cache * cancel multi-stage provider * refactored `getInitialImage` * only wait for disposal for full images * cached image works * formatting * lower asset viewer ram usage --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -1,17 +1,114 @@
|
||||
import 'package:async/async.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/infrastructure/loaders/image_request.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
abstract class CancellableImageProvider<T extends Object> extends ImageProvider<T> {
|
||||
void cancel();
|
||||
}
|
||||
|
||||
mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvider<T> {
|
||||
static final _log = Logger('CancellableImageProviderMixin');
|
||||
|
||||
bool isCancelled = false;
|
||||
ImageRequest? request;
|
||||
CancelableOperation<ImageInfo?>? cachedOperation;
|
||||
|
||||
ImageInfo? getInitialImage(CancellableImageProvider provider) {
|
||||
final completer = CancelableCompleter<ImageInfo?>(onCancel: provider.cancel);
|
||||
final cachedStream = provider.resolve(const ImageConfiguration());
|
||||
ImageInfo? cachedImage;
|
||||
final listener = ImageStreamListener((image, synchronousCall) {
|
||||
if (synchronousCall) {
|
||||
cachedImage = image;
|
||||
}
|
||||
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(image);
|
||||
}
|
||||
}, onError: completer.completeError);
|
||||
|
||||
cachedStream.addListener(listener);
|
||||
if (cachedImage != null) {
|
||||
cachedStream.removeListener(listener);
|
||||
return cachedImage;
|
||||
}
|
||||
|
||||
completer.operation.valueOrCancellation().whenComplete(() {
|
||||
cachedStream.removeListener(listener);
|
||||
cachedOperation = null;
|
||||
});
|
||||
cachedOperation = completer.operation;
|
||||
return null;
|
||||
}
|
||||
|
||||
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode) async* {
|
||||
if (isCancelled) {
|
||||
evict();
|
||||
return;
|
||||
}
|
||||
|
||||
this.request = request;
|
||||
|
||||
try {
|
||||
final image = await request.load(decode);
|
||||
if (image == null || isCancelled) {
|
||||
evict();
|
||||
return;
|
||||
}
|
||||
yield image;
|
||||
} finally {
|
||||
this.request = null;
|
||||
}
|
||||
}
|
||||
|
||||
Stream<ImageInfo> initialImageStream() async* {
|
||||
final cachedOperation = this.cachedOperation;
|
||||
if (cachedOperation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final cachedImage = await cachedOperation.valueOrCancellation();
|
||||
if (cachedImage != null && !isCancelled) {
|
||||
yield cachedImage;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
_log.severe('Error loading initial image', e, stack);
|
||||
} finally {
|
||||
this.cachedOperation = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void cancel() {
|
||||
isCancelled = true;
|
||||
final request = this.request;
|
||||
if (request != null) {
|
||||
this.request = null;
|
||||
request.cancel();
|
||||
}
|
||||
|
||||
final operation = cachedOperation;
|
||||
if (operation != null) {
|
||||
this.cachedOperation = null;
|
||||
operation.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
|
||||
// Create new provider and cache it
|
||||
final ImageProvider provider;
|
||||
if (_shouldUseLocalAsset(asset)) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||
provider = LocalFullImageProvider(id: id, size: size, type: asset.type, updatedAt: asset.updatedAt);
|
||||
provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type);
|
||||
} else {
|
||||
final String assetId;
|
||||
if (asset is LocalAsset && asset.hasRemote) {
|
||||
@@ -36,7 +133,7 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz
|
||||
|
||||
if (_shouldUseLocalAsset(asset!)) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, size: size);
|
||||
return LocalThumbProvider(id: id, size: size, assetType: asset.type);
|
||||
}
|
||||
|
||||
final String assetId;
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.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/extensions/codec_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/loaders/image_request.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';
|
||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
||||
final AssetMediaRepository _assetMediaRepository = const AssetMediaRepository();
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
|
||||
with CancellableImageProviderMixin<LocalThumbProvider> {
|
||||
final String id;
|
||||
final DateTime updatedAt;
|
||||
final Size size;
|
||||
final AssetType assetType;
|
||||
|
||||
const LocalThumbProvider({
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
this.size = kThumbnailResolution,
|
||||
this.cacheManager,
|
||||
});
|
||||
LocalThumbProvider({required this.id, required this.assetType, this.size = kThumbnailResolution});
|
||||
|
||||
@override
|
||||
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -39,63 +24,39 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(LocalThumbProvider key, ImageDecoderCallback decode) {
|
||||
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode),
|
||||
scale: 1.0,
|
||||
return OneFramePlaceholderImageStreamCompleter(
|
||||
_codec(key, decode),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<String>('Id', key.id),
|
||||
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||
DiagnosticsProperty<Size>('Size', key.size),
|
||||
],
|
||||
);
|
||||
)..addOnLastListenerRemovedCallback(cancel);
|
||||
}
|
||||
|
||||
Future<Codec> _codec(LocalThumbProvider key, CacheManager cache, ImageDecoderCallback decode) async {
|
||||
final cacheKey = '${key.id}-${key.updatedAt}-${key.size.width}x${key.size.height}';
|
||||
|
||||
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
||||
if (fileFromCache != null) {
|
||||
try {
|
||||
final buffer = await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
|
||||
return decode(buffer);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||
if (thumbnailBytes == null) {
|
||||
PaintingBinding.instance.imageCache.evict(key);
|
||||
throw StateError("Loading thumb for local photo ${key.id} failed");
|
||||
}
|
||||
|
||||
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||
unawaited(cache.putFile(cacheKey, thumbnailBytes));
|
||||
return decode(buffer);
|
||||
Stream<ImageInfo> _codec(LocalThumbProvider key, ImageDecoderCallback decode) {
|
||||
return loadRequest(LocalImageRequest(localId: key.id, size: size, assetType: key.assetType), decode);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is LocalThumbProvider) {
|
||||
return id == other.id && updatedAt == other.updatedAt;
|
||||
return id == other.id && size == other.size;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ updatedAt.hashCode;
|
||||
int get hashCode => id.hashCode ^ size.hashCode;
|
||||
}
|
||||
|
||||
class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
||||
final AssetMediaRepository _assetMediaRepository = const AssetMediaRepository();
|
||||
final StorageRepository _storageRepository = const StorageRepository();
|
||||
|
||||
class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProvider>
|
||||
with CancellableImageProviderMixin<LocalFullImageProvider> {
|
||||
final String id;
|
||||
final Size size;
|
||||
final AssetType type;
|
||||
final DateTime updatedAt; // temporary, only exists to fetch cached thumbnail until local disk cache is removed
|
||||
final AssetType assetType;
|
||||
|
||||
const LocalFullImageProvider({required this.id, required this.size, required this.type, required this.updatedAt});
|
||||
LocalFullImageProvider({required this.id, required this.assetType, required this.size});
|
||||
|
||||
@override
|
||||
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -106,114 +67,43 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
||||
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||
return OneFramePlaceholderImageStreamCompleter(
|
||||
_codec(key, decode),
|
||||
initialImage: getCachedImage(LocalThumbProvider(id: key.id, assetType: key.assetType)),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Id', key.id),
|
||||
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||
DiagnosticsProperty<Size>('Size', key.size),
|
||||
],
|
||||
onDispose: cancel,
|
||||
);
|
||||
}
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
try {
|
||||
// First, yield the thumbnail image from LocalThumbProvider
|
||||
final thumbProvider = LocalThumbProvider(id: key.id, updatedAt: key.updatedAt);
|
||||
try {
|
||||
final thumbCodec = await thumbProvider._codec(
|
||||
thumbProvider,
|
||||
thumbProvider.cacheManager ?? ThumbnailImageCacheManager(),
|
||||
decode,
|
||||
);
|
||||
final thumbImageInfo = await thumbCodec.getImageInfo();
|
||||
yield thumbImageInfo;
|
||||
} catch (_) {}
|
||||
yield* initialImageStream();
|
||||
|
||||
// Then proceed with the main image loading stream
|
||||
final mainStream = switch (key.type) {
|
||||
AssetType.image => _decodeProgressive(key, decode),
|
||||
AssetType.video => _getThumbnailCodec(key, decode),
|
||||
_ => throw StateError('Unsupported asset type ${key.type}'),
|
||||
};
|
||||
|
||||
await for (final imageInfo in mainStream) {
|
||||
yield imageInfo;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.id}', error, stack);
|
||||
throw const ImageLoadingException('Could not load image from local storage');
|
||||
}
|
||||
}
|
||||
|
||||
Stream<ImageInfo> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||
if (thumbBytes == null) {
|
||||
throw StateError("Failed to load preview for ${key.id}");
|
||||
}
|
||||
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield await codec.getImageInfo();
|
||||
}
|
||||
|
||||
Stream<ImageInfo> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
final file = await _storageRepository.getFileForAsset(key.id);
|
||||
if (file == null) {
|
||||
throw StateError("Opening file for asset ${key.id} failed");
|
||||
}
|
||||
|
||||
final fileSize = await file.length();
|
||||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
final isLargeFile = fileSize > 20 * 1024 * 1024; // 20MB
|
||||
final isHEIC = file.path.toLowerCase().contains(RegExp(r'\.(heic|heif)$'));
|
||||
final isProgressive = isLargeFile || (isHEIC && !Platform.isIOS);
|
||||
|
||||
if (isProgressive) {
|
||||
try {
|
||||
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 1.3 : 1.2;
|
||||
final size = Size(
|
||||
(key.size.width * progressiveMultiplier).clamp(256, 1024),
|
||||
(key.size.height * progressiveMultiplier).clamp(256, 1024),
|
||||
);
|
||||
final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||
if (mediumThumb != null) {
|
||||
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
||||
final codec = await decode(mediumBuffer);
|
||||
yield await codec.getImageInfo();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Load original only when the file is smaller or if the user wants to load original images
|
||||
// Or load a slightly larger image for progressive loading
|
||||
if (isProgressive && !(AppSetting.get(Setting.loadOriginal))) {
|
||||
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 2.0 : 1.6;
|
||||
final size = Size(
|
||||
(key.size.width * progressiveMultiplier).clamp(512, 2048),
|
||||
(key.size.height * progressiveMultiplier).clamp(512, 2048),
|
||||
);
|
||||
final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||
if (highThumb != null) {
|
||||
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
||||
final codec = await decode(highBuffer);
|
||||
yield await codec.getImageInfo();
|
||||
}
|
||||
if (isCancelled) {
|
||||
evict();
|
||||
return;
|
||||
}
|
||||
|
||||
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
||||
final codec = await decode(buffer);
|
||||
yield await codec.getImageInfo();
|
||||
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
final request = LocalImageRequest(
|
||||
localId: key.id,
|
||||
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
|
||||
assetType: key.assetType,
|
||||
);
|
||||
|
||||
yield* loadRequest(request, decode);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is LocalFullImageProvider) {
|
||||
return id == other.id && size == other.size && type == other.type;
|
||||
return id == other.id && size == other.size;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode;
|
||||
int get hashCode => id.hashCode ^ size.hashCode;
|
||||
}
|
||||
|
||||
+12
-28
@@ -9,7 +9,7 @@ import 'package:flutter/painting.dart';
|
||||
|
||||
/// An ImageStreamCompleter with support for loading multiple images.
|
||||
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
ImageInfo? _initialImage;
|
||||
void Function()? _onDispose;
|
||||
|
||||
/// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [images]
|
||||
/// should be the primary images to display (typically asynchronously as they load).
|
||||
@@ -19,10 +19,14 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
Stream<ImageInfo> images, {
|
||||
ImageInfo? initialImage,
|
||||
InformationCollector? informationCollector,
|
||||
void Function()? onDispose,
|
||||
}) {
|
||||
_initialImage = initialImage;
|
||||
if (initialImage != null) {
|
||||
setImage(initialImage);
|
||||
}
|
||||
_onDispose = onDispose;
|
||||
images.listen(
|
||||
_onImage,
|
||||
setImage,
|
||||
onError: (Object error, StackTrace stack) {
|
||||
reportError(
|
||||
context: ErrorDescription('resolving a single-frame image stream'),
|
||||
@@ -35,33 +39,13 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
);
|
||||
}
|
||||
|
||||
void _onImage(ImageInfo image) {
|
||||
setImage(image);
|
||||
_initialImage?.dispose();
|
||||
_initialImage = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(ImageStreamListener listener) {
|
||||
final initialImage = _initialImage;
|
||||
if (initialImage != null) {
|
||||
try {
|
||||
listener.onImage(initialImage.clone(), true);
|
||||
} catch (exception, stack) {
|
||||
reportError(
|
||||
context: ErrorDescription('by a synchronously-called image listener'),
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
super.addListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDisposed() {
|
||||
_initialImage?.dispose();
|
||||
_initialImage = null;
|
||||
final onDispose = _onDispose;
|
||||
if (onDispose != null) {
|
||||
_onDispose = null;
|
||||
onDispose();
|
||||
}
|
||||
super.onDisposed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/extensions/codec_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/loaders/image_request.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/remote_image_cache_manager.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||
class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
|
||||
with CancellableImageProviderMixin<RemoteThumbProvider> {
|
||||
static final cacheManager = RemoteThumbnailCacheManager();
|
||||
final String assetId;
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
const RemoteThumbProvider({required this.assetId, this.cacheManager});
|
||||
RemoteThumbProvider({required this.assetId});
|
||||
|
||||
@override
|
||||
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -26,33 +25,22 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(RemoteThumbProvider key, ImageDecoderCallback decode) {
|
||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||
final chunkController = StreamController<ImageChunkEvent>();
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode, chunkController),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkController.stream,
|
||||
return OneFramePlaceholderImageStreamCompleter(
|
||||
_codec(key, decode),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||
],
|
||||
);
|
||||
)..addOnLastListenerRemovedCallback(cancel);
|
||||
}
|
||||
|
||||
Future<Codec> _codec(
|
||||
RemoteThumbProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkController,
|
||||
) async {
|
||||
final preview = getThumbnailUrlForRemoteId(key.assetId);
|
||||
|
||||
return ImageLoader.loadImageFromCache(
|
||||
preview,
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
chunkEvents: chunkController,
|
||||
).whenComplete(chunkController.close);
|
||||
Stream<ImageInfo> _codec(RemoteThumbProvider key, ImageDecoderCallback decode) {
|
||||
final request = RemoteImageRequest(
|
||||
uri: getThumbnailUrlForRemoteId(key.assetId),
|
||||
headers: ApiService.getRequestHeaders(),
|
||||
cacheManager: cacheManager,
|
||||
);
|
||||
return loadRequest(request, decode);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -69,11 +57,12 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||
int get hashCode => assetId.hashCode;
|
||||
}
|
||||
|
||||
class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
|
||||
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
|
||||
with CancellableImageProviderMixin<RemoteFullImageProvider> {
|
||||
static final cacheManager = RemoteThumbnailCacheManager();
|
||||
final String assetId;
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
const RemoteFullImageProvider({required this.assetId, this.cacheManager});
|
||||
RemoteFullImageProvider({required this.assetId});
|
||||
|
||||
@override
|
||||
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -82,28 +71,49 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(RemoteFullImageProvider key, ImageDecoderCallback decode) {
|
||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||
return OneFramePlaceholderImageStreamCompleter(
|
||||
_codec(key, cache, decode),
|
||||
_codec(key, decode),
|
||||
initialImage: getCachedImage(RemoteThumbProvider(assetId: key.assetId)),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||
],
|
||||
onDispose: cancel,
|
||||
);
|
||||
}
|
||||
|
||||
Stream<ImageInfo> _codec(RemoteFullImageProvider key, CacheManager cache, ImageDecoderCallback decode) async* {
|
||||
final codec = await ImageLoader.loadImageFromCache(
|
||||
getPreviewUrlForRemoteId(key.assetId),
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
);
|
||||
yield await codec.getImageInfo();
|
||||
Stream<ImageInfo> _codec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||
yield* initialImageStream();
|
||||
|
||||
if (isCancelled) {
|
||||
evict();
|
||||
return;
|
||||
}
|
||||
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
try {
|
||||
final request = RemoteImageRequest(
|
||||
uri: getPreviewUrlForRemoteId(key.assetId),
|
||||
headers: headers,
|
||||
cacheManager: cacheManager,
|
||||
);
|
||||
yield* loadRequest(request, decode);
|
||||
} finally {
|
||||
request = null;
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
evict();
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppSetting.get(Setting.loadOriginal)) {
|
||||
final codec = await ImageLoader.loadImageFromCache(
|
||||
getOriginalUrlForRemoteId(key.assetId),
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
);
|
||||
yield await codec.getImageInfo();
|
||||
try {
|
||||
final request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId), headers: headers);
|
||||
yield* loadRequest(request, decode);
|
||||
} finally {
|
||||
request = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'dart:convert' hide Codec;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:thumbhash/thumbhash.dart';
|
||||
import 'package:immich_mobile/infrastructure/loaders/image_request.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';
|
||||
|
||||
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
|
||||
class ThumbHashProvider extends CancellableImageProvider<ThumbHashProvider>
|
||||
with CancellableImageProviderMixin<ThumbHashProvider> {
|
||||
final String thumbHash;
|
||||
|
||||
const ThumbHashProvider({required this.thumbHash});
|
||||
ThumbHashProvider({required this.thumbHash});
|
||||
|
||||
@override
|
||||
Future<ThumbHashProvider> obtainKey(ImageConfiguration configuration) {
|
||||
@@ -17,12 +17,11 @@ class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) {
|
||||
return MultiFrameImageStreamCompleter(codec: _loadCodec(key, decode), scale: 1.0);
|
||||
return OneFramePlaceholderImageStreamCompleter(_loadCodec(key, decode))..addOnLastListenerRemovedCallback(cancel);
|
||||
}
|
||||
|
||||
Future<Codec> _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) async {
|
||||
final image = thumbHashToRGBA(base64Decode(key.thumbHash));
|
||||
return decode(await ImmutableBuffer.fromUint8List(rgbaToBmp(image)));
|
||||
Stream<ImageInfo> _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) {
|
||||
return loadRequest(ThumbhashImageRequest(thumbhash: key.thumbHash), decode);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user