diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index 8c6776d715..b78bd2d224 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -5,7 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/memories/models/memory.dart'; import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:intl/intl.dart'; +import 'package:openapi/api.dart' as api; class MemoryPage extends HookConsumerWidget { final List memories; @@ -37,11 +40,16 @@ class MemoryPage extends HookConsumerWidget { } toNextAsset(int currentAssetIndex) { - (currentAssetIndex + 1 < currentMemory.value.assets.length) - ? memoryAssetPageController.jumpToPage( - (currentAssetIndex + 1), - ) - : toNextMemory(); + if (currentAssetIndex + 1 < currentMemory.value.assets.length) { + // Go to the next asset + memoryAssetPageController.nextPage( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 500), + ); + } else { + // Go to the next memory since we are at the end of our assets + toNextMemory(); + } } updateProgressText() { @@ -49,9 +57,67 @@ class MemoryPage extends HookConsumerWidget { "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; } + /// Downloads and caches the image for the asset at this [currentMemory]'s index + precacheAsset(int index) async { + // Guard index out of range + if (index < 0) { + return; + } + + late Asset asset; + if (index < currentMemory.value.assets.length) { + // Uses the next asset in this current memory + asset = currentMemory.value.assets[index]; + } else { + // Precache the first asset in the next memory if available + final currentMemoryIndex = memories.indexOf(currentMemory.value); + + // Guard no memory found + if (currentMemoryIndex == -1) { + return; + } + + final nextMemoryIndex = currentMemoryIndex + 1; + // Guard no next memory + if (nextMemoryIndex >= memories.length) { + return; + } + + // Get the first asset from the next memory + asset = memories[nextMemoryIndex].assets.first; + } + + // Gets the thumbnail url and precaches it + final precaches = >[]; + + precaches.add( + ImmichImage.precacheAsset( + asset, + context, + type: api.ThumbnailFormat.WEBP, + ), + ); + precaches.add( + ImmichImage.precacheAsset( + asset, + context, + type: api.ThumbnailFormat.JPEG, + ), + ); + + await Future.wait(precaches); + } + + // Precache the next page right away if we are on the first page + if (currentAssetPage.value == 0) { + Future.delayed(const Duration(milliseconds: 200)) + .then((_) => precacheAsset(1)); + } + onAssetChanged(int otherIndex) { HapticFeedback.selectionClick(); currentAssetPage.value = otherIndex; + precacheAsset(otherIndex + 1); updateProgressText(); } diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart index 606092673c..8653bdd716 100644 --- a/mobile/lib/shared/ui/immich_image.dart +++ b/mobile/lib/shared/ui/immich_image.dart @@ -147,4 +147,46 @@ class ImmichImage extends StatelessWidget { }, ); } + + /// Precaches this asset for instant load the next time it is shown + static Future precacheAsset( + Asset asset, + BuildContext context, { + type = api.ThumbnailFormat.WEBP, + }) { + final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}'; + + if (type == api.ThumbnailFormat.WEBP) { + final thumbnailUrl = getThumbnailUrl(asset); + final thumbnailCacheKey = getThumbnailCacheKey(asset); + final thumbnailProvider = CachedNetworkImageProvider( + thumbnailUrl, + cacheKey: thumbnailCacheKey, + headers: {"Authorization": authToken}, + ); + return precacheImage(thumbnailProvider, context); + } + // Precache the local image + if (!asset.isRemote && + (asset.isLocal || !Store.get(StoreKey.preferRemoteImage, false))) { + final provider = AssetEntityImageProvider( + asset.local!, + isOriginal: false, + thumbnailSize: const ThumbnailSize.square(250), // like server thumbs + ); + return precacheImage(provider, context); + } else { + // Precache the remote image since we are not using local images + final url = getThumbnailUrl(asset, type: api.ThumbnailFormat.JPEG); + final cacheKey = + getThumbnailCacheKey(asset, type: api.ThumbnailFormat.JPEG); + final provider = CachedNetworkImageProvider( + url, + cacheKey: cacheKey, + headers: {"Authorization": authToken}, + ); + + return precacheImage(provider, context); + } + } }