From 64e23a3b5c75e374b62f95914c27d7cf6a2a04ac Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Fri, 8 Nov 2024 21:19:12 -0500 Subject: [PATCH] higher res placeholder for local videos --- .../lib/pages/common/gallery_viewer.page.dart | 17 ++-- mobile/lib/pages/photos/memory.page.dart | 4 + .../image/immich_local_image_provider.dart | 80 ++++++++++++------- mobile/lib/widgets/common/immich_image.dart | 9 ++- mobile/lib/widgets/memories/memory_card.dart | 2 + 5 files changed, 75 insertions(+), 37 deletions(-) diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 11f0d3679b..6cc36867f4 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -100,7 +100,11 @@ class GalleryViewerPage extends HookConsumerWidget { if (index < totalAssets.value && index >= 0) { final asset = loadAsset(index); await precacheImage( - ImmichImage.imageProvider(asset: asset), + ImmichImage.imageProvider( + asset: asset, + width: context.width, + height: context.height, + ), context, onError: onError, ); @@ -313,7 +317,11 @@ class GalleryViewerPage extends HookConsumerWidget { asset: asset, placeholder: Image( key: ValueKey(asset), - image: ImmichImage.imageProvider(asset: asset), + image: ImmichImage.imageProvider( + asset: asset, + width: context.width, + height: context.height, + ), fit: BoxFit.contain, height: context.height, width: context.width, @@ -396,12 +404,7 @@ class GalleryViewerPage extends HookConsumerWidget { stackIndex.value = -1; isPlayingMotionVideo.value = false; - // Delay setting the new asset to avoid a stutter in the page change animation - // TODO: make the scroll animation finish more quickly, and ideally have a callback for when it's done ref.read(currentAssetProvider.notifier).set(newAsset); - // Timer(const Duration(milliseconds: 450), () { - // ref.read(currentAssetProvider.notifier).set(newAsset); - // }); // Wait for page change animation to finish, then precache the next image Timer(const Duration(milliseconds: 400), () { diff --git a/mobile/lib/pages/photos/memory.page.dart b/mobile/lib/pages/photos/memory.page.dart index 3f86f5be08..74a94ed6ee 100644 --- a/mobile/lib/pages/photos/memory.page.dart +++ b/mobile/lib/pages/photos/memory.page.dart @@ -113,11 +113,15 @@ class MemoryPage extends HookConsumerWidget { } // Precache the asset + final size = MediaQuery.sizeOf(context); await precacheImage( ImmichImage.imageProvider( asset: asset, + width: size.width, + height: size.height, ), context, + size: size, ); } diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index bbfaf12a4f..36fd3334b9 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -7,14 +7,21 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart' show ThumbnailSize; /// The local image provider for an asset class ImmichLocalImageProvider extends ImageProvider { final Asset asset; + // only used for videos + final double width; + final double height; + final Logger log = Logger('ImmichLocalImageProvider'); ImmichLocalImageProvider({ required this.asset, + required this.width, + required this.height, }) : assert(asset.local != null, 'Only usable when asset.local is set'); /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key @@ -42,38 +49,57 @@ class ImmichLocalImageProvider extends ImageProvider { // Streams in each stage of the image as we ask for it Stream _codec( - Asset key, + Asset asset, ImageDecoderCallback decode, StreamController chunkEvents, ) async* { - // Load a small thumbnail - final thumbBytes = await asset.local?.thumbnailDataWithSize( - const ThumbnailSize.square(256), - quality: 80, - ); - 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"); - } - - if (asset.isImage) { - final File? file = await asset.local?.originFile; - if (file == null) { - throw StateError("Opening file for asset ${asset.fileName} failed"); + ui.ImmutableBuffer? buffer; + try { + final local = asset.local; + if (local == null) { + throw StateError('Asset ${asset.fileName} has no local data'); } - try { - final buffer = await ui.ImmutableBuffer.fromFilePath(file.path); - final codec = await decode(buffer); - yield codec; - } catch (error) { - throw StateError("Loading asset ${asset.fileName} failed"); - } - } - chunkEvents.close(); + var thumbBytes = await local + .thumbnailDataWithSize(const ThumbnailSize.square(256), quality: 80); + if (thumbBytes == null) { + throw StateError("Loading thumbnail for ${asset.fileName} failed"); + } + buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + thumbBytes = null; + yield await decode(buffer); + buffer = null; + + switch (asset.type) { + case AssetType.image: + final File? file = await local.originFile; + if (file == null) { + throw StateError("Opening file for asset ${asset.fileName} failed"); + } + buffer = await ui.ImmutableBuffer.fromFilePath(file.path); + yield await decode(buffer); + buffer = null; + break; + case AssetType.video: + final size = ThumbnailSize(width.ceil(), height.ceil()); + thumbBytes = await local.thumbnailDataWithSize(size); + if (thumbBytes == null) { + throw StateError("Failed to load preview for ${asset.fileName}"); + } + buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + thumbBytes = null; + yield await decode(buffer); + buffer = null; + break; + default: + throw StateError('Unsupported asset type ${asset.type}'); + } + } catch (error, stack) { + log.severe('Error loading local image ${asset.fileName}', error, stack); + buffer?.dispose(); + } finally { + chunkEvents.close(); + } } @override diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart index 5946dee453..ab0f2584b5 100644 --- a/mobile/lib/widgets/common/immich_image.dart +++ b/mobile/lib/widgets/common/immich_image.dart @@ -28,12 +28,11 @@ class ImmichImage extends StatelessWidget { // 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, + double width = 1080, + double height = 1920, }) { if (asset == null && assetId == null) { throw Exception('Must supply either asset or assetId'); @@ -48,6 +47,8 @@ class ImmichImage extends StatelessWidget { if (useLocal(asset)) { return ImmichLocalImageProvider( asset: asset, + width: width, + height: height, ); } else { return ImmichRemoteImageProvider( @@ -87,6 +88,8 @@ class ImmichImage extends StatelessWidget { }, image: ImmichImage.imageProvider( asset: asset, + width: context.width, + height: context.height, ), width: width, height: height, diff --git a/mobile/lib/widgets/memories/memory_card.dart b/mobile/lib/widgets/memories/memory_card.dart index de331670ab..ba74b0496d 100644 --- a/mobile/lib/widgets/memories/memory_card.dart +++ b/mobile/lib/widgets/memories/memory_card.dart @@ -136,6 +136,8 @@ class _BlurredBackdrop extends HookWidget { image: DecorationImage( image: ImmichImage.imageProvider( asset: asset, + height: context.height, + width: context.width, ), fit: BoxFit.cover, ),