mirror of
https://github.com/immich-app/immich.git
synced 2025-08-11 09:16:31 -04:00
use cached thumbnail
This commit is contained in:
parent
c8f9a72d3e
commit
57970800bb
@ -13,6 +13,9 @@ extension ContextHelper on BuildContext {
|
|||||||
// Returns the current height from MediaQuery
|
// Returns the current height from MediaQuery
|
||||||
double get height => MediaQuery.sizeOf(this).height;
|
double get height => MediaQuery.sizeOf(this).height;
|
||||||
|
|
||||||
|
// Returns the current size from MediaQuery
|
||||||
|
Size get sizeData => MediaQuery.sizeOf(this);
|
||||||
|
|
||||||
// Returns true if the app is running on a mobile device (!tablets)
|
// Returns true if the app is running on a mobile device (!tablets)
|
||||||
bool get isMobile => width < 550;
|
bool get isMobile => width < 550;
|
||||||
|
|
||||||
|
10
mobile/lib/extensions/codec_extensions.dart
Normal file
10
mobile/lib/extensions/codec_extensions.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
extension CodecImageInfoExtension on Codec {
|
||||||
|
Future<ImageInfo> getImageInfo({double scale = 1.0}) async {
|
||||||
|
final frame = await getNextFrame();
|
||||||
|
return ImageInfo(image: frame.image, scale: scale);
|
||||||
|
}
|
||||||
|
}
|
@ -147,11 +147,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
// Precache both thumbnail and full image for smooth transitions
|
// Precache both thumbnail and full image for smooth transitions
|
||||||
unawaited(
|
unawaited(
|
||||||
Future.wait([
|
Future.wait([
|
||||||
precacheImage(
|
precacheImage(getThumbnailImageProvider(asset: asset), context, onError: (_, __) {}),
|
||||||
getThumbnailImageProvider(asset: asset, size: screenSize),
|
|
||||||
context,
|
|
||||||
onError: (_, __) {},
|
|
||||||
),
|
|
||||||
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
|
precacheImage(getFullImageProvider(asset, size: screenSize), context, onError: (_, __) {}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@ -482,7 +478,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: Size(ctx.width, ctx.height)),
|
child: Thumbnail(asset: asset, fit: BoxFit.contain),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,7 +509,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) {
|
PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) {
|
||||||
final size = Size(ctx.width, ctx.height);
|
final size = ctx.sizeData;
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
key: ValueKey(asset.heroTag),
|
key: ValueKey(asset.heroTag),
|
||||||
imageProvider: getFullImageProvider(asset, size: size),
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
@ -529,10 +525,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
onTapDown: _onTapDown,
|
onTapDown: _onTapDown,
|
||||||
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
onLongPressStart: asset.isMotionPhoto ? _onLongPress : null,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorBuilder: (_, __, ___) => Container(
|
||||||
width: ctx.width,
|
width: size.width,
|
||||||
height: ctx.height,
|
height: size.height,
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
child: Thumbnail(asset: asset, fit: BoxFit.contain, size: size),
|
child: Thumbnail(asset: asset, fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -562,7 +558,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
asset: asset,
|
asset: asset,
|
||||||
image: Image(
|
image: Image(
|
||||||
key: ValueKey(asset),
|
key: ValueKey(asset),
|
||||||
image: getFullImageProvider(asset, size: Size(ctx.width, ctx.height)),
|
image: getFullImageProvider(asset, size: ctx.sizeData),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
height: ctx.height,
|
height: ctx.height,
|
||||||
width: ctx.width,
|
width: ctx.width,
|
||||||
|
@ -4,13 +4,14 @@ 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/local_image_provider.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/images/remote_image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
|
||||||
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
|
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) {
|
||||||
// Create new provider and cache it
|
// Create new provider and cache it
|
||||||
final ImageProvider provider;
|
final ImageProvider provider;
|
||||||
if (_shouldUseLocalAsset(asset)) {
|
if (_shouldUseLocalAsset(asset)) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||||
provider = LocalFullImageProvider(id: id, name: asset.name, size: size, type: asset.type);
|
provider = LocalFullImageProvider(id: id, size: size, type: asset.type, updatedAt: asset.updatedAt);
|
||||||
} else {
|
} else {
|
||||||
final String assetId;
|
final String assetId;
|
||||||
if (asset is LocalAsset && asset.hasRemote) {
|
if (asset is LocalAsset && asset.hasRemote) {
|
||||||
@ -26,7 +27,7 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
|
|||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = const Size.square(256)}) {
|
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = kThumbnailResolution}) {
|
||||||
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided');
|
||||||
|
|
||||||
if (remoteId != null) {
|
if (remoteId != null) {
|
||||||
@ -35,7 +36,7 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz
|
|||||||
|
|
||||||
if (_shouldUseLocalAsset(asset!)) {
|
if (_shouldUseLocalAsset(asset!)) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
||||||
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, name: asset.name, size: size);
|
return LocalThumbProvider(id: id, updatedAt: asset.updatedAt, size: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String assetId;
|
final String assetId;
|
||||||
@ -52,3 +53,25 @@ ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Siz
|
|||||||
|
|
||||||
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 cached
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stream != null) {
|
||||||
|
void listener(ImageInfo info, bool synchronousCall) {
|
||||||
|
thumbnail = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream.addListener(ImageStreamListener(listener));
|
||||||
|
} finally {
|
||||||
|
stream.removeListener(ImageStreamListener(listener));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnail;
|
||||||
|
}
|
||||||
|
@ -2,15 +2,17 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
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/widgets.dart';
|
import 'package:flutter/widgets.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/asset/base_asset.model.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/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/extensions/codec_extensions.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/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.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/timeline/constants.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/cache/thumbnail_image_cache_manager.dart';
|
||||||
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
||||||
@ -22,14 +24,12 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final String name;
|
|
||||||
final Size size;
|
final Size size;
|
||||||
|
|
||||||
const LocalThumbProvider({
|
const LocalThumbProvider({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.name,
|
this.size = kThumbnailResolution,
|
||||||
this.size = const Size.square(kTimelineFixedTileExtent),
|
|
||||||
this.cacheManager,
|
this.cacheManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,10 +45,8 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
codec: _codec(key, cache, decode),
|
codec: _codec(key, cache, decode),
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
|
||||||
DiagnosticsProperty<String>('Id', key.id),
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||||
DiagnosticsProperty<String>('Name', key.name),
|
|
||||||
DiagnosticsProperty<Size>('Size', key.size),
|
DiagnosticsProperty<Size>('Size', key.size),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -68,7 +66,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|||||||
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
final thumbnailBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||||
if (thumbnailBytes == null) {
|
if (thumbnailBytes == null) {
|
||||||
PaintingBinding.instance.imageCache.evict(key);
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
throw StateError("Loading thumb for local photo ${key.name} failed");
|
throw StateError("Loading thumb for local photo ${key.id} failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||||
@ -94,11 +92,11 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final StorageRepository _storageRepository = const StorageRepository();
|
final StorageRepository _storageRepository = const StorageRepository();
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
|
||||||
final Size size;
|
final Size size;
|
||||||
final AssetType type;
|
final AssetType type;
|
||||||
|
final DateTime updatedAt; // temporary, only exists to fetch cached thumbnail until local disk cache is removed
|
||||||
|
|
||||||
const LocalFullImageProvider({required this.id, required this.name, required this.size, required this.type});
|
const LocalFullImageProvider({required this.id, required this.size, required this.type, required this.updatedAt});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
@ -107,52 +105,45 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
return MultiImageStreamCompleter(
|
return OneFramePlaceholderImageStreamCompleter(
|
||||||
codec: _codec(key, decode),
|
_codec(key, decode),
|
||||||
scale: 1.0,
|
initialImage: getCachedImage(LocalThumbProvider(id: key.id, updatedAt: key.updatedAt)),
|
||||||
informationCollector: () sync* {
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
yield ErrorDescription(name);
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
},
|
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
|
||||||
|
DiagnosticsProperty<Size>('Size', key.size),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streams in each stage of the image as we ask for it
|
// Streams in each stage of the image as we ask for it
|
||||||
Stream<Codec> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
Stream<ImageInfo> _codec(LocalFullImageProvider key, ImageDecoderCallback decode) {
|
||||||
try {
|
try {
|
||||||
switch (key.type) {
|
return switch (key.type) {
|
||||||
case AssetType.image:
|
AssetType.image => _decodeProgressive(key, decode),
|
||||||
yield* _decodeProgressive(key, decode);
|
AssetType.video => _getThumbnailCodec(key, decode),
|
||||||
break;
|
_ => throw StateError('Unsupported asset type ${key.type}'),
|
||||||
case AssetType.video:
|
};
|
||||||
final codec = await _getThumbnailCodec(key, decode);
|
|
||||||
if (codec == null) {
|
|
||||||
throw StateError("Failed to load preview for ${key.name}");
|
|
||||||
}
|
|
||||||
yield codec;
|
|
||||||
break;
|
|
||||||
case AssetType.other:
|
|
||||||
case AssetType.audio:
|
|
||||||
throw StateError('Unsupported asset type ${key.type}');
|
|
||||||
}
|
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.name}', error, stack);
|
Logger('ImmichLocalImageProvider').severe('Error loading local image ${key.id}', error, stack);
|
||||||
throw const ImageLoadingException('Could not load image from local storage');
|
throw const ImageLoadingException('Could not load image from local storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Codec?> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async {
|
Stream<ImageInfo> _getThumbnailCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||||
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
final thumbBytes = await _assetMediaRepository.getThumbnail(key.id, size: key.size);
|
||||||
if (thumbBytes == null) {
|
if (thumbBytes == null) {
|
||||||
return null;
|
throw StateError("Failed to load preview for ${key.id}");
|
||||||
}
|
}
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
||||||
return decode(buffer);
|
final codec = await decode(buffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Codec> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
Stream<ImageInfo> _decodeProgressive(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
|
||||||
final file = await _storageRepository.getFileForAsset(key.id);
|
final file = await _storageRepository.getFileForAsset(key.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw StateError("Opening file for asset ${key.name} failed");
|
throw StateError("Opening file for asset ${key.id} failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
final fileSize = await file.length();
|
final fileSize = await file.length();
|
||||||
@ -171,7 +162,8 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
final mediumThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||||
if (mediumThumb != null) {
|
if (mediumThumb != null) {
|
||||||
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
||||||
yield await decode(mediumBuffer);
|
final codec = await decode(mediumBuffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
@ -187,24 +179,26 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
|||||||
final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
final highThumb = await _assetMediaRepository.getThumbnail(key.id, size: size);
|
||||||
if (highThumb != null) {
|
if (highThumb != null) {
|
||||||
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
||||||
yield await decode(highBuffer);
|
final codec = await decode(highBuffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
||||||
yield await decode(buffer);
|
final codec = await decode(buffer);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
if (other is LocalFullImageProvider) {
|
if (other is LocalFullImageProvider) {
|
||||||
return id == other.id && size == other.size && type == other.type && name == other.name;
|
return id == other.id && size == other.size && type == other.type;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode ^ name.hashCode;
|
int get hashCode => id.hashCode ^ size.hashCode ^ type.hashCode;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
// The below code is adapted from cached_network_image package's
|
||||||
|
// MultiImageStreamCompleter to better suit one-frame image loading.
|
||||||
|
// In particular, it allows providing an initial image to emit synchronously.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
/// An ImageStreamCompleter with support for loading multiple images.
|
||||||
|
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||||
|
ImageInfo? _initialImage;
|
||||||
|
|
||||||
|
/// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [image]
|
||||||
|
/// should be the primary image to display. The [initialImage] is an optional
|
||||||
|
/// image that will be emitted synchronously, useful as a thumbnail or placeholder.
|
||||||
|
OneFramePlaceholderImageStreamCompleter(
|
||||||
|
Stream<ImageInfo> image, {
|
||||||
|
ImageInfo? initialImage,
|
||||||
|
InformationCollector? informationCollector,
|
||||||
|
}) {
|
||||||
|
_initialImage = initialImage;
|
||||||
|
image.listen(
|
||||||
|
setImage,
|
||||||
|
onError: (Object error, StackTrace stack) {
|
||||||
|
reportError(
|
||||||
|
context: ErrorDescription('resolving a single-frame image stream'),
|
||||||
|
exception: error,
|
||||||
|
stack: stack,
|
||||||
|
informationCollector: informationCollector,
|
||||||
|
silent: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We must avoid disposing a completer if it never had a listener, even
|
||||||
|
/// if all [keepAlive] handles get disposed.
|
||||||
|
bool __hadAtLeastOneListener = false;
|
||||||
|
|
||||||
|
bool __disposed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addListener(ImageStreamListener listener) {
|
||||||
|
__hadAtLeastOneListener = true;
|
||||||
|
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 removeListener(ImageStreamListener listener) {
|
||||||
|
super.removeListener(listener);
|
||||||
|
if (!hasListeners) {
|
||||||
|
__maybeDispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int __keepAliveHandles = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleterHandle keepAlive() {
|
||||||
|
final delegateHandle = super.keepAlive();
|
||||||
|
return _OneFramePlaceholderImageStreamCompleterHandle(this, delegateHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void __maybeDispose() {
|
||||||
|
if (!__hadAtLeastOneListener || __disposed || hasListeners || __keepAliveHandles != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
__disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDisposed() {
|
||||||
|
_initialImage?.dispose();
|
||||||
|
_initialImage = null;
|
||||||
|
super.onDisposed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneFramePlaceholderImageStreamCompleterHandle implements ImageStreamCompleterHandle {
|
||||||
|
_OneFramePlaceholderImageStreamCompleterHandle(this._completer, this._delegateHandle) {
|
||||||
|
_completer!.__keepAliveHandles += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
OneFramePlaceholderImageStreamCompleter? _completer;
|
||||||
|
final ImageStreamCompleterHandle _delegateHandle;
|
||||||
|
|
||||||
|
/// Call this method to signal the [ImageStreamCompleter] that it can now be
|
||||||
|
/// disposed when its last listener drops.
|
||||||
|
///
|
||||||
|
/// This method must only be called once per object.
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
assert(_completer != null);
|
||||||
|
assert(_completer!.__keepAliveHandles > 0);
|
||||||
|
assert(!_completer!.__disposed);
|
||||||
|
|
||||||
|
_delegateHandle.dispose();
|
||||||
|
|
||||||
|
_completer!.__keepAliveHandles -= 1;
|
||||||
|
_completer!.__maybeDispose();
|
||||||
|
_completer = null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
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/extensions/codec_extensions.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';
|
||||||
@ -81,36 +83,28 @@ 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: key.assetId)),
|
||||||
scale: 1.0,
|
|
||||||
chunkEvents: chunkEvents.stream,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Codec> _codec(
|
Stream<ImageInfo> _codec(RemoteFullImageProvider key, CacheManager cache, ImageDecoderCallback decode) async* {
|
||||||
RemoteFullImageProvider key,
|
final codec = await ImageLoader.loadImageFromCache(
|
||||||
CacheManager cache,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
StreamController<ImageChunkEvent> chunkController,
|
|
||||||
) async* {
|
|
||||||
yield await ImageLoader.loadImageFromCache(
|
|
||||||
getPreviewUrlForRemoteId(key.assetId),
|
getPreviewUrlForRemoteId(key.assetId),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
decode: decode,
|
decode: decode,
|
||||||
chunkEvents: chunkController,
|
|
||||||
);
|
);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
|
|
||||||
if (AppSetting.get(Setting.loadOriginal)) {
|
if (AppSetting.get(Setting.loadOriginal)) {
|
||||||
yield await ImageLoader.loadImageFromCache(
|
final codec = await ImageLoader.loadImageFromCache(
|
||||||
getOriginalUrlForRemoteId(key.assetId),
|
getOriginalUrlForRemoteId(key.assetId),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
decode: decode,
|
decode: decode,
|
||||||
chunkEvents: chunkController,
|
|
||||||
);
|
);
|
||||||
|
yield await codec.getImageInfo();
|
||||||
}
|
}
|
||||||
await chunkController.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -19,7 +19,7 @@ class Thumbnail extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
final thumbHash = asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
||||||
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId, size: size);
|
final provider = getThumbnailImageProvider(asset: asset, remoteId: remoteId);
|
||||||
|
|
||||||
return OctoImage.fromSet(
|
return OctoImage.fromSet(
|
||||||
image: provider,
|
image: provider,
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
const double kTimelineHeaderExtent = 80.0;
|
const double kTimelineHeaderExtent = 80.0;
|
||||||
const double kTimelineFixedTileExtent = 256;
|
const Size kTimelineFixedTileExtent = Size.square(256);
|
||||||
|
const Size kThumbnailResolution = kTimelineFixedTileExtent;
|
||||||
const double kTimelineSpacing = 2.0;
|
const double kTimelineSpacing = 2.0;
|
||||||
const int kTimelineColumnCount = 3;
|
const int kTimelineColumnCount = 3;
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ abstract class SegmentBuilder {
|
|||||||
static Widget buildPlaceholder(
|
static Widget buildPlaceholder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
int count, {
|
int count, {
|
||||||
Size size = const Size.square(kTimelineFixedTileExtent),
|
Size size = kTimelineFixedTileExtent,
|
||||||
double spacing = kTimelineSpacing,
|
double spacing = kTimelineSpacing,
|
||||||
}) => RepaintBoundary(
|
}) => RepaintBoundary(
|
||||||
child: FixedTimelineRow(
|
child: FixedTimelineRow(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user