feat: full local assets / album sync
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/utils/constants/globals.dart';
|
||||
|
||||
/// The cache manager for thumbnail images [ImRemoteThumbnailProvider]
|
||||
class ImRemoteThumbnailCacheManager extends CacheManager {
|
||||
static final ImRemoteThumbnailCacheManager _instance =
|
||||
ImRemoteThumbnailCacheManager._();
|
||||
|
||||
factory ImRemoteThumbnailCacheManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
ImRemoteThumbnailCacheManager._()
|
||||
: super(
|
||||
Config(
|
||||
kCacheThumbnailsKey,
|
||||
maxNrOfCacheObjects: kCacheMaxNrOfThumbnails,
|
||||
stalePeriod: const Duration(days: kCacheStalePeriod),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The cache manager for full size images [ImRemoteImageProvider]
|
||||
class ImRemoteImageCacheManager extends CacheManager {
|
||||
static final ImRemoteImageCacheManager _instance =
|
||||
ImRemoteImageCacheManager._();
|
||||
|
||||
factory ImRemoteImageCacheManager() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
ImRemoteImageCacheManager._()
|
||||
: super(
|
||||
Config(
|
||||
kCacheFullImagesKey,
|
||||
maxNrOfCacheObjects: kCacheMaxNrOfFullImages,
|
||||
stalePeriod: const Duration(days: kCacheStalePeriod),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||
|
||||
/// An exception for the [ImageLoader] and the Immich image providers
|
||||
class ImageLoadingException implements Exception {
|
||||
final String message;
|
||||
const ImageLoadingException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'ImageLoadingException: $message';
|
||||
}
|
||||
|
||||
/// 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 CacheManager cache,
|
||||
required ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent>? chunkEvents,
|
||||
}) async {
|
||||
final stream = cache.getFileStream(
|
||||
uri,
|
||||
withProgress: chunkEvents != null,
|
||||
headers: di<ImApiClient>().headers,
|
||||
);
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
} else if (result is FileInfo) {
|
||||
// We have the file
|
||||
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
|
||||
final decoded = await decode(buffer);
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, the image failed to load from the cache stream
|
||||
throw const ImageLoadingException('Could not load image from stream');
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,89 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||
import 'package:immich_mobile/utils/immich_image_url_helper.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/provider/immich_local_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/provider/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
|
||||
class ImImagePlaceholder extends StatelessWidget {
|
||||
const ImImagePlaceholder({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 200,
|
||||
height: 200,
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: prefer-single-widget-per-file
|
||||
class ImImage extends StatelessWidget {
|
||||
final Asset asset;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Widget placeholder;
|
||||
|
||||
const ImImage(this.asset, {this.width, this.height, super.key});
|
||||
const ImImage(
|
||||
this.asset, {
|
||||
this.width,
|
||||
this.height,
|
||||
this.placeholder = const ImImagePlaceholder(),
|
||||
super.key,
|
||||
});
|
||||
|
||||
// Helper function to return the image provider for the asset
|
||||
// either by using the asset ID or the asset itself
|
||||
/// [asset] is the Asset to request, or else use [assetId] to get a remote
|
||||
/// image provider
|
||||
/// Use [isThumbnail] and [thumbnailSize] if you'd like to request a thumbnail
|
||||
/// The size of the square thumbnail to request. Ignored if isThumbnail
|
||||
/// is not true
|
||||
static ImageProvider imageProvider({Asset? asset, String? assetId}) {
|
||||
if (asset == null && assetId == null) {
|
||||
throw Exception('Must supply either asset or assetId');
|
||||
}
|
||||
|
||||
if (asset == null) {
|
||||
return ImRemoteImageProvider(assetId: assetId!);
|
||||
}
|
||||
|
||||
// Whether to use the local asset image provider or a remote one
|
||||
final useLocal = !asset.isRemote || asset.isLocal;
|
||||
|
||||
if (useLocal) {
|
||||
return ImLocalImageProvider(asset: asset);
|
||||
}
|
||||
|
||||
return ImRemoteImageProvider(assetId: asset.remoteId!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: ImImageUrlHelper.getThumbnailUrl(asset),
|
||||
httpHeaders: di<ImmichApiClient>().headers,
|
||||
cacheKey: ImImageUrlHelper.getThumbnailUrl(asset),
|
||||
return OctoImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 200),
|
||||
placeholderBuilder: (_) => placeholder,
|
||||
image: ImImage.imageProvider(asset: asset),
|
||||
width: width,
|
||||
height: height,
|
||||
// keeping memCacheWidth, memCacheHeight, maxWidthDiskCache and
|
||||
// maxHeightDiskCache = null allows to simply store the webp thumbnail
|
||||
// from the server and use it for all rendered thumbnail sizes
|
||||
fit: BoxFit.cover,
|
||||
fadeInDuration: const Duration(milliseconds: 250),
|
||||
progressIndicatorBuilder: (_, url, downloadProgress) {
|
||||
// Show loading if desired
|
||||
return const SizedBox.square(
|
||||
dimension: 250,
|
||||
child: DecoratedBox(decoration: BoxDecoration(color: Colors.grey)),
|
||||
);
|
||||
},
|
||||
errorWidget: (_, url, error) {
|
||||
if (error is HttpExceptionWithStatus &&
|
||||
error.statusCode >= 400 &&
|
||||
error.statusCode < 500) {
|
||||
CachedNetworkImage.evictFromCache(url);
|
||||
errorBuilder: (_, error, stackTrace) {
|
||||
if (error is PlatformException &&
|
||||
error.code == "The asset not found!") {
|
||||
debugPrint(
|
||||
"Asset ${asset.localId ?? asset.id ?? "-"} does not exist anymore on device!",
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
"Error getting thumb for assetId=${asset.localId ?? asset.id ?? "-"}: $error",
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
Symbols.image_not_supported_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
Icons.image_not_supported_outlined,
|
||||
color: context.colorScheme.primary,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ class ImLogo extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: prefer-single-widget-per-file
|
||||
class ImLogoText extends StatelessWidget {
|
||||
const ImLogoText({
|
||||
super.key,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
IconData _getStorageIcon(Asset asset) {
|
||||
if (asset.isMerged) {
|
||||
return Symbols.cloud_done_rounded;
|
||||
} else if (asset.isRemote) {
|
||||
return Symbols.cloud_rounded;
|
||||
}
|
||||
return Symbols.cloud_off_rounded;
|
||||
}
|
||||
|
||||
class ImThumbnail extends StatelessWidget {
|
||||
final Asset asset;
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const ImThumbnail(this.asset, {this.width, this.height, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(child: ImImage(asset, width: width, height: height)),
|
||||
_PadAlignedIcon(
|
||||
alignment: Alignment.bottomRight,
|
||||
icon: _getStorageIcon(asset),
|
||||
),
|
||||
if (!asset.isImage)
|
||||
const _PadAlignedIcon(
|
||||
alignment: Alignment.topLeft,
|
||||
icon: Symbols.play_circle_rounded,
|
||||
filled: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PadAlignedIcon extends StatelessWidget {
|
||||
final Alignment alignment;
|
||||
final IconData icon;
|
||||
final bool? filled;
|
||||
|
||||
const _PadAlignedIcon({
|
||||
required this.alignment,
|
||||
required this.icon,
|
||||
this.filled,
|
||||
});
|
||||
|
||||
double _calculateLeft(Alignment align) {
|
||||
return align.x == -1 ? 5 : 0;
|
||||
}
|
||||
|
||||
double _calculateTop(Alignment align) {
|
||||
return align.y == -1 ? 4 : 0;
|
||||
}
|
||||
|
||||
double _calculateRight(Alignment align) {
|
||||
return align.x == 1 ? 5 : 0;
|
||||
}
|
||||
|
||||
double _calculateBottom(Alignment align) {
|
||||
return align.y == 1 ? 4 : 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
left: _calculateLeft(alignment),
|
||||
top: _calculateTop(alignment),
|
||||
right: _calculateRight(alignment),
|
||||
bottom: _calculateBottom(alignment),
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
fill: (filled != null && filled!) ? 1 : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/extensions/file.extension.dart';
|
||||
|
||||
/// The local image provider for an asset
|
||||
class ImLocalImageProvider extends ImageProvider<ImLocalImageProvider> {
|
||||
final Asset asset;
|
||||
|
||||
ImLocalImageProvider({required this.asset})
|
||||
: assert(asset.localId != null, 'Only usable when asset.local is set');
|
||||
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<ImLocalImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ImLocalImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key.asset, decode, chunkEvents),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription(asset.name);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
Asset a,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async* {
|
||||
// Load a small thumbnail
|
||||
final thumbBytes =
|
||||
await di<IDeviceAssetRepository>().getThumbnail(a.localId!);
|
||||
if (thumbBytes != null) {
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
} else {
|
||||
debugPrint("Loading thumb for ${a.name} failed");
|
||||
}
|
||||
|
||||
if (asset.isImage) {
|
||||
/// Using 2K thumbnail for local iOS image to avoid double swiping issue
|
||||
if (Platform.isIOS) {
|
||||
final largeImageBytes = await di<IDeviceAssetRepository>()
|
||||
.getThumbnail(a.localId!, width: 3840, height: 2160);
|
||||
|
||||
if (largeImageBytes == null) {
|
||||
throw StateError("Loading thumb for local photo ${a.name} failed");
|
||||
}
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(largeImageBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
} else {
|
||||
// Use the original file for Android
|
||||
final File? file =
|
||||
await di<IDeviceAssetRepository>().getOriginalFile(a.localId!);
|
||||
if (file == null) {
|
||||
throw StateError("Opening file for asset ${a.name} failed");
|
||||
}
|
||||
try {
|
||||
final buffer = await ui.ImmutableBuffer.fromFilePath(file.path);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
} catch (error, stack) {
|
||||
Error.throwWithStackTrace(
|
||||
StateError("Loading asset ${a.name} failed"),
|
||||
stack,
|
||||
);
|
||||
} finally {
|
||||
await file.deleteDarwinCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await chunkEvents.close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is ImLocalImageProvider) {
|
||||
return asset == other.asset;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => asset.hashCode;
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||
import 'package:immich_mobile/service_locator.dart';
|
||||
import 'package:immich_mobile/utils/constants/globals.dart';
|
||||
|
||||
/// The local image provider for an asset
|
||||
/// Only viable
|
||||
class ImLocalThumbnailProvider extends ImageProvider<ImLocalThumbnailProvider> {
|
||||
final Asset asset;
|
||||
final int height;
|
||||
final int width;
|
||||
|
||||
ImLocalThumbnailProvider({
|
||||
required this.asset,
|
||||
this.height = kGridThumbnailSize,
|
||||
this.width = kGridThumbnailSize,
|
||||
}) : assert(asset.localId != null, 'Only usable when asset.local is set');
|
||||
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<ImLocalThumbnailProvider> obtainKey(
|
||||
ImageConfiguration configuration,
|
||||
) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ImLocalThumbnailProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key.asset, decode, chunkEvents),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription(asset.name);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
Asset a,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async* {
|
||||
// Load a small thumbnail
|
||||
final thumbBytes = await di<IDeviceAssetRepository>()
|
||||
.getThumbnail(a.localId!, width: 32, height: 32, quality: 75);
|
||||
if (thumbBytes != null) {
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
} else {
|
||||
debugPrint("Loading thumb for ${a.name} failed");
|
||||
}
|
||||
|
||||
final normalThumbBytes = await di<IDeviceAssetRepository>()
|
||||
.getThumbnail(a.localId!, width: width, height: height);
|
||||
if (normalThumbBytes == null) {
|
||||
throw StateError("Loading thumb for local photo ${a.name} failed");
|
||||
}
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes);
|
||||
final codec = await decode(buffer);
|
||||
yield codec;
|
||||
|
||||
await chunkEvents.close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is! ImLocalThumbnailProvider) return false;
|
||||
if (identical(this, other)) return true;
|
||||
return asset == other.asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => asset.hashCode;
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/cache/cache_manager.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/cache/image_loader.dart';
|
||||
import 'package:immich_mobile/utils/immich_image_url_helper.dart';
|
||||
|
||||
/// The remote image provider for full size remote images
|
||||
class ImRemoteImageProvider extends ImageProvider<ImRemoteImageProvider> {
|
||||
/// The [Asset.remoteId] of the asset to fetch
|
||||
final String assetId;
|
||||
|
||||
/// The image cache manager
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
const ImRemoteImageProvider({required this.assetId, this.cacheManager});
|
||||
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<ImRemoteImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ImRemoteImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final cache = cacheManager ?? ImRemoteImageCacheManager();
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode, chunkEvents),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
);
|
||||
}
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
ImRemoteImageProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkEvents,
|
||||
) async* {
|
||||
// Load a preview to the chunk events
|
||||
final preview = ImImageUrlHelper.getThumbnailUrlForRemoteId(key.assetId);
|
||||
|
||||
yield await ImageLoader.loadImageFromCache(
|
||||
preview,
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
chunkEvents: chunkEvents,
|
||||
);
|
||||
|
||||
// Load the higher resolution version of the image
|
||||
final url = ImImageUrlHelper.getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
type: AssetMediaSize.preview,
|
||||
);
|
||||
final codec = await ImageLoader.loadImageFromCache(
|
||||
url,
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
chunkEvents: chunkEvents,
|
||||
);
|
||||
yield codec;
|
||||
|
||||
await chunkEvents.close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is ImRemoteImageProvider) {
|
||||
return assetId == other.assetId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode;
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/cache/cache_manager.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/cache/image_loader.dart';
|
||||
import 'package:immich_mobile/utils/immich_image_url_helper.dart';
|
||||
|
||||
/// The remote image provider
|
||||
class ImmichRemoteThumbnailProvider
|
||||
extends ImageProvider<ImmichRemoteThumbnailProvider> {
|
||||
/// The [Asset.remoteId] of the asset to fetch
|
||||
final String assetId;
|
||||
|
||||
final int? height;
|
||||
final int? width;
|
||||
|
||||
/// The image cache manager
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
const ImmichRemoteThumbnailProvider({
|
||||
required this.assetId,
|
||||
this.height,
|
||||
this.width,
|
||||
this.cacheManager,
|
||||
});
|
||||
|
||||
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
|
||||
/// that describes the precise image to load.
|
||||
@override
|
||||
Future<ImmichRemoteThumbnailProvider> obtainKey(
|
||||
ImageConfiguration configuration,
|
||||
) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ImmichRemoteThumbnailProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final cache = cacheManager ?? ImRemoteThumbnailCacheManager();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode),
|
||||
scale: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<ui.Codec> _codec(
|
||||
ImmichRemoteThumbnailProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
) async* {
|
||||
final preview = ImImageUrlHelper.getThumbnailUrlForRemoteId(key.assetId);
|
||||
|
||||
yield await ImageLoader.loadImageFromCache(
|
||||
preview,
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is ImmichRemoteThumbnailProvider) {
|
||||
return assetId == other.assetId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode;
|
||||
}
|
||||
Reference in New Issue
Block a user