mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:49:11 -04:00 
			
		
		
		
	Better caching for mobile (#521)
* Use custom caches in all modules * Cache Settings * Fix wrong key * Create custom cache repository based on hive * Show cache usage in settings * Show cache sizes * Change settings ranges and default value * Handle cache clear by operating system * Resolve review comments
This commit is contained in:
		
							parent
							
								
									e527685ebf
								
							
						
					
					
						commit
						25e68cf826
					
				| @ -149,5 +149,18 @@ | |||||||
|   "setting_notifications_notify_immediately": "immediately", |   "setting_notifications_notify_immediately": "immediately", | ||||||
|   "setting_notifications_notify_minutes": "{} minutes", |   "setting_notifications_notify_minutes": "{} minutes", | ||||||
|   "setting_notifications_notify_hours": "{} hours", |   "setting_notifications_notify_hours": "{} hours", | ||||||
|   "setting_notifications_notify_never": "never" |   "setting_notifications_notify_never": "never", | ||||||
|  |   "cache_settings_title": "Caching Settings", | ||||||
|  |   "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", | ||||||
|  |   "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", | ||||||
|  |   "cache_settings_image_cache_size": "Image cache size ({} assets)", | ||||||
|  |   "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", | ||||||
|  |   "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", | ||||||
|  |   "cache_settings_clear_cache_button": "Clear cache", | ||||||
|  |   "cache_settings_statistics_title": "Cache usage", | ||||||
|  |   "cache_settings_statistics_assets": "{} assets ({})", | ||||||
|  |   "cache_settings_statistics_thumbnail": "Thumbnails", | ||||||
|  |   "cache_settings_statistics_album": "Library thumbnails", | ||||||
|  |   "cache_settings_statistics_shared": "Shared album thumbnails", | ||||||
|  |   "cache_settings_statistics_full": "Full images" | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| 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:cached_network_image/cached_network_image.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| @ -8,6 +9,7 @@ import 'package:immich_mobile/constants/hive_box.dart'; | |||||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; | import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; | ||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/cache.service.dart'; | ||||||
| import 'package:immich_mobile/utils/image_url_builder.dart'; | import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| 
 | 
 | ||||||
| @ -15,17 +17,18 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | |||||||
|   final AssetResponseDto asset; |   final AssetResponseDto asset; | ||||||
|   final List<AssetResponseDto> assetList; |   final List<AssetResponseDto> assetList; | ||||||
|   final bool showStorageIndicator; |   final bool showStorageIndicator; | ||||||
|  |   final BaseCacheManager? cacheManager; | ||||||
| 
 | 
 | ||||||
|   const AlbumViewerThumbnail({ |   const AlbumViewerThumbnail({ | ||||||
|     Key? key, |     Key? key, | ||||||
|     required this.asset, |     required this.asset, | ||||||
|     required this.assetList, |     required this.assetList, | ||||||
|  |     this.cacheManager, | ||||||
|     this.showStorageIndicator = true, |     this.showStorageIndicator = true, | ||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final cacheKey = useState(1); |  | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = getThumbnailUrl(asset); |     var thumbnailRequestUrl = getThumbnailUrl(asset); | ||||||
|     var deviceId = ref.watch(authenticationProvider).deviceId; |     var deviceId = ref.watch(authenticationProvider).deviceId; | ||||||
| @ -123,7 +126,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget { | |||||||
|       return Container( |       return Container( | ||||||
|         decoration: BoxDecoration(border: drawBorderColor()), |         decoration: BoxDecoration(border: drawBorderColor()), | ||||||
|         child: CachedNetworkImage( |         child: CachedNetworkImage( | ||||||
|           cacheKey: "${asset.id}-${cacheKey.value}", |           cacheManager: cacheManager, | ||||||
|  |           cacheKey: asset.id, | ||||||
|           width: 300, |           width: 300, | ||||||
|           height: 300, |           height: 300, | ||||||
|           memCacheHeight: 200, |           memCacheHeight: 200, | ||||||
|  | |||||||
| @ -5,6 +5,8 @@ import 'package:hive_flutter/hive_flutter.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; | import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/cache.service.dart'; | ||||||
|  | import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| 
 | 
 | ||||||
| class SelectionThumbnailImage extends HookConsumerWidget { | class SelectionThumbnailImage extends HookConsumerWidget { | ||||||
| @ -15,15 +17,14 @@ class SelectionThumbnailImage extends HookConsumerWidget { | |||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final cacheKey = useState(1); |  | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = |     var thumbnailRequestUrl = getThumbnailUrl(asset); | ||||||
|         '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}'; |  | ||||||
|     var selectedAsset = |     var selectedAsset = | ||||||
|         ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; |         ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; | ||||||
|     var newAssetsForAlbum = |     var newAssetsForAlbum = | ||||||
|         ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum; |         ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum; | ||||||
|     var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; |     var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; | ||||||
|  |     final cacheService = ref.watch(cacheServiceProvider); | ||||||
| 
 | 
 | ||||||
|     Widget _buildSelectionIcon(AssetResponseDto asset) { |     Widget _buildSelectionIcon(AssetResponseDto asset) { | ||||||
|       var isSelected = selectedAsset.map((item) => item.id).contains(asset.id); |       var isSelected = selectedAsset.map((item) => item.id).contains(asset.id); | ||||||
| @ -113,7 +114,8 @@ class SelectionThumbnailImage extends HookConsumerWidget { | |||||||
|           Container( |           Container( | ||||||
|             decoration: BoxDecoration(border: drawBorderColor()), |             decoration: BoxDecoration(border: drawBorderColor()), | ||||||
|             child: CachedNetworkImage( |             child: CachedNetworkImage( | ||||||
|               cacheKey: "${asset.id}-${cacheKey.value}", |               cacheManager: cacheService.getCache(CacheType.thumbnail), | ||||||
|  |               cacheKey: asset.id, | ||||||
|               width: 150, |               width: 150, | ||||||
|               height: 150, |               height: 150, | ||||||
|               memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150, |               memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150, | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | |||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/cache.service.dart'; | ||||||
| import 'package:immich_mobile/utils/image_url_builder.dart'; | import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| 
 | 
 | ||||||
| @ -15,8 +16,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { | |||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final cacheKey = useState(1); |     final cacheService = ref.watch(cacheServiceProvider); | ||||||
| 
 |  | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
| 
 | 
 | ||||||
|     return GestureDetector( |     return GestureDetector( | ||||||
| @ -26,7 +26,8 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { | |||||||
|       child: Stack( |       child: Stack( | ||||||
|         children: [ |         children: [ | ||||||
|           CachedNetworkImage( |           CachedNetworkImage( | ||||||
|             cacheKey: "${asset.id}-${cacheKey.value}", |             cacheManager: cacheService.getCache(CacheType.thumbnail), | ||||||
|  |             cacheKey: asset.id, | ||||||
|             width: 500, |             width: 500, | ||||||
|             height: 500, |             height: 500, | ||||||
|             memCacheHeight: 500, |             memCacheHeight: 500, | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.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/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/cache.service.dart'; | ||||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||||
| import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart'; | import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart'; | ||||||
| import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; | ||||||
| @ -191,6 +192,7 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|       final appSettingService = ref.watch(appSettingsServiceProvider); |       final appSettingService = ref.watch(appSettingsServiceProvider); | ||||||
|       final bool showStorageIndicator = |       final bool showStorageIndicator = | ||||||
|           appSettingService.getSetting(AppSettingsEnum.storageIndicator); |           appSettingService.getSetting(AppSettingsEnum.storageIndicator); | ||||||
|  |       final cacheService = ref.watch(cacheServiceProvider); | ||||||
| 
 | 
 | ||||||
|       if (albumInfo.assets.isNotEmpty) { |       if (albumInfo.assets.isNotEmpty) { | ||||||
|         return SliverPadding( |         return SliverPadding( | ||||||
| @ -205,6 +207,7 @@ class AlbumViewerPage extends HookConsumerWidget { | |||||||
|             delegate: SliverChildBuilderDelegate( |             delegate: SliverChildBuilderDelegate( | ||||||
|               (BuildContext context, int index) { |               (BuildContext context, int index) { | ||||||
|                 return AlbumViewerThumbnail( |                 return AlbumViewerThumbnail( | ||||||
|  |                   cacheManager: cacheService.getCache(CacheType.thumbnail), | ||||||
|                   asset: albumInfo.assets[index], |                   asset: albumInfo.assets[index], | ||||||
|                   assetList: albumInfo.assets, |                   assetList: albumInfo.assets, | ||||||
|                   showStorageIndicator: showStorageIndicator, |                   showStorageIndicator: showStorageIndicator, | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import 'package:cached_network_image/cached_network_image.dart'; | import 'package:cached_network_image/cached_network_image.dart'; | ||||||
| import 'package:flutter/cupertino.dart'; | import 'package:flutter/cupertino.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; | ||||||
| import 'package:photo_view/photo_view.dart'; | import 'package:photo_view/photo_view.dart'; | ||||||
| 
 | 
 | ||||||
| enum _RemoteImageStatus { empty, thumbnail, preview, full } | enum _RemoteImageStatus { empty, thumbnail, preview, full } | ||||||
| @ -63,11 +64,13 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|     widget.onLoadingCompleted(); |     widget.onLoadingCompleted(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   CachedNetworkImageProvider _authorizedImageProvider(String url) { |   CachedNetworkImageProvider _authorizedImageProvider( | ||||||
|  |       String url, String cacheKey, BaseCacheManager? cacheManager) { | ||||||
|     return CachedNetworkImageProvider( |     return CachedNetworkImageProvider( | ||||||
|       url, |       url, | ||||||
|       headers: {"Authorization": widget.authToken}, |       headers: {"Authorization": widget.authToken}, | ||||||
|       cacheKey: url, |       cacheKey: cacheKey, | ||||||
|  |       cacheManager: cacheManager, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -101,8 +104,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   void _loadImages() { |   void _loadImages() { | ||||||
|     CachedNetworkImageProvider thumbnailProvider = |     CachedNetworkImageProvider thumbnailProvider = _authorizedImageProvider( | ||||||
|         _authorizedImageProvider(widget.thumbnailUrl); |       widget.thumbnailUrl, | ||||||
|  |       widget.cacheKey, | ||||||
|  |       widget.thumbnailCacheManager, | ||||||
|  |     ); | ||||||
|     _imageProvider = thumbnailProvider; |     _imageProvider = thumbnailProvider; | ||||||
| 
 | 
 | ||||||
|     thumbnailProvider.resolve(const ImageConfiguration()).addListener( |     thumbnailProvider.resolve(const ImageConfiguration()).addListener( | ||||||
| @ -115,8 +121,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     if (widget.previewUrl != null) { |     if (widget.previewUrl != null) { | ||||||
|       CachedNetworkImageProvider previewProvider = |       CachedNetworkImageProvider previewProvider = _authorizedImageProvider( | ||||||
|           _authorizedImageProvider(widget.previewUrl!); |         widget.previewUrl!, | ||||||
|  |         "${widget.cacheKey}_previewStage", | ||||||
|  |         widget.previewCacheManager, | ||||||
|  |       ); | ||||||
|       previewProvider.resolve(const ImageConfiguration()).addListener( |       previewProvider.resolve(const ImageConfiguration()).addListener( | ||||||
|         ImageStreamListener((ImageInfo imageInfo, _) { |         ImageStreamListener((ImageInfo imageInfo, _) { | ||||||
|           _performStateTransition(_RemoteImageStatus.preview, previewProvider); |           _performStateTransition(_RemoteImageStatus.preview, previewProvider); | ||||||
| @ -124,8 +133,11 @@ class _RemotePhotoViewState extends State<RemotePhotoView> { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     CachedNetworkImageProvider fullProvider = |     CachedNetworkImageProvider fullProvider = _authorizedImageProvider( | ||||||
|         _authorizedImageProvider(widget.imageUrl); |       widget.imageUrl, | ||||||
|  |       "${widget.cacheKey}_fullStage", | ||||||
|  |       widget.fullCacheManager, | ||||||
|  |     ); | ||||||
|     fullProvider.resolve(const ImageConfiguration()).addListener( |     fullProvider.resolve(const ImageConfiguration()).addListener( | ||||||
|       ImageStreamListener((ImageInfo imageInfo, _) { |       ImageStreamListener((ImageInfo imageInfo, _) { | ||||||
|         _performStateTransition(_RemoteImageStatus.full, fullProvider); |         _performStateTransition(_RemoteImageStatus.full, fullProvider); | ||||||
| @ -153,6 +165,10 @@ class RemotePhotoView extends StatefulWidget { | |||||||
|     this.previewUrl, |     this.previewUrl, | ||||||
|     required this.onLoadingCompleted, |     required this.onLoadingCompleted, | ||||||
|     required this.onLoadingStart, |     required this.onLoadingStart, | ||||||
|  |     this.thumbnailCacheManager, | ||||||
|  |     this.previewCacheManager, | ||||||
|  |     this.fullCacheManager, | ||||||
|  |     required this.cacheKey, | ||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
| 
 | 
 | ||||||
|   final String thumbnailUrl; |   final String thumbnailUrl; | ||||||
| @ -161,6 +177,10 @@ class RemotePhotoView extends StatefulWidget { | |||||||
|   final String? previewUrl; |   final String? previewUrl; | ||||||
|   final Function onLoadingCompleted; |   final Function onLoadingCompleted; | ||||||
|   final Function onLoadingStart; |   final Function onLoadingStart; | ||||||
|  |   final BaseCacheManager? thumbnailCacheManager; | ||||||
|  |   final BaseCacheManager? previewCacheManager; | ||||||
|  |   final BaseCacheManager? fullCacheManager; | ||||||
|  |   final String cacheKey; | ||||||
| 
 | 
 | ||||||
|   final void Function() onSwipeDown; |   final void Function() onSwipeDown; | ||||||
|   final void Function() onSwipeUp; |   final void Function() onSwipeUp; | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator | |||||||
| import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | ||||||
| import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; | import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; | ||||||
| import 'package:immich_mobile/modules/home/services/asset.service.dart'; | import 'package:immich_mobile/modules/home/services/asset.service.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/cache.service.dart'; | ||||||
| import 'package:immich_mobile/utils/image_url_builder.dart'; | import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| 
 | 
 | ||||||
| @ -40,6 +41,7 @@ class ImageViewerPage extends HookConsumerWidget { | |||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final downloadAssetStatus = |     final downloadAssetStatus = | ||||||
|         ref.watch(imageViewerStateProvider).downloadAssetStatus; |         ref.watch(imageViewerStateProvider).downloadAssetStatus; | ||||||
|  |     final cacheService = ref.watch(cacheServiceProvider); | ||||||
| 
 | 
 | ||||||
|     getAssetExif() async { |     getAssetExif() async { | ||||||
|       assetDetail = |       assetDetail = | ||||||
| @ -73,6 +75,7 @@ class ImageViewerPage extends HookConsumerWidget { | |||||||
|             tag: heroTag, |             tag: heroTag, | ||||||
|             child: RemotePhotoView( |             child: RemotePhotoView( | ||||||
|               thumbnailUrl: getThumbnailUrl(asset), |               thumbnailUrl: getThumbnailUrl(asset), | ||||||
|  |               cacheKey: asset.id, | ||||||
|               imageUrl: getImageUrl(asset), |               imageUrl: getImageUrl(asset), | ||||||
|               previewUrl: threeStageLoading |               previewUrl: threeStageLoading | ||||||
|                   ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) |                   ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG) | ||||||
| @ -84,6 +87,12 @@ class ImageViewerPage extends HookConsumerWidget { | |||||||
|               onSwipeUp: () => showInfo(), |               onSwipeUp: () => showInfo(), | ||||||
|               onLoadingCompleted: onLoadingCompleted, |               onLoadingCompleted: onLoadingCompleted, | ||||||
|               onLoadingStart: onLoadingStart, |               onLoadingStart: onLoadingStart, | ||||||
|  |               thumbnailCacheManager: | ||||||
|  |                   cacheService.getCache(CacheType.thumbnail), | ||||||
|  |               previewCacheManager: | ||||||
|  |                   cacheService.getCache(CacheType.imageViewerPreview), | ||||||
|  |               fullCacheManager: | ||||||
|  |                   cacheService.getCache(CacheType.imageViewerFull), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; | import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| @ -9,11 +10,13 @@ class ImageGrid extends ConsumerWidget { | |||||||
|   final List<AssetResponseDto> sortedAssetGroup; |   final List<AssetResponseDto> sortedAssetGroup; | ||||||
|   final int tilesPerRow; |   final int tilesPerRow; | ||||||
|   final bool showStorageIndicator; |   final bool showStorageIndicator; | ||||||
|  |   final BaseCacheManager? cacheManager; | ||||||
| 
 | 
 | ||||||
|   ImageGrid({ |   ImageGrid({ | ||||||
|     Key? key, |     Key? key, | ||||||
|     required this.assetGroup, |     required this.assetGroup, | ||||||
|     required this.sortedAssetGroup, |     required this.sortedAssetGroup, | ||||||
|  |     this.cacheManager, | ||||||
|     this.tilesPerRow = 4, |     this.tilesPerRow = 4, | ||||||
|     this.showStorageIndicator = true, |     this.showStorageIndicator = true, | ||||||
|   }) : super(key: key); |   }) : super(key: key); | ||||||
| @ -36,6 +39,7 @@ class ImageGrid extends ConsumerWidget { | |||||||
|             child: Stack( |             child: Stack( | ||||||
|               children: [ |               children: [ | ||||||
|                 ThumbnailImage( |                 ThumbnailImage( | ||||||
|  |                   cacheManager: cacheManager, | ||||||
|                   asset: assetGroup[index], |                   asset: assetGroup[index], | ||||||
|                   assetList: sortedAssetGroup, |                   assetList: sortedAssetGroup, | ||||||
|                   showStorageIndicator: showStorageIndicator, |                   showStorageIndicator: showStorageIndicator, | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; | |||||||
| import 'package:cached_network_image/cached_network_image.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_cache_manager/flutter_cache_manager.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| @ -16,18 +17,18 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|   final AssetResponseDto asset; |   final AssetResponseDto asset; | ||||||
|   final List<AssetResponseDto> assetList; |   final List<AssetResponseDto> assetList; | ||||||
|   final bool showStorageIndicator; |   final bool showStorageIndicator; | ||||||
|  |   final BaseCacheManager? cacheManager; | ||||||
| 
 | 
 | ||||||
|   const ThumbnailImage( |   const ThumbnailImage({ | ||||||
|       {Key? key, |     Key? key, | ||||||
|       required this.asset, |     required this.asset, | ||||||
|       required this.assetList, |     required this.assetList, | ||||||
|       this.showStorageIndicator = true}) |     this.cacheManager, | ||||||
|       : super(key: key); |     this.showStorageIndicator = true, | ||||||
|  |   }) : super(key: key); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final cacheKey = useState(1); |  | ||||||
| 
 |  | ||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     var thumbnailRequestUrl = getThumbnailUrl(asset); |     var thumbnailRequestUrl = getThumbnailUrl(asset); | ||||||
|     var selectedAsset = ref.watch(homePageStateProvider).selectedItems; |     var selectedAsset = ref.watch(homePageStateProvider).selectedItems; | ||||||
| @ -94,7 +95,8 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|                     : const Border(), |                     : const Border(), | ||||||
|               ), |               ), | ||||||
|               child: CachedNetworkImage( |               child: CachedNetworkImage( | ||||||
|                 cacheKey: "${asset.id}-${cacheKey.value}", |                 cacheKey: asset.id, | ||||||
|  |                 cacheManager: cacheManager, | ||||||
|                 width: 300, |                 width: 300, | ||||||
|                 height: 300, |                 height: 300, | ||||||
|                 memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400, |                 memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400, | ||||||
| @ -128,17 +130,18 @@ class ThumbnailImage extends HookConsumerWidget { | |||||||
|                   child: _buildSelectionIcon(asset), |                   child: _buildSelectionIcon(asset), | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             if (showStorageIndicator) Positioned( |             if (showStorageIndicator) | ||||||
|               right: 10, |               Positioned( | ||||||
|               bottom: 5, |                 right: 10, | ||||||
|               child: Icon( |                 bottom: 5, | ||||||
|                 (deviceId != asset.deviceId) |                 child: Icon( | ||||||
|                     ? Icons.cloud_done_outlined |                   (deviceId != asset.deviceId) | ||||||
|                     : Icons.photo_library_rounded, |                       ? Icons.cloud_done_outlined | ||||||
|                 color: Colors.white, |                       : Icons.photo_library_rounded, | ||||||
|                 size: 18, |                   color: Colors.white, | ||||||
|               ), |                   size: 18, | ||||||
|             ) |                 ), | ||||||
|  |               ) | ||||||
|           ], |           ], | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import 'package:immich_mobile/modules/settings/services/app_settings.service.dar | |||||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | import 'package:immich_mobile/shared/providers/asset.provider.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/websocket.provider.dart'; | import 'package:immich_mobile/shared/providers/websocket.provider.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/cache.service.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| 
 | 
 | ||||||
| class HomePage extends HookConsumerWidget { | class HomePage extends HookConsumerWidget { | ||||||
| @ -24,6 +25,7 @@ class HomePage extends HookConsumerWidget { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final appSettingService = ref.watch(appSettingsServiceProvider); |     final appSettingService = ref.watch(appSettingsServiceProvider); | ||||||
|  |     final cacheService = ref.watch(cacheServiceProvider); | ||||||
| 
 | 
 | ||||||
|     ScrollController scrollController = useScrollController(); |     ScrollController scrollController = useScrollController(); | ||||||
|     var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider); |     var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider); | ||||||
| @ -89,6 +91,7 @@ class HomePage extends HookConsumerWidget { | |||||||
| 
 | 
 | ||||||
|             imageGridGroup.add( |             imageGridGroup.add( | ||||||
|               ImageGrid( |               ImageGrid( | ||||||
|  |                 cacheManager: cacheService.getCache(CacheType.thumbnail), | ||||||
|                 assetGroup: immichAssetList, |                 assetGroup: immichAssetList, | ||||||
|                 sortedAssetGroup: sortedAssetList, |                 sortedAssetGroup: sortedAssetList, | ||||||
|                 tilesPerRow: |                 tilesPerRow: | ||||||
|  | |||||||
| @ -7,7 +7,10 @@ enum AppSettingsEnum<T> { | |||||||
|   tilesPerRow<int>("tilesPerRow", 4), |   tilesPerRow<int>("tilesPerRow", 4), | ||||||
|   uploadErrorNotificationGracePeriod<int>( |   uploadErrorNotificationGracePeriod<int>( | ||||||
|       "uploadErrorNotificationGracePeriod", 2), |       "uploadErrorNotificationGracePeriod", 2), | ||||||
|   storageIndicator<bool>("storageIndicator", true); |   storageIndicator<bool>("storageIndicator", true), | ||||||
|  |   thumbnailCacheSize<int>("thumbnailCacheSize", 10000), | ||||||
|  |   imageCacheSize<int>("imageCacheSize", 350), | ||||||
|  |   albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200); | ||||||
| 
 | 
 | ||||||
|   const AppSettingsEnum(this.hiveKey, this.defaultValue); |   const AppSettingsEnum(this.hiveKey, this.defaultValue); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,142 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings_slider_pref.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/cache.service.dart'; | ||||||
|  | import 'package:immich_mobile/utils/bytes_units.dart'; | ||||||
|  | 
 | ||||||
|  | class CacheSettings extends HookConsumerWidget { | ||||||
|  |   const CacheSettings({ | ||||||
|  |     Key? key, | ||||||
|  |   }) : super(key: key); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final CacheService cacheService = ref.watch(cacheServiceProvider); | ||||||
|  | 
 | ||||||
|  |     final clearCacheState = useState(false); | ||||||
|  | 
 | ||||||
|  |     Future<void> clearCache() async { | ||||||
|  |       await cacheService.emptyAllCaches(); | ||||||
|  |       clearCacheState.value = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Widget cacheStatisticsRow(String name, CacheType type) { | ||||||
|  |       final cacheSize = useState(0); | ||||||
|  |       final cacheAssets = useState(0); | ||||||
|  | 
 | ||||||
|  |       if (!clearCacheState.value) { | ||||||
|  |         final repo = cacheService.getCacheRepo(type); | ||||||
|  | 
 | ||||||
|  |         repo.open().then((_) { | ||||||
|  |           cacheSize.value = repo.getCacheSize(); | ||||||
|  |           cacheAssets.value = repo.getNumberOfCachedObjects(); | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         cacheSize.value = 0; | ||||||
|  |         cacheAssets.value = 0; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return Container( | ||||||
|  |         margin: const EdgeInsets.only(left: 20, bottom: 10), | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             Text( | ||||||
|  |               name, | ||||||
|  |               style: const TextStyle( | ||||||
|  |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             const Text( | ||||||
|  |               "cache_settings_statistics_assets", | ||||||
|  |               style: TextStyle(color: Colors.grey), | ||||||
|  |             ).tr( | ||||||
|  |               args: ["${cacheAssets.value}", formatBytes(cacheSize.value)], | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return ExpansionTile( | ||||||
|  |       expandedCrossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       textColor: Theme.of(context).primaryColor, | ||||||
|  |       title: const Text( | ||||||
|  |         'cache_settings_title', | ||||||
|  |         style: TextStyle( | ||||||
|  |           fontWeight: FontWeight.bold, | ||||||
|  |         ), | ||||||
|  |       ).tr(), | ||||||
|  |       subtitle: const Text( | ||||||
|  |         'cache_settings_subtitle', | ||||||
|  |         style: TextStyle( | ||||||
|  |           fontSize: 13, | ||||||
|  |         ), | ||||||
|  |       ).tr(), | ||||||
|  |       children: [ | ||||||
|  |         const CacheSettingsSliderPref( | ||||||
|  |           setting: AppSettingsEnum.thumbnailCacheSize, | ||||||
|  |           translationKey: "cache_settings_thumbnail_size", | ||||||
|  |           min: 1000, | ||||||
|  |           max: 20000, | ||||||
|  |           divisions: 19, | ||||||
|  |         ), | ||||||
|  |         const CacheSettingsSliderPref( | ||||||
|  |           setting: AppSettingsEnum.imageCacheSize, | ||||||
|  |           translationKey: "cache_settings_image_cache_size", | ||||||
|  |           min: 0, | ||||||
|  |           max: 1000, | ||||||
|  |           divisions: 20, | ||||||
|  |         ), | ||||||
|  |         const CacheSettingsSliderPref( | ||||||
|  |           setting: AppSettingsEnum.albumThumbnailCacheSize, | ||||||
|  |           translationKey: "cache_settings_album_thumbnails", | ||||||
|  |           min: 0, | ||||||
|  |           max: 1000, | ||||||
|  |           divisions: 20, | ||||||
|  |         ), | ||||||
|  |         ListTile( | ||||||
|  |           title: const Text( | ||||||
|  |             "cache_settings_statistics_title", | ||||||
|  |             style: TextStyle( | ||||||
|  |               fontSize: 12, | ||||||
|  |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|  |           ).tr(), | ||||||
|  |         ), | ||||||
|  |         cacheStatisticsRow( | ||||||
|  |             "cache_settings_statistics_thumbnail".tr(), CacheType.thumbnail), | ||||||
|  |         cacheStatisticsRow( | ||||||
|  |             "cache_settings_statistics_album".tr(), CacheType.albumThumbnail), | ||||||
|  |         cacheStatisticsRow("cache_settings_statistics_shared".tr(), | ||||||
|  |             CacheType.sharedAlbumThumbnail), | ||||||
|  |         cacheStatisticsRow( | ||||||
|  |             "cache_settings_statistics_full".tr(), CacheType.imageViewerFull), | ||||||
|  |         ListTile( | ||||||
|  |           title: const Text( | ||||||
|  |             "cache_settings_clear_cache_button_title", | ||||||
|  |             style: TextStyle( | ||||||
|  |               fontSize: 12, | ||||||
|  |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|  |           ).tr(), | ||||||
|  |         ), | ||||||
|  |         Container( | ||||||
|  |           alignment: Alignment.center, | ||||||
|  |           child: TextButton( | ||||||
|  |             onPressed: clearCache, | ||||||
|  |             child: Text( | ||||||
|  |               "cache_settings_clear_cache_button", | ||||||
|  |               style: TextStyle( | ||||||
|  |                 color: Theme.of(context).primaryColor, | ||||||
|  |               ), | ||||||
|  |             ).tr(), | ||||||
|  |           ), | ||||||
|  |         ) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,63 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||||
|  | 
 | ||||||
|  | class CacheSettingsSliderPref extends HookConsumerWidget { | ||||||
|  |   final AppSettingsEnum<int> setting; | ||||||
|  |   final String translationKey; | ||||||
|  |   final int min; | ||||||
|  |   final int max; | ||||||
|  |   final int divisions; | ||||||
|  | 
 | ||||||
|  |   const CacheSettingsSliderPref({ | ||||||
|  |     Key? key, | ||||||
|  |     required this.setting, | ||||||
|  |     required this.translationKey, | ||||||
|  |     required this.min, | ||||||
|  |     required this.max, | ||||||
|  |     required this.divisions, | ||||||
|  |   }) : super(key: key); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final appSettingService = ref.watch(appSettingsServiceProvider); | ||||||
|  | 
 | ||||||
|  |     final itemsValue = useState(appSettingService.getSetting<int>(setting)); | ||||||
|  | 
 | ||||||
|  |     void sliderChanged(double value) { | ||||||
|  |       itemsValue.value = value.toInt(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void sliderChangedEnd(double value) { | ||||||
|  |       appSettingService.setSetting(setting, value.toInt()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       children: [ | ||||||
|  |         ListTile( | ||||||
|  |           title: Text( | ||||||
|  |             translationKey, | ||||||
|  |             style: const TextStyle( | ||||||
|  |               fontSize: 12, | ||||||
|  |               fontWeight: FontWeight.bold, | ||||||
|  |             ), | ||||||
|  |           ).tr(args: ["${itemsValue.value.toInt()}"]), | ||||||
|  |         ), | ||||||
|  |         Slider( | ||||||
|  |           onChangeEnd: sliderChangedEnd, | ||||||
|  |           onChanged: sliderChanged, | ||||||
|  |           value: itemsValue.value.toDouble(), | ||||||
|  |           min: min.toDouble(), | ||||||
|  |           max: max.toDouble(), | ||||||
|  |           divisions: divisions, | ||||||
|  |           label: "${itemsValue.value.toInt()}", | ||||||
|  |           activeColor: Theme.of(context).primaryColor, | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; | |||||||
| 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/modules/settings/ui/asset_list_settings/asset_list_settings.dart'; | import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart'; | ||||||
|  | import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings.dart'; | ||||||
| import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart'; | import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart'; | ||||||
| import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart'; | import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart'; | ||||||
| import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart'; | import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart'; | ||||||
| @ -41,6 +42,7 @@ class SettingsPage extends HookConsumerWidget { | |||||||
|               const ImageViewerQualitySetting(), |               const ImageViewerQualitySetting(), | ||||||
|               const ThemeSetting(), |               const ThemeSetting(), | ||||||
|               const AssetListSettings(), |               const AssetListSettings(), | ||||||
|  |               const CacheSettings(), | ||||||
|               if (Platform.isAndroid) const NotificationSetting(), |               if (Platform.isAndroid) const NotificationSetting(), | ||||||
|             ], |             ], | ||||||
|           ).toList(), |           ).toList(), | ||||||
|  | |||||||
| @ -1,21 +1,79 @@ | |||||||
| import 'package:flutter_cache_manager/flutter_cache_manager.dart'; | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.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/utils/immich_cache_info_repository.dart'; | ||||||
| 
 | 
 | ||||||
| enum CacheType { | enum CacheType { | ||||||
|  |   // Shared cache for asset thumbnails in various modules | ||||||
|  |   thumbnail, | ||||||
|  | 
 | ||||||
|  |   imageViewerPreview, | ||||||
|  |   imageViewerFull, | ||||||
|   albumThumbnail, |   albumThumbnail, | ||||||
|   sharedAlbumThumbnail; |   sharedAlbumThumbnail; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| final cacheServiceProvider = Provider((_) => CacheService()); | final cacheServiceProvider = Provider( | ||||||
|  |   (ref) => CacheService(ref.watch(appSettingsServiceProvider)), | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| class CacheService { | class CacheService { | ||||||
|  |   final AppSettingsService _settingsService; | ||||||
|  |   final _cacheRepositoryInstances = <CacheType, ImmichCacheRepository>{}; | ||||||
|  | 
 | ||||||
|  |   CacheService(this._settingsService); | ||||||
| 
 | 
 | ||||||
|   BaseCacheManager getCache(CacheType type) { |   BaseCacheManager getCache(CacheType type) { | ||||||
|     return _getDefaultCache(type.name); |     return _getDefaultCache( | ||||||
|  |       type.name, | ||||||
|  |       _getCacheSize(type) + 1, | ||||||
|  |       getCacheRepo(type), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   BaseCacheManager _getDefaultCache(String cacheName) { |   ImmichCacheRepository getCacheRepo(CacheType type) { | ||||||
|     return CacheManager(Config(cacheName)); |     if (!_cacheRepositoryInstances.containsKey(type)) { | ||||||
|  |       final repo = ImmichCacheInfoRepository( | ||||||
|  |         "cache_${type.name}", | ||||||
|  |         "cacheKeys_${type.name}", | ||||||
|  |       ); | ||||||
|  |       _cacheRepositoryInstances[type] = repo; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return _cacheRepositoryInstances[type]!; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| } |   Future<void> emptyAllCaches() async { | ||||||
|  |     for (var type in CacheType.values) { | ||||||
|  |       await getCache(type).emptyCache(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   int _getCacheSize(CacheType type) { | ||||||
|  |     switch (type) { | ||||||
|  |       case CacheType.thumbnail: | ||||||
|  |         return _settingsService.getSetting(AppSettingsEnum.thumbnailCacheSize); | ||||||
|  |       case CacheType.imageViewerPreview: | ||||||
|  |       case CacheType.imageViewerFull: | ||||||
|  |         return _settingsService.getSetting(AppSettingsEnum.imageCacheSize); | ||||||
|  |       case CacheType.sharedAlbumThumbnail: | ||||||
|  |       case CacheType.albumThumbnail: | ||||||
|  |         return _settingsService | ||||||
|  |             .getSetting(AppSettingsEnum.albumThumbnailCacheSize); | ||||||
|  |       default: | ||||||
|  |         return 200; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   BaseCacheManager _getDefaultCache( | ||||||
|  |       String cacheName, int size, CacheInfoRepository repo) { | ||||||
|  |     return CacheManager( | ||||||
|  |       Config( | ||||||
|  |         cacheName, | ||||||
|  |         maxNrOfCacheObjects: size, | ||||||
|  |         repo: repo, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								mobile/lib/utils/bytes_units.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mobile/lib/utils/bytes_units.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | 
 | ||||||
|  | String formatBytes(int bytes) { | ||||||
|  |   if (bytes < 1000) { | ||||||
|  |     return "$bytes B"; | ||||||
|  |   } else if (bytes < 1000000) { | ||||||
|  |     final kb = (bytes / 1000).toStringAsFixed(1); | ||||||
|  |     return "$kb kB"; | ||||||
|  |   } else if (bytes < 1000000000) { | ||||||
|  |     final mb = (bytes / 1000000).toStringAsFixed(1); | ||||||
|  |     return "$mb MB"; | ||||||
|  |   } else { | ||||||
|  |     final gb = (bytes / 1000000000).toStringAsFixed(1); | ||||||
|  |     return "$gb GB"; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										204
									
								
								mobile/lib/utils/immich_cache_info_repository.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								mobile/lib/utils/immich_cache_info_repository.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,204 @@ | |||||||
|  | import 'dart:io'; | ||||||
|  | import 'dart:math'; | ||||||
|  | 
 | ||||||
|  | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; | ||||||
|  | import 'package:flutter_cache_manager/src/storage/cache_object.dart'; | ||||||
|  | import 'package:hive/hive.dart'; | ||||||
|  | import 'package:path/path.dart' as p; | ||||||
|  | import 'package:path_provider/path_provider.dart'; | ||||||
|  | 
 | ||||||
|  | // Implementation of a CacheInfoRepository based on Hive | ||||||
|  | abstract class ImmichCacheRepository extends CacheInfoRepository { | ||||||
|  |   int getNumberOfCachedObjects(); | ||||||
|  |   int getCacheSize(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class ImmichCacheInfoRepository extends ImmichCacheRepository { | ||||||
|  |   final String hiveBoxName; | ||||||
|  |   final String keyLookupHiveBoxName; | ||||||
|  | 
 | ||||||
|  |   // To circumvent some of the limitations of a non-relational key-value database, | ||||||
|  |   // we use two hive boxes per cache. | ||||||
|  |   // [cacheObjectLookupBox] maps ids to cache objects. | ||||||
|  |   // [keyLookupHiveBox] maps keys to ids. | ||||||
|  |   // The lookup of a cache object by key therefore involves two steps: | ||||||
|  |   // id = keyLookupHiveBox[key] | ||||||
|  |   // object = cacheObjectLookupBox[id] | ||||||
|  |   late Box<Map<dynamic, dynamic>> cacheObjectLookupBox; | ||||||
|  |   late Box<int> keyLookupHiveBox; | ||||||
|  | 
 | ||||||
|  |   ImmichCacheInfoRepository(this.hiveBoxName, this.keyLookupHiveBoxName); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<bool> close() async { | ||||||
|  |     await cacheObjectLookupBox.close(); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<int> delete(int id) async { | ||||||
|  |     if (cacheObjectLookupBox.containsKey(id)) { | ||||||
|  |       await cacheObjectLookupBox.delete(id); | ||||||
|  |       return 1; | ||||||
|  |     } | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<int> deleteAll(Iterable<int> ids) async { | ||||||
|  |     int deleted = 0; | ||||||
|  |     for (var id in ids) { | ||||||
|  |       if (cacheObjectLookupBox.containsKey(id)) { | ||||||
|  |         deleted++; | ||||||
|  |         await cacheObjectLookupBox.delete(id); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return deleted; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<void> deleteDataFile() async { | ||||||
|  |     await cacheObjectLookupBox.clear(); | ||||||
|  |     await keyLookupHiveBox.clear(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<bool> exists() async { | ||||||
|  |     return cacheObjectLookupBox.isNotEmpty && keyLookupHiveBox.isNotEmpty; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<CacheObject?> get(String key) async { | ||||||
|  |     if (!keyLookupHiveBox.containsKey(key)) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     int id = keyLookupHiveBox.get(key)!; | ||||||
|  |     if (!cacheObjectLookupBox.containsKey(id)) { | ||||||
|  |       keyLookupHiveBox.delete(key); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return _deserialize(cacheObjectLookupBox.get(id)!); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<List<CacheObject>> getAllObjects() async { | ||||||
|  |     return cacheObjectLookupBox.values.map(_deserialize).toList(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<List<CacheObject>> getObjectsOverCapacity(int capacity) async { | ||||||
|  |     if (cacheObjectLookupBox.length <= capacity) { | ||||||
|  |       return List.empty(); | ||||||
|  |     } | ||||||
|  |     var values = cacheObjectLookupBox.values.map(_deserialize).toList(); | ||||||
|  |     values.sort((CacheObject a, CacheObject b) { | ||||||
|  |       final aTouched = a.touched ?? DateTime.fromMicrosecondsSinceEpoch(0); | ||||||
|  |       final bTouched = b.touched ?? DateTime.fromMicrosecondsSinceEpoch(0); | ||||||
|  | 
 | ||||||
|  |       return aTouched.compareTo(bTouched); | ||||||
|  |     }); | ||||||
|  |     return values.skip(capacity).toList(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<List<CacheObject>> getOldObjects(Duration maxAge) async { | ||||||
|  |     return cacheObjectLookupBox.values | ||||||
|  |         .map(_deserialize) | ||||||
|  |         .where((CacheObject element) { | ||||||
|  |       DateTime touched = | ||||||
|  |           element.touched ?? DateTime.fromMicrosecondsSinceEpoch(0); | ||||||
|  |       return touched.isBefore(DateTime.now().subtract(maxAge)); | ||||||
|  |     }).toList(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<CacheObject> insert(CacheObject cacheObject, | ||||||
|  |       {bool setTouchedToNow = true}) async { | ||||||
|  |     int newId = keyLookupHiveBox.length == 0 | ||||||
|  |         ? 0 | ||||||
|  |         : keyLookupHiveBox.values.reduce(max) + 1; | ||||||
|  |     cacheObject = cacheObject.copyWith(id: newId); | ||||||
|  | 
 | ||||||
|  |     keyLookupHiveBox.put(cacheObject.key, newId); | ||||||
|  |     cacheObjectLookupBox.put(newId, cacheObject.toMap()); | ||||||
|  | 
 | ||||||
|  |     return cacheObject; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<bool> open() async { | ||||||
|  |     cacheObjectLookupBox = await Hive.openBox(hiveBoxName); | ||||||
|  |     keyLookupHiveBox = await Hive.openBox(keyLookupHiveBoxName); | ||||||
|  | 
 | ||||||
|  |     // The cache might have cleared by the operating system. | ||||||
|  |     // This could create inconsistencies between the file system cache and database. | ||||||
|  |     // To check whether the cache was cleared, a file within the cache directory | ||||||
|  |     // is created for each database. If the file is absent, the cache was cleared and therefore | ||||||
|  |     // the database has to be cleared as well. | ||||||
|  |     if (!await _checkAndCreateAnchorFile()) { | ||||||
|  |       await cacheObjectLookupBox.clear(); | ||||||
|  |       await keyLookupHiveBox.clear(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return cacheObjectLookupBox.isOpen; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<int> update(CacheObject cacheObject, | ||||||
|  |       {bool setTouchedToNow = true}) async { | ||||||
|  |     if (cacheObject.id != null) { | ||||||
|  |       cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap()); | ||||||
|  |       return 1; | ||||||
|  |     } | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future updateOrInsert(CacheObject cacheObject) { | ||||||
|  |     if (cacheObject.id == null) { | ||||||
|  |       return insert(cacheObject); | ||||||
|  |     } else { | ||||||
|  |       return update(cacheObject); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int getNumberOfCachedObjects() { | ||||||
|  |     return cacheObjectLookupBox.length; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int getCacheSize() { | ||||||
|  |     final cacheElementsWithSize = | ||||||
|  |         cacheObjectLookupBox.values.map(_deserialize).map((e) => e.length ?? 0); | ||||||
|  | 
 | ||||||
|  |     if (cacheElementsWithSize.isEmpty) { | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return cacheElementsWithSize.reduce((value, element) => value + element); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   CacheObject _deserialize(Map serData) { | ||||||
|  |     Map<String, dynamic> converted = {}; | ||||||
|  | 
 | ||||||
|  |     serData.forEach((key, value) { | ||||||
|  |       converted[key.toString()] = value; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return CacheObject.fromMap(converted); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<bool> _checkAndCreateAnchorFile() async { | ||||||
|  |     final tmpDir = await getTemporaryDirectory(); | ||||||
|  |     final cacheFile = File(p.join(tmpDir.path, "$hiveBoxName.tmp")); | ||||||
|  | 
 | ||||||
|  |     if (await cacheFile.exists()) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await cacheFile.create(); | ||||||
|  | 
 | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user