mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Refactor to use ImmichThumbnail and local thumbnail image provider
format
This commit is contained in:
parent
73825918c0
commit
718c258a07
@ -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,
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
118
mobile/lib/shared/ui/immich_thumbnail.dart
Normal file
118
mobile/lib/shared/ui/immich_thumbnail.dart
Normal 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user