mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
refactor(mobile): Use ImmichThumbnail and local thumbnail image provider (#7279)
* Refactor to use ImmichThumbnail and local thumbnail image provider format * dart format linter errors linter * Adds blurhash format * Fixes image blur * uses hook instead of stateful widget to be more consistent * Uses blurhash hook state * Uses blurhash ref instead of state * Fixes fade in duration for fade in placeholder * Fixes an issue where thumbnails fail to load if too many thumbnail requests are made simultaenously --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
5e485e35e9
commit
d76baee50d
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
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_thumbnail.dart';
|
||||||
|
|
||||||
class AlbumThumbnailCard extends StatelessWidget {
|
class AlbumThumbnailCard extends StatelessWidget {
|
||||||
final Function()? onTap;
|
final Function()? onTap;
|
||||||
@ -45,8 +45,8 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildAlbumThumbnail() => ImmichImage.thumbnail(
|
buildAlbumThumbnail() => ImmichThumbnail(
|
||||||
album.thumbnail.value,
|
asset: 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,
|
||||||
),
|
),
|
||||||
|
@ -12,7 +12,7 @@ import 'package:immich_mobile/modules/partner/ui/partner_list.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
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_thumbnail.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SharingPage extends HookConsumerWidget {
|
class SharingPage extends HookConsumerWidget {
|
||||||
@ -72,8 +72,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;
|
||||||
|
}
|
@ -13,10 +13,13 @@ 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';
|
||||||
|
|
||||||
/// Our Image Provider HTTP client to make the request
|
/// Our Image Provider HTTP client to make the request
|
||||||
final _httpClient = HttpClient()..autoUncompress = false;
|
final _httpClient = HttpClient()
|
||||||
|
..autoUncompress = false
|
||||||
|
..maxConnectionsPerHost = 10;
|
||||||
|
|
||||||
/// 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 +35,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 +68,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 +87,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 +103,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 +144,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
|
||||||
|
@ -12,14 +12,17 @@ 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/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
|
/// Our HTTP client to make the request
|
||||||
|
final _httpClient = HttpClient()
|
||||||
|
..autoUncompress = false
|
||||||
|
..maxConnectionsPerHost = 100;
|
||||||
|
|
||||||
/// 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;
|
||||||
|
|
||||||
/// Our HTTP client to make the request
|
|
||||||
final _httpClient = HttpClient()..autoUncompress = false;
|
|
||||||
|
|
||||||
ImmichRemoteThumbnailProvider({
|
ImmichRemoteThumbnailProvider({
|
||||||
required this.assetId,
|
required this.assetId,
|
||||||
});
|
});
|
||||||
@ -27,12 +30,17 @@ 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(
|
||||||
return SynchronousFuture(assetId);
|
ImageConfiguration configuration,
|
||||||
|
) {
|
||||||
|
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 +51,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';
|
||||||
@ -26,13 +27,13 @@ import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.da
|
|||||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
|
||||||
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/store.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
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 +482,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),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -740,9 +735,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) => ImageFiltered(
|
||||||
asset(),
|
imageFilter: ui.ImageFilter.blur(
|
||||||
fit: BoxFit.contain,
|
sigmaX: 1,
|
||||||
|
sigmaY: 1,
|
||||||
|
),
|
||||||
|
child: ImmichThumbnail(
|
||||||
|
asset: asset(),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pageController: controller,
|
pageController: controller,
|
||||||
scrollPhysics: isZoomed.value
|
scrollPhysics: isZoomed.value
|
||||||
|
@ -4,7 +4,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
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_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 +134,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,
|
||||||
),
|
),
|
||||||
|
@ -38,7 +38,8 @@ class Asset {
|
|||||||
// stack handling to properly handle it
|
// stack handling to properly handle it
|
||||||
stackParentId =
|
stackParentId =
|
||||||
remote.stackParentId == remote.id ? null : remote.stackParentId,
|
remote.stackParentId == remote.id ? null : remote.stackParentId,
|
||||||
stackCount = remote.stackCount;
|
stackCount = remote.stackCount,
|
||||||
|
thumbhash = remote.thumbhash;
|
||||||
|
|
||||||
Asset.local(AssetEntity local, List<int> hash)
|
Asset.local(AssetEntity local, List<int> hash)
|
||||||
: localId = local.id,
|
: localId = local.id,
|
||||||
@ -91,6 +92,7 @@ class Asset {
|
|||||||
this.stackCount = 0,
|
this.stackCount = 0,
|
||||||
this.isReadOnly = false,
|
this.isReadOnly = false,
|
||||||
this.isOffline = false,
|
this.isOffline = false,
|
||||||
|
this.thumbhash,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
@ -119,6 +121,8 @@ class Asset {
|
|||||||
/// because Isar cannot sort lists of byte arrays
|
/// because Isar cannot sort lists of byte arrays
|
||||||
String checksum;
|
String checksum;
|
||||||
|
|
||||||
|
String? thumbhash;
|
||||||
|
|
||||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||||
String? remoteId;
|
String? remoteId;
|
||||||
|
|
||||||
@ -279,6 +283,7 @@ class Asset {
|
|||||||
a.exifInfo?.latitude != exifInfo?.latitude ||
|
a.exifInfo?.latitude != exifInfo?.latitude ||
|
||||||
a.exifInfo?.longitude != exifInfo?.longitude ||
|
a.exifInfo?.longitude != exifInfo?.longitude ||
|
||||||
// no local stack count or different count from remote
|
// no local stack count or different count from remote
|
||||||
|
a.thumbhash != thumbhash ||
|
||||||
((stackCount == null && a.stackCount != null) ||
|
((stackCount == null && a.stackCount != null) ||
|
||||||
(stackCount != null &&
|
(stackCount != null &&
|
||||||
a.stackCount != null &&
|
a.stackCount != null &&
|
||||||
@ -343,6 +348,7 @@ class Asset {
|
|||||||
isReadOnly: a.isReadOnly,
|
isReadOnly: a.isReadOnly,
|
||||||
isOffline: a.isOffline,
|
isOffline: a.isOffline,
|
||||||
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
|
||||||
|
thumbhash: a.thumbhash,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// add only missing values (and set isLocal to true)
|
// add only missing values (and set isLocal to true)
|
||||||
@ -379,6 +385,7 @@ class Asset {
|
|||||||
ExifInfo? exifInfo,
|
ExifInfo? exifInfo,
|
||||||
String? stackParentId,
|
String? stackParentId,
|
||||||
int? stackCount,
|
int? stackCount,
|
||||||
|
String? thumbhash,
|
||||||
}) =>
|
}) =>
|
||||||
Asset(
|
Asset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@ -403,6 +410,7 @@ class Asset {
|
|||||||
exifInfo: exifInfo ?? this.exifInfo,
|
exifInfo: exifInfo ?? this.exifInfo,
|
||||||
stackParentId: stackParentId ?? this.stackParentId,
|
stackParentId: stackParentId ?? this.stackParentId,
|
||||||
stackCount: stackCount ?? this.stackCount,
|
stackCount: stackCount ?? this.stackCount,
|
||||||
|
thumbhash: thumbhash ?? this.thumbhash,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> put(Isar db) async {
|
Future<void> put(Isar db) async {
|
||||||
|
BIN
mobile/lib/shared/models/asset.g.dart
generated
BIN
mobile/lib/shared/models/asset.g.dart
generated
Binary file not shown.
35
mobile/lib/shared/ui/fade_in_placeholder_image.dart
Normal file
35
mobile/lib/shared/ui/fade_in_placeholder_image.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
||||||
|
|
||||||
|
class FadeInPlaceholderImage extends StatelessWidget {
|
||||||
|
final Widget placeholder;
|
||||||
|
final ImageProvider image;
|
||||||
|
final Duration duration;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
|
const FadeInPlaceholderImage({
|
||||||
|
super.key,
|
||||||
|
required this.placeholder,
|
||||||
|
required this.image,
|
||||||
|
this.duration = const Duration(milliseconds: 100),
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox.expand(
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
placeholder,
|
||||||
|
FadeInImage(
|
||||||
|
fadeInDuration: duration,
|
||||||
|
image: image,
|
||||||
|
fit: fit,
|
||||||
|
placeholder: MemoryImage(kTransparentImage),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
17
mobile/lib/shared/ui/hooks/blurhash_hook.dart
Normal file
17
mobile/lib/shared/ui/hooks/blurhash_hook.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:thumbhash/thumbhash.dart' as thumbhash;
|
||||||
|
|
||||||
|
ObjectRef<Uint8List?> useBlurHashRef(Asset? asset) {
|
||||||
|
if (asset?.thumbhash == null) {
|
||||||
|
return useRef(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final rbga = thumbhash.thumbHashToRGBA(
|
||||||
|
base64Decode(asset!.thumbhash!),
|
||||||
|
);
|
||||||
|
|
||||||
|
return useRef(thumbhash.rgbaToBmp(rbga));
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
@ -9,8 +7,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.d
|
|||||||
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:octo_image/octo_image.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 ImmichImage extends StatelessWidget {
|
class ImmichImage extends StatelessWidget {
|
||||||
const ImmichImage(
|
const ImmichImage(
|
||||||
@ -19,8 +15,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 +23,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,8 +34,6 @@ 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');
|
||||||
@ -76,24 +42,18 @@ class ImmichImage extends StatelessWidget {
|
|||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
return ImmichRemoteImageProvider(
|
return ImmichRemoteImageProvider(
|
||||||
assetId: assetId!,
|
assetId: assetId!,
|
||||||
isThumbnail: isThumbnail,
|
isThumbnail: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useLocal(asset) && isThumbnail) {
|
if (useLocal(asset)) {
|
||||||
return AssetEntityImageProvider(
|
|
||||||
asset.local!,
|
|
||||||
isOriginal: false,
|
|
||||||
thumbnailSize: ThumbnailSize.square(thumbnailSize),
|
|
||||||
);
|
|
||||||
} else if (useLocal(asset) && !isThumbnail) {
|
|
||||||
return ImmichLocalImageProvider(
|
return ImmichLocalImageProvider(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ImmichRemoteImageProvider(
|
return ImmichRemoteImageProvider(
|
||||||
assetId: asset.remoteId!,
|
assetId: asset.remoteId!,
|
||||||
isThumbnail: isThumbnail,
|
isThumbnail: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,15 +65,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 +87,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,
|
||||||
|
89
mobile/lib/shared/ui/immich_thumbnail.dart
Normal file
89
mobile/lib/shared/ui/immich_thumbnail.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.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/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
|
||||||
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
|
class ImmichThumbnail extends HookWidget {
|
||||||
|
const ImmichThumbnail({
|
||||||
|
this.asset,
|
||||||
|
this.width = 250,
|
||||||
|
this.height = 250,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Asset? asset;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
|
/// Helper function to return the image provider for the asset thumbnail
|
||||||
|
/// 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,
|
||||||
|
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) {
|
||||||
|
Uint8List? blurhash = useBlurHashRef(asset).value;
|
||||||
|
if (asset == null) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.no_photography),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OctoImage.fromSet(
|
||||||
|
placeholderFadeInDuration: Duration.zero,
|
||||||
|
fadeInDuration: Duration.zero,
|
||||||
|
fadeOutDuration: const Duration(milliseconds: 100),
|
||||||
|
octoSet: blurHashOrPlaceholder(blurhash),
|
||||||
|
image: ImmichThumbnail.imageProvider(
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
mobile/lib/shared/ui/thumbhash_placeholder.dart
Normal file
48
mobile/lib/shared/ui/thumbhash_placeholder.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/fade_in_placeholder_image.dart';
|
||||||
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
|
/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as
|
||||||
|
/// placeholder and [OctoError.icon] as error.
|
||||||
|
OctoSet blurHashOrPlaceholder(
|
||||||
|
Uint8List? blurhash, {
|
||||||
|
BoxFit? fit,
|
||||||
|
Text? errorMessage,
|
||||||
|
}) {
|
||||||
|
return OctoSet(
|
||||||
|
placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
|
||||||
|
errorBuilder: blurHashErrorBuilder(blurhash, fit: fit),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OctoPlaceholderBuilder blurHashPlaceholderBuilder(
|
||||||
|
Uint8List? blurhash, {
|
||||||
|
BoxFit? fit,
|
||||||
|
}) {
|
||||||
|
return (context) => blurhash == null
|
||||||
|
? const ThumbnailPlaceholder()
|
||||||
|
: FadeInPlaceholderImage(
|
||||||
|
placeholder: const ThumbnailPlaceholder(),
|
||||||
|
image: MemoryImage(blurhash),
|
||||||
|
fit: fit ?? BoxFit.cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OctoErrorBuilder blurHashErrorBuilder(
|
||||||
|
Uint8List? blurhash, {
|
||||||
|
BoxFit? fit,
|
||||||
|
Text? message,
|
||||||
|
IconData? icon,
|
||||||
|
Color? iconColor,
|
||||||
|
double? iconSize,
|
||||||
|
}) {
|
||||||
|
return OctoError.placeholderWithErrorIcon(
|
||||||
|
blurHashPlaceholderBuilder(blurhash, fit: fit),
|
||||||
|
message: message,
|
||||||
|
icon: icon,
|
||||||
|
iconColor: iconColor,
|
||||||
|
iconSize: iconSize,
|
||||||
|
);
|
||||||
|
}
|
@ -1491,6 +1491,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.6.1"
|
||||||
|
thumbhash:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: thumbhash
|
||||||
|
sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0+1"
|
||||||
time:
|
time:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -57,6 +57,7 @@ dependencies:
|
|||||||
flutter_local_notifications: ^16.3.2
|
flutter_local_notifications: ^16.3.2
|
||||||
timezone: ^0.9.2
|
timezone: ^0.9.2
|
||||||
octo_image: ^2.0.0
|
octo_image: ^2.0.0
|
||||||
|
thumbhash: 0.1.0+1
|
||||||
|
|
||||||
openapi:
|
openapi:
|
||||||
path: openapi
|
path: openapi
|
||||||
|
Loading…
Reference in New Issue
Block a user