Compare commits

...

8 Commits

7 changed files with 114 additions and 64 deletions
@@ -1,20 +1,28 @@
class ImageConfig { class ImageConfig {
final bool preferRemote; final bool preferRemote;
final bool loadPreview;
final bool loadOriginal; 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 copyWith({bool? preferRemote, bool? loadPreview, bool? loadOriginal}) => ImageConfig(
ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal); preferRemote: preferRemote ?? this.preferRemote,
loadPreview: loadPreview ?? this.loadPreview,
loadOriginal: loadOriginal ?? this.loadOriginal,
);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, 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 @override
int get hashCode => Object.hash(preferRemote, loadOriginal); int get hashCode => Object.hash(preferRemote, loadPreview, loadOriginal);
@override @override
String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)'; String toString() =>
'ImageConfig(preferRemoteImage: $preferRemote, loadPreview: $loadPreview, loadOriginal: $loadOriginal)';
} }
@@ -27,6 +27,7 @@ enum MetadataKey<T extends Object> {
// Image // Image
imagePreferRemote<bool>(.appConfig, 'image.preferRemote', false), imagePreferRemote<bool>(.appConfig, 'image.preferRemote', false),
imageLoadPreview<bool>(.appConfig, 'image.loadPreview', true),
imageLoadOriginal<bool>(.appConfig, 'image.loadOriginal', false), imageLoadOriginal<bool>(.appConfig, 'image.loadOriginal', false),
// Viewer // Viewer
@@ -133,7 +133,11 @@ extension<T extends Object> on MetadataDomain<T> {
groupAssetsBy: repo._read(.timelineGroupAssetsBy), groupAssetsBy: repo._read(.timelineGroupAssetsBy),
storageIndicator: repo._read(.timelineStorageIndicator), 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( viewer: .new(
loopVideo: repo._read(.viewerLoopVideo), loopVideo: repo._read(.viewerLoopVideo),
loadOriginalVideo: repo._read(.viewerLoadOriginalVideo), loadOriginalVideo: repo._read(.viewerLoadOriginalVideo),
@@ -44,7 +44,6 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
completer.operation.valueOrCancellation().whenComplete(() { completer.operation.valueOrCancellation().whenComplete(() {
cachedStream.removeListener(listener); cachedStream.removeListener(listener);
cachedOperation = null;
}); });
cachedOperation = completer.operation; cachedOperation = completer.operation;
return null; return null;
@@ -94,6 +93,9 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
isFinished = isFinal; isFinished = isFinal;
return codec; return codec;
} catch (e) { } catch (e) {
if (isCancelled) {
return null;
}
if (isFinal) { if (isFinal) {
isFinished = true; isFinished = true;
PaintingBinding.instance.imageCache.evict(this); PaintingBinding.instance.imageCache.evict(this);
@@ -105,18 +107,33 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
} }
} }
Stream<ImageInfo> initialImageStream() async* { Stream<ImageInfo> initialImageStream({required bool isFinal}) async* {
final cachedOperation = this.cachedOperation; final cachedOperation = this.cachedOperation;
if (isCancelled) {
return;
}
if (cachedOperation == null) { if (cachedOperation == null) {
// image resolved synchronously
isFinished = isFinal;
return; return;
} }
try { try {
final cachedImage = await cachedOperation.valueOrCancellation(); final cachedImage = await cachedOperation.valueOrCancellation();
if (cachedImage != null && !isCancelled) { if (isCancelled || cachedImage == null) {
yield cachedImage; return;
} }
isFinished = isFinal;
yield cachedImage;
} catch (e, stack) { } catch (e, stack) {
if (isCancelled) {
return;
}
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
_log.severe('Error loading initial image', e, stack); _log.severe('Error loading initial image', e, stack);
} finally { } finally {
this.cachedOperation = null; this.cachedOperation = null;
@@ -98,51 +98,55 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
} }
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { Stream<ImageInfo> _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) { if (isCancelled) {
return; return;
} }
final loadOriginal = MetadataRepository.instance.appConfig.image.loadOriginal; if (loadPreview) {
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio; final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
var request = this.request = LocalImageRequest( final previewRequest = request = LocalImageRequest(
localId: key.id, localId: key.id,
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio), size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType, assetType: key.assetType,
); );
yield* loadRequest(request, decode, isFinal: !loadOriginal); yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (isCancelled) {
return;
}
}
if (!loadOriginal) { if (!loadOriginal) {
return; return;
} }
if (isCancelled) { final originalRequest = request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
return; yield* loadRequest(originalRequest, decode, isFinal: true);
}
request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
yield* loadRequest(request, decode, isFinal: true);
} }
Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
yield* initialImageStream(); yield* initialImageStream(isFinal: false);
if (isCancelled) { if (isCancelled) {
return; return;
} }
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio; if (MetadataRepository.instance.appConfig.image.loadPreview) {
final previewRequest = request = LocalImageRequest( final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
localId: key.id, final previewRequest = request = LocalImageRequest(
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio), localId: key.id,
assetType: key.assetType, size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
); assetType: key.assetType,
yield* loadRequest(previewRequest, decode, isFinal: false); );
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) { if (isCancelled) {
return; return;
}
} }
// always try original for animated, since previews don't support animation // always try original for animated, since previews don't support animation
@@ -108,31 +108,35 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
} }
Stream<ImageInfo> _codec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* { Stream<ImageInfo> _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) { if (isCancelled) {
return; return;
} }
final previewRequest = request = RemoteImageRequest( if (loadPreview) {
uri: getThumbnailUrlForRemoteId( final previewRequest = request = RemoteImageRequest(
key.assetId, uri: getThumbnailUrlForRemoteId(
type: AssetMediaSize.preview, key.assetId,
thumbhash: key.thumbhash, type: AssetMediaSize.preview,
edited: key.edited, thumbhash: key.thumbhash,
), edited: key.edited,
); ),
final loadOriginal = assetType == AssetType.image && MetadataRepository.instance.appConfig.image.loadOriginal; );
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal); yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (isCancelled) {
return;
}
}
if (!loadOriginal) { if (!loadOriginal) {
return; return;
} }
if (isCancelled) {
return;
}
final originalRequest = request = RemoteImageRequest( final originalRequest = request = RemoteImageRequest(
uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited), uri: getOriginalUrlForRemoteId(key.assetId, edited: key.edited),
); );
@@ -140,24 +144,26 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
} }
Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* { Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
yield* initialImageStream(); yield* initialImageStream(isFinal: false);
if (isCancelled) { if (isCancelled) {
return; return;
} }
final previewRequest = request = RemoteImageRequest( if (MetadataRepository.instance.appConfig.image.loadPreview) {
uri: getThumbnailUrlForRemoteId( final previewRequest = request = RemoteImageRequest(
key.assetId, uri: getThumbnailUrlForRemoteId(
type: AssetMediaSize.preview, key.assetId,
thumbhash: key.thumbhash, type: AssetMediaSize.preview,
edited: key.edited, thumbhash: key.thumbhash,
), edited: key.edited,
); ),
yield* loadRequest(previewRequest, decode, isFinal: false); );
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) { if (isCancelled) {
return; return;
}
} }
// always try original for animated, since previews don't support animation // always try original for animated, since previews don't support animation
@@ -12,7 +12,11 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isPreview = useState(ref.read(appConfigProvider).image.loadPreview);
final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal); final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal);
useValueChanged<bool, void>(isPreview.value, (_, __) {
ref.read(metadataProvider).write(.imageLoadPreview, isPreview.value);
});
useValueChanged<bool, void>(isOriginal.value, (_, __) { useValueChanged<bool, void>(isOriginal.value, (_, __) {
ref.read(metadataProvider).write(.imageLoadOriginal, isOriginal.value); ref.read(metadataProvider).write(.imageLoadOriginal, isOriginal.value);
}); });
@@ -25,6 +29,12 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
icon: Icons.image_outlined, icon: Icons.image_outlined,
subtitle: "setting_image_viewer_help".t(context: context), 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( SettingsSwitchListTile(
valueNotifier: isOriginal, valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".t(context: context), title: "setting_image_viewer_original_title".t(context: context),