mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	feat(mobile): Remote thumbnails and images use an on-disk image cache (#7929)
* Fixes remote full / thumbnail provider * Adds image cache manager to both remote image providers format format Fix typo in equals remove unused import renames image loader * Adds height and width to the image cache for thumbs format * Uses a separate remote and thumbnail cache format * Fixes key name * Changes uri to string, fixes comment * Chunk events are optional and remote thumbnails don't report chunk events * better exception handling --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									5a589babcb
								
							
						
					
					
						commit
						582cdcab82
					
				@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
					import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
				
			||||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
 | 
					import 'package:immich_mobile/extensions/datetime_extensions.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 | 
					import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 | 
					import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -106,9 +106,8 @@ class _ActivityAssetThumbnail extends StatelessWidget {
 | 
				
			|||||||
      decoration: BoxDecoration(
 | 
					      decoration: BoxDecoration(
 | 
				
			||||||
        borderRadius: const BorderRadius.all(Radius.circular(4)),
 | 
					        borderRadius: const BorderRadius.all(Radius.circular(4)),
 | 
				
			||||||
        image: DecorationImage(
 | 
					        image: DecorationImage(
 | 
				
			||||||
          image: ImmichRemoteImageProvider(
 | 
					          image: ImmichRemoteThumbnailProvider(
 | 
				
			||||||
            assetId: assetId,
 | 
					            assetId: assetId,
 | 
				
			||||||
            isThumbnail: true,
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          fit: BoxFit.cover,
 | 
					          fit: BoxFit.cover,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										58
									
								
								mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:ui' as ui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/exceptions/image_loading_exception.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/store.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Loads the codec from the URI and sends the events to the [chunkEvents] stream
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Credit to [flutter_cached_network_image](https://github.com/Baseflow/flutter_cached_network_image/blob/develop/cached_network_image/lib/src/image_provider/_image_loader.dart)
 | 
				
			||||||
 | 
					/// for this wonderful implementation of their image loader
 | 
				
			||||||
 | 
					class ImageLoader {
 | 
				
			||||||
 | 
					  static Future<ui.Codec> loadImageFromCache(
 | 
				
			||||||
 | 
					    String uri, {
 | 
				
			||||||
 | 
					    required ImageCacheManager cache,
 | 
				
			||||||
 | 
					    required ImageDecoderCallback decode,
 | 
				
			||||||
 | 
					    StreamController<ImageChunkEvent>? chunkEvents,
 | 
				
			||||||
 | 
					    int? height,
 | 
				
			||||||
 | 
					    int? width,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    final headers = {
 | 
				
			||||||
 | 
					      'x-immich-user-token': Store.get(StoreKey.accessToken),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final stream = cache.getImageFile(
 | 
				
			||||||
 | 
					      uri,
 | 
				
			||||||
 | 
					      withProgress: true,
 | 
				
			||||||
 | 
					      headers: headers,
 | 
				
			||||||
 | 
					      maxHeight: height,
 | 
				
			||||||
 | 
					      maxWidth: width,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await for (final result in stream) {
 | 
				
			||||||
 | 
					      if (result is DownloadProgress) {
 | 
				
			||||||
 | 
					        // We are downloading the file, so update the [chunkEvents]
 | 
				
			||||||
 | 
					        chunkEvents?.add(
 | 
				
			||||||
 | 
					          ImageChunkEvent(
 | 
				
			||||||
 | 
					            cumulativeBytesLoaded: result.downloaded,
 | 
				
			||||||
 | 
					            expectedTotalBytes: result.totalSize,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (result is FileInfo) {
 | 
				
			||||||
 | 
					        // We have the file
 | 
				
			||||||
 | 
					        final file = result.file;
 | 
				
			||||||
 | 
					        final bytes = await file.readAsBytes();
 | 
				
			||||||
 | 
					        final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
 | 
				
			||||||
 | 
					        final decoded = await decode(buffer);
 | 
				
			||||||
 | 
					        return decoded;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If we get here, the image failed to load from the cache stream
 | 
				
			||||||
 | 
					    throw ImageLoadingException('Could not load image from stream');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								mobile/lib/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mobile/lib/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The cache manager for full size images [ImmichRemoteImageProvider]
 | 
				
			||||||
 | 
					class RemoteImageCacheManager extends CacheManager with ImageCacheManager {
 | 
				
			||||||
 | 
					  static const key = 'remoteImageCacheKey';
 | 
				
			||||||
 | 
					  static final RemoteImageCacheManager _instance = RemoteImageCacheManager._();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory RemoteImageCacheManager() {
 | 
				
			||||||
 | 
					    return _instance;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  RemoteImageCacheManager._()
 | 
				
			||||||
 | 
					      : super(
 | 
				
			||||||
 | 
					          Config(
 | 
				
			||||||
 | 
					            key,
 | 
				
			||||||
 | 
					            maxNrOfCacheObjects: 500,
 | 
				
			||||||
 | 
					            stalePeriod: const Duration(days: 30),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								mobile/lib/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								mobile/lib/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The cache manager for thumbnail images [ImmichRemoteThumbnailProvider]
 | 
				
			||||||
 | 
					class ThumbnailImageCacheManager extends CacheManager with ImageCacheManager {
 | 
				
			||||||
 | 
					  static const key = 'thumbnailImageCacheKey';
 | 
				
			||||||
 | 
					  static final ThumbnailImageCacheManager _instance =
 | 
				
			||||||
 | 
					      ThumbnailImageCacheManager._();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ThumbnailImageCacheManager() {
 | 
				
			||||||
 | 
					    return _instance;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ThumbnailImageCacheManager._()
 | 
				
			||||||
 | 
					      : super(
 | 
				
			||||||
 | 
					          Config(
 | 
				
			||||||
 | 
					            key,
 | 
				
			||||||
 | 
					            maxNrOfCacheObjects: 5000,
 | 
				
			||||||
 | 
					            stalePeriod: const Duration(days: 30),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					/// An exception for the [ImageLoader] and the Immich image providers
 | 
				
			||||||
 | 
					class ImageLoadingException implements Exception {
 | 
				
			||||||
 | 
					  final String message;
 | 
				
			||||||
 | 
					  ImageLoadingException(this.message);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,8 +1,10 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:io';
 | 
					 | 
				
			||||||
import 'dart:ui' as ui;
 | 
					import 'dart:ui' as ui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/image_loader.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart';
 | 
				
			||||||
import 'package:openapi/api.dart' as api;
 | 
					import 'package:openapi/api.dart' as api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
@ -12,24 +14,18 @@ import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			|||||||
import 'package:immich_mobile/shared/models/store.dart';
 | 
					import 'package:immich_mobile/shared/models/store.dart';
 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Our Image Provider HTTP client to make the request
 | 
					/// The remote image provider for full size remote images
 | 
				
			||||||
final _httpClient = HttpClient()
 | 
					 | 
				
			||||||
  ..autoUncompress = false
 | 
					 | 
				
			||||||
  ..maxConnectionsPerHost = 10;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// The remote image provider
 | 
					 | 
				
			||||||
class ImmichRemoteImageProvider
 | 
					class ImmichRemoteImageProvider
 | 
				
			||||||
    extends ImageProvider<ImmichRemoteImageProvider> {
 | 
					    extends ImageProvider<ImmichRemoteImageProvider> {
 | 
				
			||||||
  /// The [Asset.remoteId] of the asset to fetch
 | 
					  /// The [Asset.remoteId] of the asset to fetch
 | 
				
			||||||
  final String assetId;
 | 
					  final String assetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // If this is a thumbnail, we stop at loading the
 | 
					  /// The image cache manager
 | 
				
			||||||
  // smallest version of the remote image
 | 
					  final ImageCacheManager? cacheManager;
 | 
				
			||||||
  final bool isThumbnail;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ImmichRemoteImageProvider({
 | 
					  ImmichRemoteImageProvider({
 | 
				
			||||||
    required this.assetId,
 | 
					    required this.assetId,
 | 
				
			||||||
    this.isThumbnail = false,
 | 
					    this.cacheManager,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
 | 
					  /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
 | 
				
			||||||
@ -46,9 +42,10 @@ class ImmichRemoteImageProvider
 | 
				
			|||||||
    ImmichRemoteImageProvider key,
 | 
					    ImmichRemoteImageProvider key,
 | 
				
			||||||
    ImageDecoderCallback decode,
 | 
					    ImageDecoderCallback decode,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
 | 
					    final cache = cacheManager ?? RemoteImageCacheManager();
 | 
				
			||||||
    final chunkEvents = StreamController<ImageChunkEvent>();
 | 
					    final chunkEvents = StreamController<ImageChunkEvent>();
 | 
				
			||||||
    return MultiImageStreamCompleter(
 | 
					    return MultiImageStreamCompleter(
 | 
				
			||||||
      codec: _codec(key, decode, chunkEvents),
 | 
					      codec: _codec(key, cache, decode, chunkEvents),
 | 
				
			||||||
      scale: 1.0,
 | 
					      scale: 1.0,
 | 
				
			||||||
      chunkEvents: chunkEvents.stream,
 | 
					      chunkEvents: chunkEvents.stream,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -69,82 +66,61 @@ class ImmichRemoteImageProvider
 | 
				
			|||||||
  // 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<ui.Codec> _codec(
 | 
					  Stream<ui.Codec> _codec(
 | 
				
			||||||
    ImmichRemoteImageProvider key,
 | 
					    ImmichRemoteImageProvider key,
 | 
				
			||||||
 | 
					    ImageCacheManager cache,
 | 
				
			||||||
    ImageDecoderCallback decode,
 | 
					    ImageDecoderCallback decode,
 | 
				
			||||||
    StreamController<ImageChunkEvent> chunkEvents,
 | 
					    StreamController<ImageChunkEvent> chunkEvents,
 | 
				
			||||||
  ) async* {
 | 
					  ) async* {
 | 
				
			||||||
    // Load a preview to the chunk events
 | 
					    // Load a preview to the chunk events
 | 
				
			||||||
    if (_loadPreview || key.isThumbnail) {
 | 
					    if (_loadPreview) {
 | 
				
			||||||
      final preview = getThumbnailUrlForRemoteId(
 | 
					      final preview = getThumbnailUrlForRemoteId(
 | 
				
			||||||
        key.assetId,
 | 
					        key.assetId,
 | 
				
			||||||
        type: api.ThumbnailFormat.WEBP,
 | 
					        type: api.ThumbnailFormat.WEBP,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      yield await _loadFromUri(
 | 
					      yield await ImageLoader.loadImageFromCache(
 | 
				
			||||||
        Uri.parse(preview),
 | 
					        preview,
 | 
				
			||||||
        decode,
 | 
					        cache: cache,
 | 
				
			||||||
        chunkEvents,
 | 
					        decode: decode,
 | 
				
			||||||
 | 
					        chunkEvents: chunkEvents,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Guard thumnbail rendering
 | 
					 | 
				
			||||||
    if (key.isThumbnail) {
 | 
					 | 
				
			||||||
      await chunkEvents.close();
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Load the higher resolution version of the image
 | 
					    // Load the higher resolution version of the image
 | 
				
			||||||
    final url = getThumbnailUrlForRemoteId(
 | 
					    final url = getThumbnailUrlForRemoteId(
 | 
				
			||||||
      key.assetId,
 | 
					      key.assetId,
 | 
				
			||||||
      type: api.ThumbnailFormat.JPEG,
 | 
					      type: api.ThumbnailFormat.JPEG,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
 | 
					    final codec = await ImageLoader.loadImageFromCache(
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      cache: cache,
 | 
				
			||||||
 | 
					      decode: decode,
 | 
				
			||||||
 | 
					      chunkEvents: chunkEvents,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    yield codec;
 | 
					    yield codec;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Load the final remote image
 | 
					    // Load the final remote image
 | 
				
			||||||
    if (_useOriginal) {
 | 
					    if (_useOriginal) {
 | 
				
			||||||
      // Load the original image
 | 
					      // Load the original image
 | 
				
			||||||
      final url = getImageUrlFromId(key.assetId);
 | 
					      final url = getImageUrlFromId(key.assetId);
 | 
				
			||||||
      final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
 | 
					      final codec = await ImageLoader.loadImageFromCache(
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        cache: cache,
 | 
				
			||||||
 | 
					        decode: decode,
 | 
				
			||||||
 | 
					        chunkEvents: chunkEvents,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      yield codec;
 | 
					      yield codec;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    await chunkEvents.close();
 | 
					    await chunkEvents.close();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Loads the codec from the URI and sends the events to the [chunkEvents] stream
 | 
					 | 
				
			||||||
  Future<ui.Codec> _loadFromUri(
 | 
					 | 
				
			||||||
    Uri uri,
 | 
					 | 
				
			||||||
    ImageDecoderCallback decode,
 | 
					 | 
				
			||||||
    StreamController<ImageChunkEvent> chunkEvents,
 | 
					 | 
				
			||||||
  ) async {
 | 
					 | 
				
			||||||
    final request = await _httpClient.getUrl(uri);
 | 
					 | 
				
			||||||
    request.headers.add(
 | 
					 | 
				
			||||||
      'x-immich-user-token',
 | 
					 | 
				
			||||||
      Store.get(StoreKey.accessToken),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    final response = await request.close();
 | 
					 | 
				
			||||||
    // Chunks of the completed image can be shown
 | 
					 | 
				
			||||||
    final data = await consolidateHttpClientResponseBytes(
 | 
					 | 
				
			||||||
      response,
 | 
					 | 
				
			||||||
      onBytesReceived: (cumulative, total) {
 | 
					 | 
				
			||||||
        chunkEvents.add(
 | 
					 | 
				
			||||||
          ImageChunkEvent(
 | 
					 | 
				
			||||||
            cumulativeBytesLoaded: cumulative,
 | 
					 | 
				
			||||||
            expectedTotalBytes: total,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Decode the response
 | 
					 | 
				
			||||||
    final buffer = await ui.ImmutableBuffer.fromUint8List(data);
 | 
					 | 
				
			||||||
    return decode(buffer);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (other is! ImmichRemoteImageProvider) return false;
 | 
					 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
    return assetId == other.assetId && isThumbnail == other.isThumbnail;
 | 
					    if (other is ImmichRemoteImageProvider) {
 | 
				
			||||||
 | 
					      return assetId == other.assetId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -1,30 +1,34 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:io';
 | 
					 | 
				
			||||||
import 'dart:ui' as ui;
 | 
					import 'dart:ui' as ui;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/image_loader.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart';
 | 
				
			||||||
import 'package:openapi/api.dart' as api;
 | 
					import 'package:openapi/api.dart' as api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/painting.dart';
 | 
					import 'package:flutter/painting.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/store.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
					import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Our HTTP client to make the request
 | 
					 | 
				
			||||||
final _httpClient = HttpClient()
 | 
					 | 
				
			||||||
  ..autoUncompress = false
 | 
					 | 
				
			||||||
  ..maxConnectionsPerHost = 100;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// The remote image provider
 | 
					/// The remote image provider
 | 
				
			||||||
class ImmichRemoteThumbnailProvider
 | 
					class ImmichRemoteThumbnailProvider
 | 
				
			||||||
    extends ImageProvider<ImmichRemoteThumbnailProvider> {
 | 
					    extends ImageProvider<ImmichRemoteThumbnailProvider> {
 | 
				
			||||||
  /// The [Asset.remoteId] of the asset to fetch
 | 
					  /// The [Asset.remoteId] of the asset to fetch
 | 
				
			||||||
  final String assetId;
 | 
					  final String assetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final int? height;
 | 
				
			||||||
 | 
					  final int? width;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The image cache manager
 | 
				
			||||||
 | 
					  final ImageCacheManager? cacheManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ImmichRemoteThumbnailProvider({
 | 
					  ImmichRemoteThumbnailProvider({
 | 
				
			||||||
    required this.assetId,
 | 
					    required this.assetId,
 | 
				
			||||||
 | 
					    this.height,
 | 
				
			||||||
 | 
					    this.width,
 | 
				
			||||||
 | 
					    this.cacheManager,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
 | 
					  /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
 | 
				
			||||||
@ -41,19 +45,18 @@ class ImmichRemoteThumbnailProvider
 | 
				
			|||||||
    ImmichRemoteThumbnailProvider key,
 | 
					    ImmichRemoteThumbnailProvider key,
 | 
				
			||||||
    ImageDecoderCallback decode,
 | 
					    ImageDecoderCallback decode,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    final chunkEvents = StreamController<ImageChunkEvent>();
 | 
					    final cache = cacheManager ?? ThumbnailImageCacheManager();
 | 
				
			||||||
    return MultiImageStreamCompleter(
 | 
					    return MultiImageStreamCompleter(
 | 
				
			||||||
      codec: _codec(key, decode, chunkEvents),
 | 
					      codec: _codec(key, cache, decode),
 | 
				
			||||||
      scale: 1.0,
 | 
					      scale: 1.0,
 | 
				
			||||||
      chunkEvents: chunkEvents.stream,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 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<ui.Codec> _codec(
 | 
					  Stream<ui.Codec> _codec(
 | 
				
			||||||
    ImmichRemoteThumbnailProvider key,
 | 
					    ImmichRemoteThumbnailProvider key,
 | 
				
			||||||
 | 
					    ImageCacheManager cache,
 | 
				
			||||||
    ImageDecoderCallback decode,
 | 
					    ImageDecoderCallback decode,
 | 
				
			||||||
    StreamController<ImageChunkEvent> chunkEvents,
 | 
					 | 
				
			||||||
  ) async* {
 | 
					  ) async* {
 | 
				
			||||||
    // Load a preview to the chunk events
 | 
					    // Load a preview to the chunk events
 | 
				
			||||||
    final preview = getThumbnailUrlForRemoteId(
 | 
					    final preview = getThumbnailUrlForRemoteId(
 | 
				
			||||||
@ -61,50 +64,21 @@ class ImmichRemoteThumbnailProvider
 | 
				
			|||||||
      type: api.ThumbnailFormat.WEBP,
 | 
					      type: api.ThumbnailFormat.WEBP,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    yield await _loadFromUri(
 | 
					    yield await ImageLoader.loadImageFromCache(
 | 
				
			||||||
      Uri.parse(preview),
 | 
					      preview,
 | 
				
			||||||
      decode,
 | 
					      cache: cache,
 | 
				
			||||||
      chunkEvents,
 | 
					      decode: decode,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					 | 
				
			||||||
    await chunkEvents.close();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Loads the codec from the URI and sends the events to the [chunkEvents] stream
 | 
					 | 
				
			||||||
  Future<ui.Codec> _loadFromUri(
 | 
					 | 
				
			||||||
    Uri uri,
 | 
					 | 
				
			||||||
    ImageDecoderCallback decode,
 | 
					 | 
				
			||||||
    StreamController<ImageChunkEvent> chunkEvents,
 | 
					 | 
				
			||||||
  ) async {
 | 
					 | 
				
			||||||
    final request = await _httpClient.getUrl(uri);
 | 
					 | 
				
			||||||
    request.headers.add(
 | 
					 | 
				
			||||||
      'x-immich-user-token',
 | 
					 | 
				
			||||||
      Store.get(StoreKey.accessToken),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    final response = await request.close();
 | 
					 | 
				
			||||||
    // Chunks of the completed image can be shown
 | 
					 | 
				
			||||||
    final data = await consolidateHttpClientResponseBytes(
 | 
					 | 
				
			||||||
      response,
 | 
					 | 
				
			||||||
      onBytesReceived: (cumulative, total) {
 | 
					 | 
				
			||||||
        chunkEvents.add(
 | 
					 | 
				
			||||||
          ImageChunkEvent(
 | 
					 | 
				
			||||||
            cumulativeBytesLoaded: cumulative,
 | 
					 | 
				
			||||||
            expectedTotalBytes: total,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Decode the response
 | 
					 | 
				
			||||||
    final buffer = await ui.ImmutableBuffer.fromUint8List(data);
 | 
					 | 
				
			||||||
    return decode(buffer);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (other is! ImmichRemoteImageProvider) return false;
 | 
					 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
    return assetId == other.assetId;
 | 
					    if (other is ImmichRemoteThumbnailProvider) {
 | 
				
			||||||
 | 
					      return assetId == other.assetId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,6 @@ class ImmichImage extends StatelessWidget {
 | 
				
			|||||||
    if (asset == null) {
 | 
					    if (asset == null) {
 | 
				
			||||||
      return ImmichRemoteImageProvider(
 | 
					      return ImmichRemoteImageProvider(
 | 
				
			||||||
        assetId: assetId!,
 | 
					        assetId: assetId!,
 | 
				
			||||||
        isThumbnail: false,
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,7 +52,6 @@ class ImmichImage extends StatelessWidget {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return ImmichRemoteImageProvider(
 | 
					      return ImmichRemoteImageProvider(
 | 
				
			||||||
        assetId: asset.remoteId!,
 | 
					        assetId: asset.remoteId!,
 | 
				
			||||||
        isThumbnail: false,
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ import 'dart:typed_data';
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
 | 
					import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_image.dart';
 | 
				
			||||||
@ -38,9 +38,8 @@ class ImmichThumbnail extends HookWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (asset == null) {
 | 
					    if (asset == null) {
 | 
				
			||||||
      return ImmichRemoteImageProvider(
 | 
					      return ImmichRemoteThumbnailProvider(
 | 
				
			||||||
        assetId: assetId!,
 | 
					        assetId: assetId!,
 | 
				
			||||||
        isThumbnail: true,
 | 
					 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -51,9 +50,10 @@ class ImmichThumbnail extends HookWidget {
 | 
				
			|||||||
        width: thumbnailSize,
 | 
					        width: thumbnailSize,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return ImmichRemoteImageProvider(
 | 
					      return ImmichRemoteThumbnailProvider(
 | 
				
			||||||
        assetId: asset.remoteId!,
 | 
					        assetId: asset.remoteId!,
 | 
				
			||||||
        isThumbnail: true,
 | 
					        height: thumbnailSize,
 | 
				
			||||||
 | 
					        width: thumbnailSize,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user