mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
refactor(mobile): Immich image provider (#7016)
* Adds image provider * uses image provider * wip load preview * wip everything but activity asset thumbnail needs some help with a remote id * Immich provider used in gallery * First draft of the immich image provider, working nicely! * Removed OriginalImageProvider * Fixes for thumbnails * feat(mobile): thumbhash support (#7028) * feat(mobile): thumbhash support * perf(mobile): store bmp thumbhash bytes in Isar --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> * Uses octoimage for fade in and placeholders * fixes thumbnails, removes unused values, adds better thumbnail size * removes thumbhash support for now * Forgot one thumbhash removal * Use big thumbnail for local image on ios * fix(mobile): Multipart image loading for iOS double swipe (#7064) * uses local thumb first * Multipart thumbnail * Clean up file delete * await file delete * Fynn's comments, made thumbnail smaller and doesn't crash on erroring out on thumbnail * lint --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm> Co-authored-by: Alex <alex.tran1502@gmail.com> * Moves http client to global private place for reuse * Got rid of usePreview for local image providers since we always show a thumbnail anyway first * linter --------- Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Marty Fuhry <marty@fuhry.farm>
This commit is contained in:
parent
4b3f8d1946
commit
9b4a770b9d
21 changed files with 540 additions and 364 deletions
|
|
@ -1,16 +1,16 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.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_remote_image_provider.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:octo_image/octo_image.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:openapi/api.dart' as api;
|
||||
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
|
||||
|
||||
/// Renders an Asset using local data if available, else remote data
|
||||
class ImmichImage extends StatelessWidget {
|
||||
const ImmichImage(
|
||||
this.asset, {
|
||||
|
|
@ -18,23 +18,89 @@ class ImmichImage extends StatelessWidget {
|
|||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
this.useGrayBoxPlaceholder = false,
|
||||
this.useProgressIndicator = false,
|
||||
this.type = api.ThumbnailFormat.WEBP,
|
||||
this.preferredLocalAssetSize = 250,
|
||||
this.isThumbnail = false,
|
||||
this.thumbnailSize = 250,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Asset? asset;
|
||||
final bool useGrayBoxPlaceholder;
|
||||
final bool useProgressIndicator;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
final api.ThumbnailFormat type;
|
||||
final int preferredLocalAssetSize;
|
||||
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,
|
||||
useGrayBoxPlaceholder: true,
|
||||
thumbnailSize: thumbnailSize,
|
||||
);
|
||||
}
|
||||
|
||||
// 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,
|
||||
bool isThumbnail = false,
|
||||
int thumbnailSize = 250,
|
||||
}) {
|
||||
if (asset == null && assetId == null) {
|
||||
throw Exception('Must supply either asset or assetId');
|
||||
}
|
||||
|
||||
if (asset == null) {
|
||||
return ImmichRemoteImageProvider(
|
||||
assetId: assetId!,
|
||||
isThumbnail: isThumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
if (useLocal(asset) && isThumbnail) {
|
||||
return AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: ThumbnailSize.square(thumbnailSize),
|
||||
);
|
||||
} else if (useLocal(asset) && !isThumbnail) {
|
||||
return ImmichLocalImageProvider(
|
||||
asset: asset,
|
||||
);
|
||||
} else {
|
||||
return ImmichRemoteImageProvider(
|
||||
assetId: asset.remoteId!,
|
||||
isThumbnail: isThumbnail,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool useLocal(Asset asset) =>
|
||||
!asset.isRemote ||
|
||||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (this.asset == null) {
|
||||
|
||||
if (asset == null) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.grey,
|
||||
|
|
@ -48,96 +114,39 @@ class ImmichImage extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
final Asset asset = this.asset!;
|
||||
if (useLocal(asset)) {
|
||||
return Image(
|
||||
image: localImageProvider(asset, size: preferredLocalAssetSize),
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
|
||||
// Show loading if desired
|
||||
return Stack(
|
||||
children: [
|
||||
if (useGrayBoxPlaceholder)
|
||||
const SizedBox.square(
|
||||
dimension: 250,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
if (useProgressIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
return OctoImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 400),
|
||||
placeholderBuilder: (context) {
|
||||
if (useGrayBoxPlaceholder) {
|
||||
// Use the gray box placeholder
|
||||
return const SizedBox.expand(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
},
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
final String? accessToken = Store.get(StoreKey.accessToken);
|
||||
final String thumbnailRequestUrl = getThumbnailUrl(asset, type: type);
|
||||
return CachedNetworkImage(
|
||||
imageUrl: thumbnailRequestUrl,
|
||||
httpHeaders: {"x-immich-user-token": accessToken ?? ""},
|
||||
cacheKey: getThumbnailCacheKey(asset, type: type),
|
||||
}
|
||||
// No placeholder
|
||||
return const SizedBox();
|
||||
},
|
||||
image: ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
isThumbnail: isThumbnail,
|
||||
),
|
||||
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: fit,
|
||||
fadeInDuration: const Duration(milliseconds: 250),
|
||||
progressIndicatorBuilder: (context, url, downloadProgress) {
|
||||
// Show loading if desired
|
||||
return Stack(
|
||||
children: [
|
||||
if (useGrayBoxPlaceholder)
|
||||
const SizedBox.square(
|
||||
dimension: 250,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
if (useProgressIndicator)
|
||||
Transform.scale(
|
||||
scale: 2,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 1,
|
||||
value: downloadProgress.progress,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
if (error is HttpExceptionWithStatus &&
|
||||
error.statusCode >= 400 &&
|
||||
error.statusCode < 500) {
|
||||
debugPrint("Evicting thumbnail '$url' from cache: $error");
|
||||
CachedNetworkImage.evictFromCache(url);
|
||||
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,
|
||||
|
|
@ -146,65 +155,4 @@ class ImmichImage extends StatelessWidget {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
static AssetEntityImageProvider localImageProvider(
|
||||
Asset asset, {
|
||||
int size = 250,
|
||||
}) =>
|
||||
AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: ThumbnailSize.square(size),
|
||||
);
|
||||
|
||||
static CachedNetworkImageProvider remoteThumbnailProvider(
|
||||
Asset asset,
|
||||
api.ThumbnailFormat type,
|
||||
Map<String, String> authHeader,
|
||||
) =>
|
||||
CachedNetworkImageProvider(
|
||||
getThumbnailUrl(asset, type: type),
|
||||
cacheKey: getThumbnailCacheKey(asset, type: type),
|
||||
headers: authHeader,
|
||||
);
|
||||
|
||||
/// TODO: refactor image providers to separate class
|
||||
static CachedNetworkImageProvider remoteThumbnailProviderForId(
|
||||
String assetId, {
|
||||
api.ThumbnailFormat type = api.ThumbnailFormat.WEBP,
|
||||
}) =>
|
||||
CachedNetworkImageProvider(
|
||||
getThumbnailUrlForRemoteId(assetId, type: type),
|
||||
cacheKey: getThumbnailCacheKeyForRemoteId(assetId, type: type),
|
||||
headers: {
|
||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||
},
|
||||
);
|
||||
|
||||
/// Precaches this asset for instant load the next time it is shown
|
||||
static Future<void> precacheAsset(
|
||||
Asset asset,
|
||||
BuildContext context, {
|
||||
type = api.ThumbnailFormat.WEBP,
|
||||
size = 250,
|
||||
}) {
|
||||
if (useLocal(asset)) {
|
||||
// Precache the local image
|
||||
return precacheImage(
|
||||
localImageProvider(asset, size: size),
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
final accessToken = Store.get(StoreKey.accessToken);
|
||||
// Precache the remote image since we are not using local images
|
||||
return precacheImage(
|
||||
remoteThumbnailProvider(asset, type, {"x-immich-user-token": accessToken}),
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool useLocal(Asset asset) =>
|
||||
!asset.isRemote ||
|
||||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue