diff --git a/mobile/lib/domain/models/config/image_config.dart b/mobile/lib/domain/models/config/image_config.dart index 8410a9010b..3a887ba059 100644 --- a/mobile/lib/domain/models/config/image_config.dart +++ b/mobile/lib/domain/models/config/image_config.dart @@ -1,20 +1,28 @@ class ImageConfig { final bool preferRemote; + final bool loadPreview; final bool loadOriginal; - const ImageConfig({this.preferRemote = false, this.loadOriginal = false}); + const ImageConfig({this.preferRemote = false, this.loadPreview = true, this.loadOriginal = false}); - ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) => - ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal); + ImageConfig copyWith({bool? preferRemote, bool? loadPreview, bool? loadOriginal}) => ImageConfig( + preferRemote: preferRemote ?? this.preferRemote, + loadPreview: loadPreview ?? this.loadPreview, + loadOriginal: loadOriginal ?? this.loadOriginal, + ); @override bool operator ==(Object other) => identical(this, other) || - (other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal); + (other is ImageConfig && + other.preferRemote == preferRemote && + other.loadPreview == loadPreview && + other.loadOriginal == loadOriginal); @override - int get hashCode => Object.hash(preferRemote, loadOriginal); + int get hashCode => Object.hash(preferRemote, loadPreview, loadOriginal); @override - String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)'; + String toString() => + 'ImageConfig(preferRemoteImage: $preferRemote, loadPreview: $loadPreview, loadOriginal: $loadOriginal)'; } diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 541c538169..6383e1deb7 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -27,6 +27,7 @@ enum MetadataKey { // Image imagePreferRemote(.appConfig, 'image.preferRemote', false), + imageLoadPreview(.appConfig, 'image.loadPreview', true), imageLoadOriginal(.appConfig, 'image.loadOriginal', false), // Viewer diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index fa1d275026..6752ac6876 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -133,7 +133,11 @@ extension on MetadataDomain { groupAssetsBy: repo._read(.timelineGroupAssetsBy), storageIndicator: repo._read(.timelineStorageIndicator), ), - image: .new(preferRemote: repo._read(.imagePreferRemote), loadOriginal: repo._read(.imageLoadOriginal)), + image: .new( + preferRemote: repo._read(.imagePreferRemote), + loadPreview: repo._read(.imageLoadPreview), + loadOriginal: repo._read(.imageLoadOriginal), + ), viewer: .new( loopVideo: repo._read(.viewerLoopVideo), loadOriginalVideo: repo._read(.viewerLoadOriginalVideo), diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 9364fdd091..d74f086121 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -44,7 +44,6 @@ mixin CancellableImageProviderMixin on CancellableImageProvide completer.operation.valueOrCancellation().whenComplete(() { cachedStream.removeListener(listener); - cachedOperation = null; }); cachedOperation = completer.operation; return null; @@ -94,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); @@ -105,18 +107,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 6376e07405..9f79452d1c 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -98,51 +98,55 @@ class LocalFullImageProvider extends CancellableImageProvider _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { - yield* initialImageStream(); + final loadOriginal = MetadataRepository.instance.appConfig.image.loadOriginal; + final loadPreview = MetadataRepository.instance.appConfig.image.loadPreview; + yield* initialImageStream(isFinal: !loadOriginal && !loadPreview); if (isCancelled) { return; } - final loadOriginal = MetadataRepository.instance.appConfig.image.loadOriginal; - 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; - } - - request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero); - - yield* loadRequest(request, decode, isFinal: true); + final originalRequest = request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero); + 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 (MetadataRepository.instance.appConfig.image.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 fe422bbd2d..5655f1f3eb 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -108,31 +108,35 @@ class RemoteFullImageProvider extends CancellableImageProvider _codec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* { - yield* initialImageStream(); + final isImage = assetType == AssetType.image; + final loadOriginal = isImage && MetadataRepository.instance.appConfig.image.loadOriginal; + final loadPreview = isImage && MetadataRepository.instance.appConfig.image.loadPreview; + yield* initialImageStream(isFinal: !loadOriginal && !loadPreview); 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 && MetadataRepository.instance.appConfig.image.loadOriginal; - yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal); + 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; + } + } if (!loadOriginal) { return; } - if (isCancelled) { - return; - } - final originalRequest = request = RemoteImageRequest( uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited), ); @@ -140,24 +144,26 @@ class RemoteFullImageProvider extends CancellableImageProvider _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, - edited: key.edited, - ), - ); - yield* loadRequest(previewRequest, decode, isFinal: false); + if (MetadataRepository.instance.appConfig.image.loadPreview) { + 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 diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index 7858033401..367c128bf4 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -12,7 +12,11 @@ class ImageViewerQualitySetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isPreview = useState(ref.read(appConfigProvider).image.loadPreview); final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal); + useValueChanged(isPreview.value, (_, __) { + ref.read(metadataProvider).write(.imageLoadPreview, isPreview.value); + }); useValueChanged(isOriginal.value, (_, __) { ref.read(metadataProvider).write(.imageLoadOriginal, isOriginal.value); }); @@ -25,6 +29,12 @@ class ImageViewerQualitySetting extends HookConsumerWidget { icon: Icons.image_outlined, subtitle: "setting_image_viewer_help".t(context: context), ), + SettingsSwitchListTile( + valueNotifier: isPreview, + title: "setting_image_viewer_preview_title".t(context: context), + subtitle: "setting_image_viewer_preview_subtitle".t(context: context), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), SettingsSwitchListTile( valueNotifier: isOriginal, title: "setting_image_viewer_original_title".t(context: context),