From d4f7c2ae0a24dd312f993dbd31ef5d571039fa14 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:53:13 +0200 Subject: [PATCH 1/5] feat(mobile): implement load preview setting in asset viewer --- mobile/lib/domain/models/setting.model.dart | 1 + .../widgets/images/image_provider.dart | 21 ++++++- .../widgets/images/local_image_provider.dart | 59 ++++++++++--------- .../widgets/images/remote_image_provider.dart | 40 +++++++------ 4 files changed, 74 insertions(+), 47 deletions(-) diff --git a/mobile/lib/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart index 2c46507331..de1bc37215 100644 --- a/mobile/lib/domain/models/setting.model.dart +++ b/mobile/lib/domain/models/setting.model.dart @@ -4,6 +4,7 @@ enum Setting { tilesPerRow(StoreKey.tilesPerRow, 4), groupAssetsBy(StoreKey.groupAssetsBy, 0), showStorageIndicator(StoreKey.storageIndicator, true), + loadPreview(StoreKey.loadPreview, true), loadOriginal(StoreKey.loadOriginal, false), loadOriginalVideo(StoreKey.loadOriginalVideo, false), autoPlayVideo(StoreKey.autoPlayVideo, true), diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 47ebd37014..0640c341fd 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -106,18 +106,33 @@ mixin CancellableImageProviderMixin on CancellableImageProvide } } - Stream initialImageStream() async* { + Stream initialImageStream({required bool isFinal}) async* { final cachedOperation = this.cachedOperation; + if (isCancelled) { + return; + } if (cachedOperation == null) { + // image resolved synchronously + isFinished = isFinal; return; } try { final cachedImage = await cachedOperation.valueOrCancellation(); - if (cachedImage != null && !isCancelled) { - yield cachedImage; + if (isCancelled || cachedImage == null) { + return; } + 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; diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index d29a1cd56d..39fa3dde58 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -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/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.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/animated_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; @@ -97,51 +97,56 @@ class LocalFullImageProvider extends CancellableImageProvider _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { - yield* initialImageStream(); + final loadOriginal = AppSetting.get(Setting.loadOriginal); + final loadPreview = AppSetting.get(Setting.loadPreview); + yield* initialImageStream(isFinal: !loadOriginal && !loadPreview); 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 (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; + } + } if (!loadOriginal) { return; } - if (isCancelled) { - return; - } + final originalRequest = request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero); - request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero); - - yield* loadRequest(request, decode, isFinal: true); + yield* loadRequest(originalRequest, decode, isFinal: true); } Stream _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { - yield* initialImageStream(); + yield* initialImageStream(isFinal: false); if (isCancelled) { 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, isFinal: false); + 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); - if (isCancelled) { - return; + if (isCancelled) { + return; + } } // always try original for animated, since previews don't support animation diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index d9cc053ccf..2d4b2b1c28 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -102,44 +102,50 @@ class RemoteFullImageProvider extends CancellableImageProvider _codec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* { - yield* initialImageStream(); + final isImage = assetType == AssetType.image; + final loadOriginal = isImage && AppSetting.get(Setting.loadOriginal); + final loadPreview = isImage && AppSetting.get(Setting.loadPreview); + yield* initialImageStream(isFinal: !loadOriginal && !loadPreview); if (isCancelled) { return; } - final previewRequest = request = RemoteImageRequest( - uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash), - ); - final loadOriginal = assetType == AssetType.image && AppSetting.get(Setting.loadOriginal); - yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal); + if (loadPreview) { + final previewRequest = request = RemoteImageRequest( + uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash), + ); + yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal); + + if (isCancelled) { + return; + } + } if (!loadOriginal) { return; } - if (isCancelled) { - return; - } - final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId)); yield* loadRequest(originalRequest, decode, isFinal: true); } Stream _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* { - yield* initialImageStream(); + yield* initialImageStream(isFinal: false); if (isCancelled) { return; } - final previewRequest = request = RemoteImageRequest( - uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash), - ); - yield* loadRequest(previewRequest, decode, isFinal: false); + if (AppSetting.get(Setting.loadPreview)) { + final previewRequest = request = RemoteImageRequest( + uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash), + ); + yield* loadRequest(previewRequest, decode, isFinal: false); - if (isCancelled) { - return; + if (isCancelled) { + return; + } } // always try original for animated, since previews don't support animation From 67b1a9d99ea7cc1dff9673df9740dc9cbfeca995 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:29:31 +0200 Subject: [PATCH 2/5] fix: simplify request assignment in local image provider --- mobile/lib/presentation/widgets/images/local_image_provider.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 39fa3dde58..9a8506ce8a 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -124,7 +124,6 @@ class LocalFullImageProvider extends CancellableImageProvider Date: Sun, 12 Apr 2026 14:54:05 +0200 Subject: [PATCH 3/5] fix: don't clear cachedOperation in whenComplete listener --- mobile/lib/presentation/widgets/images/image_provider.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 0640c341fd..04948192db 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -45,7 +45,6 @@ mixin CancellableImageProviderMixin on CancellableImageProvide completer.operation.valueOrCancellation().whenComplete(() { cachedStream.removeListener(listener); - cachedOperation = null; }); cachedOperation = completer.operation; return null; From c2779258b444f86a211969a9bef223eb22dd5277 Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Wed, 20 May 2026 00:27:01 +0200 Subject: [PATCH 4/5] fix: reorder image load settings in metadata configuration --- mobile/lib/domain/models/metadata_key.dart | 2 +- mobile/lib/infrastructure/repositories/metadata.repository.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index f70bba6958..6383e1deb7 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -27,8 +27,8 @@ enum MetadataKey { // Image imagePreferRemote(.appConfig, 'image.preferRemote', false), - imageLoadOriginal(.appConfig, 'image.loadOriginal', false), imageLoadPreview(.appConfig, 'image.loadPreview', true), + imageLoadOriginal(.appConfig, 'image.loadOriginal', false), // Viewer viewerLoopVideo(.appConfig, 'viewer.loopVideo', true), diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index 4ad0c0f110..6752ac6876 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -135,8 +135,8 @@ extension on MetadataDomain { ), image: .new( preferRemote: repo._read(.imagePreferRemote), - loadOriginal: repo._read(.imageLoadOriginal), loadPreview: repo._read(.imageLoadPreview), + loadOriginal: repo._read(.imageLoadOriginal), ), viewer: .new( loopVideo: repo._read(.viewerLoopVideo), From fc40c223f4699f7bff490f6dd224295541a5ae0e Mon Sep 17 00:00:00 2001 From: LeLunZ <31982496+LeLunZ@users.noreply.github.com> Date: Wed, 20 May 2026 00:39:01 +0200 Subject: [PATCH 5/5] fix: handle cancellation in image loading and simplify request assignment --- mobile/lib/presentation/widgets/images/image_provider.dart | 3 +++ .../lib/presentation/widgets/images/local_image_provider.dart | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 6c0307b6ea..d74f086121 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -93,6 +93,9 @@ mixin CancellableImageProviderMixin on CancellableImageProvide isFinished = isFinal; return codec; } catch (e) { + if (isCancelled) { + return null; + } if (isFinal) { isFinished = true; PaintingBinding.instance.imageCache.evict(this); diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index 6ff9f2cd15..9f79452d1c 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -108,7 +108,7 @@ class LocalFullImageProvider extends CancellableImageProvider