diff --git a/mobile/lib/modules/album/providers/album.provider.dart b/mobile/lib/modules/album/providers/album.provider.dart index f86ffa3ee544f..9a098aa49b208 100644 --- a/mobile/lib/modules/album/providers/album.provider.dart +++ b/mobile/lib/modules/album/providers/album.provider.dart @@ -1,22 +1,35 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart'; +import 'package:immich_mobile/modules/album/services/album_cache.service.dart'; import 'package:openapi/api.dart'; class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this._albumService) : super([]); + AlbumNotifier(this._albumService, this._albumCacheService) : super([]); final AlbumService _albumService; + final AlbumCacheService _albumCacheService; + + _cacheState() { + _albumCacheService.put(state); + } getAllAlbums() async { + + if (await _albumCacheService.isValid() && state.isEmpty) { + state = await _albumCacheService.get(); + } + List? albums = await _albumService.getAlbums(isShared: false); if (albums != null) { state = albums; + _cacheState(); } } deleteAlbum(String albumId) { state = state.where((album) => album.id != albumId).toList(); + _cacheState(); } Future createAlbum( @@ -28,6 +41,8 @@ class AlbumNotifier extends StateNotifier> { if (album != null) { state = [...state, album]; + _cacheState(); + return album; } return null; @@ -36,5 +51,8 @@ class AlbumNotifier extends StateNotifier> { final albumProvider = StateNotifierProvider>((ref) { - return AlbumNotifier(ref.watch(albumServiceProvider)); + return AlbumNotifier( + ref.watch(albumServiceProvider), + ref.watch(albumCacheServiceProvider), + ); }); diff --git a/mobile/lib/modules/album/providers/shared_album.provider.dart b/mobile/lib/modules/album/providers/shared_album.provider.dart index 202e8241f022a..c759f3263b7cd 100644 --- a/mobile/lib/modules/album/providers/shared_album.provider.dart +++ b/mobile/lib/modules/album/providers/shared_album.provider.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart'; +import 'package:immich_mobile/modules/album/services/album_cache.service.dart'; import 'package:openapi/api.dart'; class SharedAlbumNotifier extends StateNotifier> { - SharedAlbumNotifier(this._sharedAlbumService) : super([]); + SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]); final AlbumService _sharedAlbumService; + final SharedAlbumCacheService _sharedAlbumCacheService; + + _cacheState() { + _sharedAlbumCacheService.put(state); + } Future createSharedAlbum( String albumName, @@ -22,6 +28,7 @@ class SharedAlbumNotifier extends StateNotifier> { if (newAlbum != null) { state = [...state, newAlbum]; + _cacheState(); } return newAlbum; @@ -33,16 +40,22 @@ class SharedAlbumNotifier extends StateNotifier> { } getAllSharedAlbums() async { + if (await _sharedAlbumCacheService.isValid() && state.isEmpty) { + state = await _sharedAlbumCacheService.get(); + } + List? sharedAlbums = await _sharedAlbumService.getAlbums(isShared: true); if (sharedAlbums != null) { state = sharedAlbums; + _cacheState(); } } deleteAlbum(String albumId) async { state = state.where((album) => album.id != albumId).toList(); + _cacheState(); } Future leaveAlbum(String albumId) async { @@ -50,6 +63,7 @@ class SharedAlbumNotifier extends StateNotifier> { if (res) { state = state.where((album) => album.id != albumId).toList(); + _cacheState(); return true; } else { return false; @@ -72,7 +86,10 @@ class SharedAlbumNotifier extends StateNotifier> { final sharedAlbumProvider = StateNotifierProvider>((ref) { - return SharedAlbumNotifier(ref.watch(albumServiceProvider)); + return SharedAlbumNotifier( + ref.watch(albumServiceProvider), + ref.watch(sharedAlbumCacheServiceProvider), + ); }); final sharedAlbumDetailProvider = FutureProvider.autoDispose diff --git a/mobile/lib/modules/album/services/album_cache.service.dart b/mobile/lib/modules/album/services/album_cache.service.dart new file mode 100644 index 0000000000000..0e16056585ba3 --- /dev/null +++ b/mobile/lib/modules/album/services/album_cache.service.dart @@ -0,0 +1,49 @@ + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/services/json_cache.dart'; +import 'package:openapi/api.dart'; + +class BaseAlbumCacheService extends JsonCache> { + BaseAlbumCacheService(super.cacheFileName); + + @override + void put(List data) { + putRawData(data.map((e) => e.toJson()).toList()); + } + + @override + Future> get() async { + try { + final mapList = await readRawData() as List; + + final responseData = mapList + .map((e) => AlbumResponseDto.fromJson(e)) + .whereNotNull() + .toList(); + + return responseData; + } catch (e) { + debugPrint(e.toString()); + return []; + } + } +} + +class AlbumCacheService extends BaseAlbumCacheService { + AlbumCacheService() : super("album_cache"); +} + +class SharedAlbumCacheService extends BaseAlbumCacheService { + SharedAlbumCacheService() : super("shared_album_cache"); +} + +final albumCacheServiceProvider = Provider( + (ref) => AlbumCacheService(), +); + +final sharedAlbumCacheServiceProvider = Provider( + (ref) => SharedAlbumCacheService(), +); + diff --git a/mobile/lib/modules/home/services/asset_cache.service.dart b/mobile/lib/modules/home/services/asset_cache.service.dart new file mode 100644 index 0000000000000..9675938b3b2f6 --- /dev/null +++ b/mobile/lib/modules/home/services/asset_cache.service.dart @@ -0,0 +1,37 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/services/json_cache.dart'; +import 'package:openapi/api.dart'; + + +class AssetCacheService extends JsonCache> { + AssetCacheService() : super("asset_cache"); + + @override + void put(List data) { + putRawData(data.map((e) => e.toJson()).toList()); + } + + @override + Future> get() async { + try { + final mapList = await readRawData() as List; + + final responseData = mapList + .map((e) => AssetResponseDto.fromJson(e)) + .whereNotNull() + .toList(); + + return responseData; + } catch (e) { + debugPrint(e.toString()); + + return []; + } + } +} + +final assetCacheServiceProvider = Provider( + (ref) => AssetCacheService(), +); diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index 022faf49ecf89..c642f93b71b72 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/modules/album/services/album_cache.service.dart'; +import 'package:immich_mobile/modules/home/services/asset_cache.service.dart'; import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/backup/services/backup.service.dart'; @@ -16,6 +18,9 @@ class AuthenticationNotifier extends StateNotifier { this._deviceInfoService, this._backupService, this._apiService, + this._assetCacheService, + this._albumCacheService, + this._sharedAlbumCacheService, ) : super( AuthenticationState( deviceId: "", @@ -42,6 +47,9 @@ class AuthenticationNotifier extends StateNotifier { final DeviceInfoService _deviceInfoService; final BackupService _backupService; final ApiService _apiService; + final AssetCacheService _assetCacheService; + final AlbumCacheService _albumCacheService; + final SharedAlbumCacheService _sharedAlbumCacheService; Future login( String email, @@ -153,7 +161,9 @@ class AuthenticationNotifier extends StateNotifier { Future logout() async { Hive.box(userInfoBox).delete(accessTokenKey); state = state.copyWith(isAuthenticated: false); - + _assetCacheService.invalidate(); + _albumCacheService.invalidate(); + _sharedAlbumCacheService.invalidate(); return true; } @@ -199,5 +209,8 @@ final authenticationProvider = ref.watch(deviceInfoServiceProvider), ref.watch(backupServiceProvider), ref.watch(apiServiceProvider), + ref.watch(assetCacheServiceProvider), + ref.watch(albumCacheServiceProvider), + ref.watch(sharedAlbumCacheServiceProvider), ); }); diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 6329995ade07d..386d71cae0fcd 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; +import 'package:immich_mobile/modules/home/services/asset_cache.service.dart'; import 'package:immich_mobile/shared/services/device_info.service.dart'; import 'package:collection/collection.dart'; import 'package:intl/intl.dart'; @@ -9,24 +10,50 @@ import 'package:photo_manager/photo_manager.dart'; class AssetNotifier extends StateNotifier> { final AssetService _assetService; + final AssetCacheService _assetCacheService; + final DeviceInfoService _deviceInfoService = DeviceInfoService(); - AssetNotifier(this._assetService) : super([]); + AssetNotifier(this._assetService, this._assetCacheService) : super([]); + + _cacheState() { + _assetCacheService.put(state); + } getAllAsset() async { + final stopwatch = Stopwatch(); + + + if (await _assetCacheService.isValid() && state.isEmpty) { + stopwatch.start(); + state = await _assetCacheService.get(); + debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms"); + stopwatch.reset(); + } + + stopwatch.start(); var allAssets = await _assetService.getAllAsset(); + debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms"); + stopwatch.reset(); if (allAssets != null) { state = allAssets; + + stopwatch.start(); + _cacheState(); + debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); + stopwatch.reset(); } } clearAllAsset() { state = []; + _cacheState(); } onNewAssetUploaded(AssetResponseDto newAsset) { state = [...state, newAsset]; + _cacheState(); } deleteAssets(Set deleteAssets) async { @@ -65,12 +92,15 @@ class AssetNotifier extends StateNotifier> { state.where((immichAsset) => immichAsset.id != asset.id).toList(); } } + + _cacheState(); } } final assetProvider = StateNotifierProvider>((ref) { - return AssetNotifier(ref.watch(assetServiceProvider)); + return AssetNotifier( + ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider)); }); final assetGroupByDateTimeProvider = StateProvider((ref) { diff --git a/mobile/lib/shared/services/json_cache.dart b/mobile/lib/shared/services/json_cache.dart new file mode 100644 index 0000000000000..b8a403abbaf2f --- /dev/null +++ b/mobile/lib/shared/services/json_cache.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + +abstract class JsonCache { + final String cacheFileName; + + JsonCache(this.cacheFileName); + + Future _getCacheFile() async { + final basePath = await getTemporaryDirectory(); + final basePathName = basePath.path; + + final file = File("$basePathName/$cacheFileName.bin"); + + return file; + } + + Future isValid() async { + final file = await _getCacheFile(); + return await file.exists(); + } + + Future invalidate() async { + final file = await _getCacheFile(); + await file.delete(); + } + + Future putRawData(dynamic data) async { + final jsonString = json.encode(data); + final file = await _getCacheFile(); + + if (!await file.exists()) { + await file.create(); + } + + await file.writeAsString(jsonString); + } + + dynamic readRawData() async { + final file = await _getCacheFile(); + final data = await file.readAsString(); + return json.decode(data); + } + + void put(T data); + Future get(); +} \ No newline at end of file