Refactor to use ImmichThumbnail and local thumbnail image provider

format
This commit is contained in:
Marty Fuhry 2024-02-20 21:10:53 -05:00
parent 73825918c0
commit 718c258a07
No known key found for this signature in database
GPG Key ID: E2AB6392D894D900
13 changed files with 278 additions and 104 deletions

View File

@ -4,6 +4,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
class AlbumThumbnailCard extends StatelessWidget { class AlbumThumbnailCard extends StatelessWidget {
final Function()? onTap; final Function()? onTap;
@ -45,7 +46,7 @@ class AlbumThumbnailCard extends StatelessWidget {
); );
} }
buildAlbumThumbnail() => ImmichImage.thumbnail( buildAlbumThumbnail() => ImmichThumbnail(asset:
album.thumbnail.value, album.thumbnail.value,
width: cardSize, width: cardSize,
height: cardSize, height: cardSize,

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
class SharedAlbumThumbnailImage extends HookConsumerWidget { class SharedAlbumThumbnailImage extends HookConsumerWidget {
final Asset asset; final Asset asset;
@ -16,8 +16,8 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
}, },
child: Stack( child: Stack(
children: [ children: [
ImmichImage.thumbnail( ImmichThumbnail(
asset, asset: asset,
width: 500, width: 500,
height: 500, height: 500,
), ),

View File

@ -13,6 +13,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
@RoutePage() @RoutePage()
class SharingPage extends HookConsumerWidget { class SharingPage extends HookConsumerWidget {
@ -72,8 +73,8 @@ class SharingPage extends HookConsumerWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12),
leading: ClipRRect( leading: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: ImmichImage.thumbnail( child: ImmichThumbnail(
album.thumbnail.value, asset: album.thumbnail.value,
width: 60, width: 60,
height: 60, height: 60,
), ),

View File

@ -11,7 +11,7 @@ import 'package:photo_manager/photo_manager.dart';
/// The local image provider for an asset /// The local image provider for an asset
/// Only viable /// Only viable
class ImmichLocalImageProvider extends ImageProvider<Asset> { class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
final Asset asset; final Asset asset;
ImmichLocalImageProvider({ ImmichLocalImageProvider({
@ -21,15 +21,18 @@ class ImmichLocalImageProvider extends ImageProvider<Asset> {
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load. /// that describes the precise image to load.
@override @override
Future<Asset> obtainKey(ImageConfiguration configuration) { Future<ImmichLocalImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(asset); return SynchronousFuture(this);
} }
@override @override
ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) { ImageStreamCompleter loadImage(
ImmichLocalImageProvider key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>(); final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter( return MultiImageStreamCompleter(
codec: _codec(key, decode, chunkEvents), codec: _codec(key.asset, decode, chunkEvents),
scale: 1.0, scale: 1.0,
chunkEvents: chunkEvents.stream, chunkEvents: chunkEvents.stream,
informationCollector: () sync* { informationCollector: () sync* {
@ -82,11 +85,6 @@ class ImmichLocalImageProvider extends ImageProvider<Asset> {
yield codec; yield codec;
} catch (error) { } catch (error) {
throw StateError("Loading asset ${asset.fileName} failed"); throw StateError("Loading asset ${asset.fileName} failed");
} finally {
if (Platform.isIOS) {
// Clean up this file
await file.delete();
}
} }
} }
} }

View File

@ -0,0 +1,86 @@
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/shared/models/asset.dart';
import 'package:photo_manager/photo_manager.dart';
/// The local image provider for an asset
/// Only viable
class ImmichLocalThumbnailProvider extends ImageProvider<Asset> {
final Asset asset;
final int height;
final int width;
ImmichLocalThumbnailProvider({
required this.asset,
this.height = 256,
this.width = 256,
}) : assert(asset.local != 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<Asset> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(asset);
}
@override
ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription(asset.fileName);
},
);
}
// Streams in each stage of the image as we ask for it
Stream<ui.Codec> _codec(
Asset key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// Load a small thumbnail
final thumbBytes = await asset.local?.thumbnailDataWithSize(
const ThumbnailSize.square(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 ${asset.fileName} failed");
}
final normalThumbBytes = await asset.local
?.thumbnailDataWithSize(ThumbnailSize(width, height));
if (normalThumbBytes == null) {
throw StateError(
"Loading thumb for local photo ${asset.fileName} failed",
);
}
final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes);
final codec = await decode(buffer);
yield codec;
chunkEvents.close();
}
@override
bool operator ==(Object other) {
if (other is! ImmichLocalThumbnailProvider) return false;
if (identical(this, other)) return true;
return asset == other.asset;
}
@override
int get hashCode => asset.hashCode;
}

View File

@ -16,7 +16,8 @@ import 'package:immich_mobile/utils/image_url_builder.dart';
final _httpClient = HttpClient()..autoUncompress = false; final _httpClient = HttpClient()..autoUncompress = false;
/// The remote image provider /// The remote image provider
class ImmichRemoteImageProvider extends ImageProvider<String> { class 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;
@ -32,16 +33,20 @@ class ImmichRemoteImageProvider extends ImageProvider<String> {
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load. /// that describes the precise image to load.
@override @override
Future<String> obtainKey(ImageConfiguration configuration) { Future<ImmichRemoteImageProvider> obtainKey(
return SynchronousFuture('$assetId,$isThumbnail'); ImageConfiguration configuration,
) {
return SynchronousFuture(this);
} }
@override @override
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { ImageStreamCompleter loadImage(
final id = key.split(',').first; ImmichRemoteImageProvider key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>(); final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter( return MultiImageStreamCompleter(
codec: _codec(id, decode, chunkEvents), codec: _codec(key, decode, chunkEvents),
scale: 1.0, scale: 1.0,
chunkEvents: chunkEvents.stream, chunkEvents: chunkEvents.stream,
); );
@ -61,14 +66,14 @@ class ImmichRemoteImageProvider extends ImageProvider<String> {
// 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(
String key, ImmichRemoteImageProvider key,
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 || isThumbnail) { if (_loadPreview || key.isThumbnail) {
final preview = getThumbnailUrlForRemoteId( final preview = getThumbnailUrlForRemoteId(
assetId, key.assetId,
type: api.ThumbnailFormat.WEBP, type: api.ThumbnailFormat.WEBP,
); );
@ -80,14 +85,14 @@ class ImmichRemoteImageProvider extends ImageProvider<String> {
} }
// Guard thumnbail rendering // Guard thumnbail rendering
if (isThumbnail) { if (key.isThumbnail) {
await chunkEvents.close(); await chunkEvents.close();
return; return;
} }
// Load the higher resolution version of the image // Load the higher resolution version of the image
final url = getThumbnailUrlForRemoteId( final url = getThumbnailUrlForRemoteId(
assetId, key.assetId,
type: api.ThumbnailFormat.JPEG, type: api.ThumbnailFormat.JPEG,
); );
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
@ -96,7 +101,7 @@ class ImmichRemoteImageProvider extends ImageProvider<String> {
// 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(assetId); final url = getImageUrlFromId(key.assetId);
final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents);
yield codec; yield codec;
} }
@ -137,7 +142,7 @@ class ImmichRemoteImageProvider extends ImageProvider<String> {
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! ImmichRemoteImageProvider) return false; if (other is! ImmichRemoteImageProvider) return false;
if (identical(this, other)) return true; if (identical(this, other)) return true;
return assetId == other.assetId; return assetId == other.assetId && isThumbnail == other.isThumbnail;
} }
@override @override

View File

@ -13,7 +13,7 @@ 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';
/// The remote image provider /// The remote image provider
class ImmichRemoteThumbnailProvider extends ImageProvider<String> { class 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;
@ -27,12 +27,12 @@ class ImmichRemoteThumbnailProvider extends ImageProvider<String> {
/// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key
/// that describes the precise image to load. /// that describes the precise image to load.
@override @override
Future<String> obtainKey(ImageConfiguration configuration) { Future<ImmichRemoteThumbnailProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture(assetId); return SynchronousFuture(this);
} }
@override @override
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { ImageStreamCompleter loadImage(ImmichRemoteThumbnailProvider key, ImageDecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>(); final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter( return MultiImageStreamCompleter(
codec: _codec(key, decode, chunkEvents), codec: _codec(key, decode, chunkEvents),
@ -43,13 +43,13 @@ class ImmichRemoteThumbnailProvider extends ImageProvider<String> {
// 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(
String key, ImmichRemoteThumbnailProvider key,
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
final preview = getThumbnailUrlForRemoteId( final preview = getThumbnailUrlForRemoteId(
assetId, key.assetId,
type: api.ThumbnailFormat.WEBP, type: api.ThumbnailFormat.WEBP,
); );

View File

@ -1,8 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
@ -10,6 +10,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/modules/album/providers/current_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.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/modules/asset_viewer/providers/show_controls.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
@ -33,6 +34,7 @@ import 'package:immich_mobile/modules/settings/services/app_settings.service.dar
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart'; import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart';
@ -481,15 +483,9 @@ class GalleryViewerPage extends HookConsumerWidget {
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage( child: Image(
fit: BoxFit.cover, fit: BoxFit.cover,
imageUrl: image: ImmichRemoteImageProvider(assetId: assetId!),
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId',
httpHeaders: {
"x-immich-user-token": Store.get(StoreKey.accessToken),
},
errorWidget: (context, url, error) =>
const Icon(Icons.image_not_supported_outlined),
), ),
), ),
), ),
@ -728,9 +724,15 @@ class GalleryViewerPage extends HookConsumerWidget {
isZoomed.value = state != PhotoViewScaleState.initial; isZoomed.value = state != PhotoViewScaleState.initial;
ref.read(showControlsProvider.notifier).show = !isZoomed.value; ref.read(showControlsProvider.notifier).show = !isZoomed.value;
}, },
loadingBuilder: (context, event, index) => ImmichImage.thumbnail( loadingBuilder: (context, event, index) => BackdropFilter(
asset(), filter: ui.ImageFilter.blur(
fit: BoxFit.contain, sigmaX: 0.2,
sigmaY: 0.2,
),
child: ImmichThumbnail(
asset: asset(),
fit: BoxFit.contain,
),
), ),
pageController: controller, pageController: controller,
scrollPhysics: isZoomed.value scrollPhysics: isZoomed.value

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
import 'package:immich_mobile/utils/storage_indicator.dart'; import 'package:immich_mobile/utils/storage_indicator.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
@ -134,10 +135,10 @@ class ThumbnailImage extends StatelessWidget {
tag: isFromDto tag: isFromDto
? '${asset.remoteId}-$heroOffset' ? '${asset.remoteId}-$heroOffset'
: asset.id + heroOffset, : asset.id + heroOffset,
child: ImmichImage.thumbnail( child: ImmichThumbnail(
asset, asset: asset,
height: 300, height: 250,
width: 300, width: 250,
), ),
), ),
); );

View File

@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.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/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
class MemoryCard extends StatelessWidget { class MemoryCard extends StatelessWidget {
final Asset asset; final Asset asset;
@ -42,9 +43,8 @@ class MemoryCard extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: ImmichImage.imageProvider( image: ImmichThumbnail.imageProvider(
asset: asset, asset: asset,
isThumbnail: true,
), ),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),

View File

@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart'; import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
@RoutePage() @RoutePage()
class MemoryPage extends HookConsumerWidget { class MemoryPage extends HookConsumerWidget {
@ -120,9 +121,8 @@ class MemoryPage extends HookConsumerWidget {
context, context,
), ),
precacheImage( precacheImage(
ImmichImage.imageProvider( ImmichThumbnail.imageProvider(
asset: asset, asset: asset,
isThumbnail: true,
), ),
context, context,
), ),

View File

@ -19,8 +19,6 @@ class ImmichImage extends StatelessWidget {
this.height, this.height,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.placeholder = const ThumbnailPlaceholder(), this.placeholder = const ThumbnailPlaceholder(),
this.isThumbnail = false,
this.thumbnailSize = 250,
super.key, super.key,
}); });
@ -29,32 +27,6 @@ class ImmichImage extends StatelessWidget {
final double? width; final double? width;
final double? height; final double? height;
final BoxFit fit; final BoxFit fit;
final bool isThumbnail;
final int thumbnailSize;
/// Factory constructor to use the thumbnail variant
factory ImmichImage.thumbnail(
Asset? asset, {
BoxFit fit = BoxFit.cover,
double? width,
double? height,
}) {
// Use the width and height to derive thumbnail size
final thumbnailSize = max(width ?? 250, height ?? 250).toInt();
return ImmichImage(
asset,
isThumbnail: true,
fit: fit,
width: width,
height: height,
placeholder: ThumbnailPlaceholder(
height: thumbnailSize.toDouble(),
width: thumbnailSize.toDouble(),
),
thumbnailSize: thumbnailSize,
);
}
// Helper function to return the image provider for the asset // Helper function to return the image provider for the asset
// either by using the asset ID or the asset itself // either by using the asset ID or the asset itself
@ -66,34 +38,29 @@ class ImmichImage extends StatelessWidget {
static ImageProvider imageProvider({ static ImageProvider imageProvider({
Asset? asset, Asset? asset,
String? assetId, String? assetId,
bool isThumbnail = false,
int thumbnailSize = 250,
}) { }) {
if (asset == null && assetId == null) { if (asset == null && assetId == null) {
throw Exception('Must supply either asset or assetId'); throw Exception('Must supply either asset or assetId');
} }
if (asset == null) { if (asset == null) {
print('using remote for $assetId');
return ImmichRemoteImageProvider( return ImmichRemoteImageProvider(
assetId: assetId!, assetId: assetId!,
isThumbnail: isThumbnail, isThumbnail: false,
); );
} }
if (useLocal(asset) && isThumbnail) { if (useLocal(asset)) {
return AssetEntityImageProvider( print('using local for ${asset.localId}');
asset.local!,
isOriginal: false,
thumbnailSize: ThumbnailSize.square(thumbnailSize),
);
} else if (useLocal(asset) && !isThumbnail) {
return ImmichLocalImageProvider( return ImmichLocalImageProvider(
asset: asset, asset: asset,
); );
} else { } else {
print('using remote for ${asset.localId}');
return ImmichRemoteImageProvider( return ImmichRemoteImageProvider(
assetId: asset.remoteId!, assetId: asset.remoteId!,
isThumbnail: isThumbnail, isThumbnail: false,
); );
} }
} }
@ -105,15 +72,11 @@ class ImmichImage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (asset == null) { if (asset == null) {
return Container( return Container(
decoration: const BoxDecoration( color: Colors.grey,
color: Colors.grey, width: width,
), height: height,
child: SizedBox( child: const Center(
width: width, child: Icon(Icons.no_photography),
height: height,
child: const Center(
child: Icon(Icons.no_photography),
),
), ),
); );
} }
@ -131,7 +94,6 @@ class ImmichImage extends StatelessWidget {
}, },
image: ImmichImage.imageProvider( image: ImmichImage.imageProvider(
asset: asset, asset: asset,
isThumbnail: isThumbnail,
), ),
width: width, width: width,
height: height, height: height,

View File

@ -0,0 +1,118 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_image_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/home/ui/asset_grid/thumbnail_placeholder.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:octo_image/octo_image.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
class ImmichThumbnail extends StatelessWidget {
const ImmichThumbnail({
this.asset,
this.width = 250,
this.height = 250,
this.fit = BoxFit.cover,
this.placeholder,
super.key,
});
final Asset? asset;
final Widget? placeholder;
final double width;
final double height;
final BoxFit fit;
// 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,
int thumbnailSize = 256,
}) {
if (asset == null && assetId == null) {
throw Exception('Must supply either asset or assetId');
}
if (asset == null) {
return ImmichRemoteImageProvider(
assetId: assetId!,
isThumbnail: true,
);
}
if (useLocal(asset)) {
return ImmichLocalThumbnailProvider(
asset: asset,
height: thumbnailSize,
width: thumbnailSize,
);
} else {
return ImmichRemoteImageProvider(
assetId: asset.remoteId!,
isThumbnail: true,
);
}
}
static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal;
@override
Widget build(BuildContext context) {
if (asset == null) {
return Container(
color: Colors.grey,
width: width,
height: height,
child: const Center(
child: Icon(Icons.no_photography),
),
);
}
return OctoImage(
fadeInDuration: const Duration(milliseconds: 0),
fadeOutDuration: const Duration(milliseconds: 100),
placeholderBuilder: (context) {
return placeholder ??
ThumbnailPlaceholder(
height: height,
width: width,
);
},
image: ImmichThumbnail.imageProvider(
asset: asset,
),
width: width,
height: height,
fit: fit,
errorBuilder: (context, error, stackTrace) {
if (error is PlatformException &&
error.code == "The asset not found!") {
debugPrint(
"Asset ${asset?.localId} does not exist anymore on device!",
);
} else {
debugPrint(
"Error getting thumb for assetId=${asset?.localId}: $error",
);
}
return Icon(
Icons.image_not_supported_outlined,
color: context.primaryColor,
);
},
);
}
}