refactor(mobile): consolidate image requests (#25743)

remote url image provider

remove cached_network_image

formatting

linting

remove thumb provider

formatting
This commit is contained in:
Mert
2026-02-05 12:16:42 -05:00
committed by GitHub
parent 237ea3aedd
commit ba2dfa7df6
32 changed files with 95 additions and 501 deletions
@@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/activity_service.provider.dart';
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@@ -102,7 +102,7 @@ class _ActivityAssetThumbnail extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
image: DecorationImage(
image: ImmichRemoteThumbnailProvider(assetId: assetId),
image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: ""),
fit: BoxFit.cover,
),
),
@@ -4,9 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/activity.provider.dart';
import 'package:immich_mobile/providers/activity_service.provider.dart';
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
@@ -56,7 +56,7 @@ class CommentBubble extends ConsumerWidget {
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10)),
child: Image(
image: ImmichRemoteThumbnailProvider(assetId: activity.assetId!),
image: RemoteImageProvider.thumbnail(assetId: activity.assetId!, thumbhash: ""),
fit: BoxFit.cover,
),
),
@@ -1,12 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@@ -32,15 +32,12 @@ class AlbumThumbnailListTile extends StatelessWidget {
}
buildAlbumThumbnail() {
return CachedNetworkImage(
return SizedBox(
width: cardSize,
height: cardSize,
fit: BoxFit.cover,
fadeInDuration: const Duration(milliseconds: 200),
imageUrl: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail),
httpHeaders: ApiService.getRequestHeaders(),
cacheKey: getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail),
errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined),
child: Thumbnail(
imageProvider: RemoteImageProvider(url: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail)),
),
);
}
+14 -5
View File
@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset;
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/image/immich_local_image_provider.dart';
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
import 'package:octo_image/octo_image.dart';
@@ -34,13 +35,21 @@ class ImmichImage extends StatelessWidget {
}
if (asset == null) {
return ImmichRemoteImageProvider(assetId: assetId!);
return RemoteFullImageProvider(assetId: assetId!, thumbhash: '', assetType: base_asset.AssetType.video);
}
if (useLocal(asset)) {
return ImmichLocalImageProvider(asset: asset, width: width, height: height);
return LocalFullImageProvider(
id: asset.localId!,
assetType: base_asset.AssetType.video,
size: Size(width, height),
);
} else {
return ImmichRemoteImageProvider(assetId: asset.remoteId!);
return RemoteFullImageProvider(
assetId: asset.remoteId!,
thumbhash: asset.thumbhash ?? '',
assetType: base_asset.AssetType.video,
);
}
}
@@ -2,15 +2,15 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
import 'package:immich_mobile/utils/thumbnail_utils.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart';
import 'package:octo_image/octo_image.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset;
class ImmichThumbnail extends HookConsumerWidget {
const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key});
@@ -24,26 +24,29 @@ class ImmichThumbnail extends HookConsumerWidget {
/// 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
static ImageProvider imageProvider({Asset? asset, String? assetId, String? userId, int thumbnailSize = 256}) {
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 ImmichRemoteThumbnailProvider(assetId: assetId!);
return RemoteImageProvider.thumbnail(assetId: assetId!, thumbhash: "");
}
if (ImmichImage.useLocal(asset)) {
return ImmichLocalThumbnailProvider(asset: asset, height: thumbnailSize, width: thumbnailSize, userId: userId);
return LocalThumbProvider(
id: asset.localId!,
assetType: base_asset.AssetType.video,
size: Size(thumbnailSize.toDouble(), thumbnailSize.toDouble()),
);
} else {
return ImmichRemoteThumbnailProvider(assetId: asset.remoteId!, height: thumbnailSize, width: thumbnailSize);
return RemoteImageProvider.thumbnail(assetId: asset.remoteId!, thumbhash: asset.thumbhash ?? "");
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
Uint8List? blurhash = useBlurHashRef(asset).value;
final userId = ref.watch(currentUserProvider)?.id;
if (asset == null) {
return Container(
@@ -56,7 +59,7 @@ class ImmichThumbnail extends HookConsumerWidget {
final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []);
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset, userId: userId);
final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset);
customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) {
thumbnailProviderInstance.evict();
@@ -14,8 +14,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/people.utils.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
@@ -230,10 +230,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
elevation: 3,
child: CircleAvatar(
maxRadius: 84 / 2,
backgroundImage: NetworkImage(
getFaceThumbnailUrl(widget.person.id),
headers: ApiService.getRequestHeaders(),
),
backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(widget.person.id)),
),
),
),
+2 -7
View File
@@ -1,10 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
final url = "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image";
@@ -12,11 +11,7 @@ Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
return CircleAvatar(
radius: radius,
backgroundColor: context.primaryColor.withAlpha(50),
foregroundImage: CachedNetworkImageProvider(
url,
headers: ApiService.getRequestHeaders(),
cacheKey: "user-${u.id}-profile",
),
foregroundImage: RemoteImageProvider(url: url),
// silence errors if user has no profile image, use initials as fallback
onForegroundImageError: (exception, stackTrace) {},
child: Text(nameFirstLetter.toUpperCase()),
@@ -1,13 +1,11 @@
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/widgets/common/transparent_image.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
// ignore: must_be_immutable
class UserCircleAvatar extends ConsumerWidget {
@@ -46,16 +44,12 @@ class UserCircleAvatar extends ConsumerWidget {
child: user.hasProfileImage
? ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(50)),
child: CachedNetworkImage(
child: Image(
fit: BoxFit.cover,
cacheKey: '${user.id}-${user.profileChangedAt.toIso8601String()}',
width: size,
height: size,
placeholder: (_, __) => Image.memory(kTransparentImage),
imageUrl: profileImageUrl,
httpHeaders: ApiService.getRequestHeaders(),
fadeInDuration: const Duration(milliseconds: 300),
errorWidget: (context, error, stackTrace) => textIcon,
image: RemoteImageProvider(url: profileImageUrl),
errorBuilder: (context, error, stackTrace) => textIcon,
),
)
: textIcon,
@@ -1,10 +1,9 @@
import 'dart:io';
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class PositionedAssetMarkerIcon extends StatelessWidget {
@@ -53,7 +52,6 @@ class _AssetMarkerIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final imageUrl = getThumbnailUrlForRemoteId(id);
final cacheKey = getThumbnailCacheKeyForRemoteId(id, thumbhash);
return LayoutBuilder(
builder: (context, constraints) {
return Stack(
@@ -79,12 +77,7 @@ class _AssetMarkerIcon extends StatelessWidget {
backgroundColor: context.colorScheme.onSurface,
child: CircleAvatar(
radius: constraints.maxHeight * 0.37,
backgroundImage: CachedNetworkImageProvider(
imageUrl,
cacheKey: cacheKey,
headers: ApiService.getRequestHeaders(),
errorListener: (_) => const Icon(Icons.image_not_supported_outlined),
),
backgroundImage: RemoteImageProvider(url: imageUrl),
),
),
),
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class CuratedPeopleRow extends StatelessWidget {
@@ -29,7 +29,6 @@ class CuratedPeopleRow extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(content.length, (index) {
final person = content[index];
final headers = ApiService.getRequestHeaders();
return Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Column(
@@ -44,7 +43,7 @@ class CuratedPeopleRow extends StatelessWidget {
elevation: 3,
child: CircleAvatar(
maxRadius: imageSize / 2,
backgroundImage: NetworkImage(getFaceThumbnailUrl(person.id), headers: headers),
backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)),
),
),
),
@@ -6,8 +6,8 @@ import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
@@ -23,7 +23,6 @@ class PeoplePicker extends HookConsumerWidget {
final imageSize = 60.0;
final searchQuery = useState('');
final people = ref.watch(getAllPeopleProvider);
final headers = ApiService.getRequestHeaders();
final selectedPeople = useState<Set<PersonDto>>(filter ?? {});
return Column(
@@ -75,7 +74,7 @@ class PeoplePicker extends HookConsumerWidget {
elevation: 3,
child: CircleAvatar(
maxRadius: imageSize / 2,
backgroundImage: NetworkImage(getFaceThumbnailUrl(person.id), headers: headers),
backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)),
),
),
),
@@ -1,8 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart';
import 'package:immich_mobile/services/api.service.dart';
class ThumbnailWithInfo extends StatelessWidget {
const ThumbnailWithInfo({
@@ -30,14 +30,7 @@ class ThumbnailWithInfo extends StatelessWidget {
child: imageUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: CachedNetworkImage(
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
imageUrl: imageUrl!,
httpHeaders: ApiService.getRequestHeaders(),
errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined),
),
child: Thumbnail(imageProvider: RemoteImageProvider(url: imageUrl!)),
)
: Center(child: Icon(noImageIcon ?? Icons.not_listed_location, color: textAndIconColor)),
);