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
17 changed files with 593 additions and 44 deletions
|
@ -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…
Reference in a new issue