You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	refactor(mobile): migrate all Hive boxes to Isar database (#2036)
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							0616a66b05
						
					
				
				
					commit
					eccde8fa07
				
			| @@ -2,7 +2,6 @@ import 'dart:async'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter_test/flutter_test.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:integration_test/integration_test.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| @@ -35,9 +34,7 @@ class ImmichTestHelper { | ||||
|   } | ||||
|  | ||||
|   static Future<void> loadApp(WidgetTester tester) async { | ||||
|     // Clear all data from Hive | ||||
|     await Hive.deleteFromDisk(); | ||||
|     await app.openBoxes(); | ||||
|     await EasyLocalization.ensureInitialized(); | ||||
|     // Clear all data from Isar (reuse existing instance if available) | ||||
|     final db = Isar.getInstance() ?? await app.loadDb(); | ||||
|     await Store.clear(); | ||||
| @@ -65,12 +62,13 @@ void immichWidgetTest( | ||||
| } | ||||
|  | ||||
| Future<void> pumpUntilFound( | ||||
|     WidgetTester tester, | ||||
|     Finder finder, { | ||||
|       Duration timeout = const Duration(seconds: 120), | ||||
|     }) async { | ||||
|   WidgetTester tester, | ||||
|   Finder finder, { | ||||
|   Duration timeout = const Duration(seconds: 120), | ||||
| }) async { | ||||
|   bool found = false; | ||||
|   final timer = Timer(timeout, () => throw TimeoutException("Pump until has timed out")); | ||||
|   final timer = | ||||
|       Timer(timeout, () => throw TimeoutException("Pump until has timed out")); | ||||
|   while (found != true) { | ||||
|     await tester.pump(); | ||||
|     found = tester.any(finder); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/exif_info.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/logger_message.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/providers/app_state.provider.dart'; | ||||
| @@ -42,35 +43,23 @@ import 'package:isar/isar.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'constants/hive_box.dart'; | ||||
|  | ||||
| void main() async { | ||||
|   await initApp(); | ||||
|   WidgetsFlutterBinding.ensureInitialized(); | ||||
|   final db = await loadDb(); | ||||
|   await initApp(); | ||||
|   await migrateHiveToStoreIfNecessary(); | ||||
|   await migrateJsonCacheIfNecessary(); | ||||
|   runApp(getMainWidget(db)); | ||||
| } | ||||
|  | ||||
| Future<void> openBoxes() async { | ||||
|   await Future.wait([ | ||||
|     Hive.openBox<ImmichLoggerMessage>(immichLoggerBox), | ||||
|     Hive.openBox(userInfoBox), | ||||
|     Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox), | ||||
|     Hive.openBox(hiveGithubReleaseInfoBox), | ||||
|     Hive.openBox(userSettingInfoBox), | ||||
|     EasyLocalization.ensureInitialized(), | ||||
|   ]); | ||||
| } | ||||
|  | ||||
| Future<void> initApp() async { | ||||
|   await Hive.initFlutter(); | ||||
|   Hive.registerAdapter(HiveSavedLoginInfoAdapter()); | ||||
|   Hive.registerAdapter(HiveBackupAlbumsAdapter()); | ||||
|   Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); | ||||
|   Hive.registerAdapter(ImmichLoggerMessageAdapter()); | ||||
|  | ||||
|   await openBoxes(); | ||||
|   await EasyLocalization.ensureInitialized(); | ||||
|  | ||||
|   if (kReleaseMode && Platform.isAndroid) { | ||||
|     try { | ||||
| @@ -82,7 +71,7 @@ Future<void> initApp() async { | ||||
|   } | ||||
|  | ||||
|   // Initialize Immich Logger Service | ||||
|   ImmichLogger().init(); | ||||
|   ImmichLogger(); | ||||
|  | ||||
|   var log = Logger("ImmichErrorLogger"); | ||||
|  | ||||
| @@ -108,6 +97,7 @@ Future<Isar> loadDb() async { | ||||
|       UserSchema, | ||||
|       BackupAlbumSchema, | ||||
|       DuplicatedAssetSchema, | ||||
|       LoggerMessageSchema, | ||||
|     ], | ||||
|     directory: dir.path, | ||||
|     maxSizeMiB: 256, | ||||
| @@ -174,6 +164,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> | ||||
|       case AppLifecycleState.inactive: | ||||
|         debugPrint("[APP STATE] inactive"); | ||||
|         ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive; | ||||
|         ImmichLogger().flush(); | ||||
|         ref.watch(websocketProvider.notifier).disconnect(); | ||||
|         ref.watch(backupProvider.notifier).cancelBackup(); | ||||
|  | ||||
|   | ||||
| @@ -265,7 +265,7 @@ class AlbumService { | ||||
|  | ||||
|   Future<bool> deleteAlbum(Album album) async { | ||||
|     try { | ||||
|       final userId = Store.get<User>(StoreKey.currentUser)!.isarId; | ||||
|       final userId = Store.get(StoreKey.currentUser).isarId; | ||||
|       if (album.owner.value?.isarId == userId) { | ||||
|         await _apiService.albumApi.deleteAlbum(album.remoteId!); | ||||
|       } | ||||
|   | ||||
| @@ -2,10 +2,9 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| @@ -21,7 +20,6 @@ class AlbumThumbnailListTile extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     var cardSize = 68.0; | ||||
|     var isDarkMode = Theme.of(context).brightness == Brightness.dark; | ||||
|  | ||||
| @@ -50,7 +48,9 @@ class AlbumThumbnailListTile extends StatelessWidget { | ||||
|           album, | ||||
|           type: ThumbnailFormat.JPEG, | ||||
|         ), | ||||
|         httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, | ||||
|         httpHeaders: { | ||||
|           "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}" | ||||
|         }, | ||||
|         cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG), | ||||
|         errorWidget: (context, url, error) => | ||||
|             const Icon(Icons.image_not_supported_outlined), | ||||
|   | ||||
| @@ -4,16 +4,15 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; | ||||
| import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/services/asset.service.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; | ||||
| import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; | ||||
| @@ -47,7 +46,6 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final Box<dynamic> box = Hive.box(userInfoBox); | ||||
|     final settings = ref.watch(appSettingsServiceProvider); | ||||
|     final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); | ||||
|     final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); | ||||
| @@ -57,7 +55,7 @@ class GalleryViewerPage extends HookConsumerWidget { | ||||
|     final isPlayingMotionVideo = useState(false); | ||||
|     final isPlayingVideo = useState(false); | ||||
|     late Offset localPosition; | ||||
|     final authToken = 'Bearer ${box.get(accessTokenKey)}'; | ||||
|     final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}'; | ||||
|  | ||||
|     showAppBar.addListener(() { | ||||
|       // Change to and from immersive mode, hiding navigation and app bar | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:chewie/chewie.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
| import 'package:video_player/video_player.dart'; | ||||
| @@ -54,17 +53,15 @@ class VideoViewerPage extends HookConsumerWidget { | ||||
|     } | ||||
|     final downloadAssetStatus = | ||||
|         ref.watch(imageViewerStateProvider).downloadAssetStatus; | ||||
|     final box = Hive.box(userInfoBox); | ||||
|     final String jwtToken = box.get(accessTokenKey); | ||||
|     final String videoUrl = isMotionVideo | ||||
|         ? '${box.get(serverEndpointKey)}/asset/file/${asset.livePhotoVideoId}' | ||||
|         : '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}'; | ||||
|         ? '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.livePhotoVideoId}' | ||||
|         : '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}'; | ||||
|  | ||||
|     return Stack( | ||||
|       children: [ | ||||
|         VideoThumbnailPlayer( | ||||
|           url: videoUrl, | ||||
|           jwtToken: jwtToken, | ||||
|           jwtToken: Store.get(StoreKey.accessToken), | ||||
|           isMotionVideo: isMotionVideo, | ||||
|           onVideoEnded: onVideoEnded, | ||||
|           onPaused: onPaused, | ||||
|   | ||||
| @@ -8,16 +8,13 @@ import 'package:collection/collection.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/main.dart'; | ||||
| import 'package:immich_mobile/modules/backup/background_service/localization.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| @@ -317,7 +314,6 @@ class BackgroundService { | ||||
|           debugPrint(error.toString()); | ||||
|           return false; | ||||
|         } finally { | ||||
|           await Hive.close(); | ||||
|           releaseLock(); | ||||
|         } | ||||
|       case "systemStop": | ||||
| @@ -332,17 +328,9 @@ class BackgroundService { | ||||
|  | ||||
|   Future<bool> _onAssetsChanged() async { | ||||
|     final Isar db = await loadDb(); | ||||
|     await Hive.initFlutter(); | ||||
|  | ||||
|     Hive.registerAdapter(HiveSavedLoginInfoAdapter()); | ||||
|  | ||||
|     await Future.wait([ | ||||
|       Hive.openBox(userInfoBox), | ||||
|       Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox), | ||||
|       Hive.openBox(userSettingInfoBox), | ||||
|     ]); | ||||
|     ApiService apiService = ApiService(); | ||||
|     apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey)); | ||||
|     apiService.setAccessToken(Store.get(StoreKey.accessToken)); | ||||
|     BackupService backupService = BackupService(apiService, db); | ||||
|     AppSettingsService settingsService = AppSettingsService(); | ||||
|  | ||||
| @@ -387,7 +375,7 @@ class BackgroundService { | ||||
|           db.backupAlbums.deleteAllSync(toDelete); | ||||
|           db.backupAlbums.putAllSync(toUpsert); | ||||
|         }); | ||||
|       } else if (Store.get(StoreKey.backupFailedSince) == null) { | ||||
|       } else if (Store.tryGet(StoreKey.backupFailedSince) == null) { | ||||
|         Store.put(StoreKey.backupFailedSince, DateTime.now()); | ||||
|         return false; | ||||
|       } | ||||
| @@ -529,7 +517,7 @@ class BackgroundService { | ||||
|     } else if (value == 5) { | ||||
|       return false; | ||||
|     } | ||||
|     final DateTime? failedSince = Store.get(StoreKey.backupFailedSince); | ||||
|     final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince); | ||||
|     if (failedSince == null) { | ||||
|       return false; | ||||
|     } | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| import 'package:cancellation_token_http/http.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; | ||||
| @@ -42,9 +40,10 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|             progressInPercentage: 0, | ||||
|             cancelToken: CancellationToken(), | ||||
|             backgroundBackup: false, | ||||
|             backupRequireWifi: true, | ||||
|             backupRequireCharging: false, | ||||
|             backupTriggerDelay: 5000, | ||||
|             backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), | ||||
|             backupRequireCharging: | ||||
|                 Store.get(StoreKey.backupRequireCharging, false), | ||||
|             backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000), | ||||
|             serverInfo: ServerInfoResponseDto( | ||||
|               diskAvailable: "0", | ||||
|               diskAvailableRaw: 0, | ||||
| @@ -163,14 +162,12 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|             triggerMaxDelay: state.backupTriggerDelay * 10, | ||||
|           ); | ||||
|       if (success) { | ||||
|         await Future.wait([ | ||||
|           Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi), | ||||
|           Store.put( | ||||
|             StoreKey.backupRequireCharging, | ||||
|             state.backupRequireCharging, | ||||
|           ), | ||||
|           Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay), | ||||
|         ]); | ||||
|         await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi); | ||||
|         await Store.put( | ||||
|           StoreKey.backupRequireCharging, | ||||
|           state.backupRequireCharging, | ||||
|         ); | ||||
|         await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay); | ||||
|       } else { | ||||
|         state = state.copyWith( | ||||
|           backgroundBackup: wasEnabled, | ||||
| @@ -544,7 +541,7 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|  | ||||
|   Future<void> _resumeBackup() async { | ||||
|     // Check if user is login | ||||
|     final accessKey = Hive.box(userInfoBox).get(accessTokenKey); | ||||
|     final accessKey = Store.tryGet(StoreKey.accessToken); | ||||
|  | ||||
|     // User has been logged out return | ||||
|     if (accessKey == null || !_authState.isAuthenticated) { | ||||
| @@ -603,9 +600,6 @@ class BackupNotifier extends StateNotifier<BackUpState> { | ||||
|       backupProgress: BackUpProgressEnum.inBackground, | ||||
|       selectedBackupAlbums: selectedAlbums, | ||||
|       excludedBackupAlbums: excludedAlbums, | ||||
|       backupRequireWifi: Store.get(StoreKey.backupRequireWifi), | ||||
|       backupRequireCharging: Store.get(StoreKey.backupRequireCharging), | ||||
|       backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay), | ||||
|     ); | ||||
|     // assumes the background service is currently running | ||||
|     // if true, waits until it has stopped to start the backup | ||||
|   | ||||
| @@ -5,13 +5,12 @@ import 'dart:io'; | ||||
| import 'package:cancellation_token_http/http.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/material.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/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/db.provider.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| @@ -38,7 +37,7 @@ class BackupService { | ||||
|   BackupService(this._apiService, this._db); | ||||
|  | ||||
|   Future<List<String>?> getDeviceBackupAsset() async { | ||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|     final String deviceId = Store.get(StoreKey.deviceId); | ||||
|  | ||||
|     try { | ||||
|       return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId); | ||||
| @@ -173,7 +172,7 @@ class BackupService { | ||||
|     } | ||||
|     final Set<String> existing = {}; | ||||
|     try { | ||||
|       final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|       final String deviceId = Store.get(StoreKey.deviceId); | ||||
|       final CheckExistingAssetsResponseDto? duplicates = | ||||
|           await _apiService.assetApi.checkExistingAssets( | ||||
|         CheckExistingAssetsDto( | ||||
| @@ -204,8 +203,8 @@ class BackupService { | ||||
|     Function(CurrentUploadAsset) setCurrentUploadAssetCb, | ||||
|     Function(ErrorUploadAsset) errorCb, | ||||
|   ) async { | ||||
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey); | ||||
|     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|     final String deviceId = Store.get(StoreKey.deviceId); | ||||
|     final String savedEndpoint = Store.get(StoreKey.serverEndpoint); | ||||
|     File? file; | ||||
|     bool anyErrors = false; | ||||
|     final List<String> duplicatedAssetIds = []; | ||||
| @@ -236,15 +235,14 @@ class BackupService { | ||||
|             ), | ||||
|           ); | ||||
|  | ||||
|           var box = Hive.box(userInfoBox); | ||||
|  | ||||
|           var req = MultipartRequest( | ||||
|             'POST', | ||||
|             Uri.parse('$savedEndpoint/asset/upload'), | ||||
|             onProgress: ((bytes, totalBytes) => | ||||
|                 uploadProgressCb(bytes, totalBytes)), | ||||
|           ); | ||||
|           req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; | ||||
|           req.headers["Authorization"] = | ||||
|               "Bearer ${Store.get(StoreKey.accessToken)}"; | ||||
|  | ||||
|           req.fields['deviceAssetId'] = entity.id; | ||||
|           req.fields['deviceId'] = deviceId; | ||||
|   | ||||
| @@ -2,9 +2,7 @@ import 'dart:math'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.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/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
|  | ||||
| @@ -12,6 +10,7 @@ import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/server_info_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/providers/server_info.provider.dart'; | ||||
| import 'package:immich_mobile/shared/ui/transparent_image.dart'; | ||||
|  | ||||
| @@ -47,7 +46,7 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget { | ||||
|           }, | ||||
|         ); | ||||
|       } else { | ||||
|         String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|         final String? endpoint = Store.get(StoreKey.serverEndpoint); | ||||
|         var dummy = Random().nextInt(1024); | ||||
|         return InkWell( | ||||
|           onTap: () { | ||||
|   | ||||
| @@ -1,14 +1,13 @@ | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/shared/ui/transparent_image.dart'; | ||||
|  | ||||
| @@ -19,7 +18,7 @@ class ProfileDrawerHeader extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); | ||||
|     final String endpoint = Store.get(StoreKey.serverEndpoint); | ||||
|     AuthenticationState authState = ref.watch(authenticationProvider); | ||||
|     final uploadProfileImageStatus = | ||||
|         ref.watch(uploadProfileImageProvider).status; | ||||
|   | ||||
| @@ -2,12 +2,9 @@ import 'dart:io'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| 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/shared/models/store.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'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| @@ -91,11 +88,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|     try { | ||||
|       await Future.wait([ | ||||
|         _apiService.authenticationApi.logout(), | ||||
|         Hive.box(userInfoBox).delete(accessTokenKey), | ||||
|         Store.delete(StoreKey.assetETag), | ||||
|         Store.delete(StoreKey.userRemoteId), | ||||
|         Store.delete(StoreKey.currentUser), | ||||
|         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey) | ||||
|         Store.delete(StoreKey.accessToken), | ||||
|       ]); | ||||
|  | ||||
|       state = state.copyWith(isAuthenticated: false); | ||||
| @@ -157,14 +153,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|     } | ||||
|  | ||||
|     if (userResponseDto != null) { | ||||
|       var userInfoHiveBox = await Hive.openBox(userInfoBox); | ||||
|       var deviceInfo = await _deviceInfoService.getDeviceInfo(); | ||||
|       userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]); | ||||
|       userInfoHiveBox.put(accessTokenKey, accessToken); | ||||
|       Store.put(StoreKey.deviceId, deviceInfo["deviceId"]); | ||||
|       Store.put(StoreKey.deviceIdHash, fastHash(deviceInfo["deviceId"])); | ||||
|       Store.put(StoreKey.userRemoteId, userResponseDto.id); | ||||
|       Store.put(StoreKey.currentUser, User.fromDto(userResponseDto)); | ||||
|       Store.put(StoreKey.serverUrl, serverUrl); | ||||
|       Store.put(StoreKey.accessToken, accessToken); | ||||
|  | ||||
|       state = state.copyWith( | ||||
|         isAuthenticated: true, | ||||
| @@ -178,17 +173,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> { | ||||
|         deviceId: deviceInfo["deviceId"], | ||||
|         deviceType: deviceInfo["deviceType"], | ||||
|       ); | ||||
|  | ||||
|       // Save login info to local storage | ||||
|       Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put( | ||||
|         savedLoginInfoKey, | ||||
|         HiveSavedLoginInfo( | ||||
|           email: "", | ||||
|           password: "", | ||||
|           serverUrl: serverUrl, | ||||
|           accessToken: accessToken, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Register device info | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/oauth.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| @@ -63,8 +61,7 @@ class LoginForm extends HookConsumerWidget { | ||||
|  | ||||
|       try { | ||||
|         isLoadingServer.value = true; | ||||
|         final endpoint = | ||||
|             await apiService.resolveAndSetEndpoint(serverUrl); | ||||
|         final endpoint = await apiService.resolveAndSetEndpoint(serverUrl); | ||||
|  | ||||
|         final loginConfig = await apiService.oAuthApi.generateConfig( | ||||
|           OAuthConfigDto(redirectUri: serverUrl), | ||||
| @@ -104,15 +101,10 @@ class LoginForm extends HookConsumerWidget { | ||||
|  | ||||
|     useEffect( | ||||
|       () { | ||||
|         var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) | ||||
|             .get(savedLoginInfoKey); | ||||
|  | ||||
|         if (loginInfo != null) { | ||||
|           usernameController.text = loginInfo.email; | ||||
|           passwordController.text = loginInfo.password; | ||||
|           serverEndpointController.text = loginInfo.serverUrl; | ||||
|         final serverUrl = Store.tryGet(StoreKey.serverUrl); | ||||
|         if (serverUrl != null) { | ||||
|           serverEndpointController.text = serverUrl; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|       }, | ||||
|       [], | ||||
| @@ -133,11 +125,11 @@ class LoginForm extends HookConsumerWidget { | ||||
|  | ||||
|       try { | ||||
|         final isAuthenticated = | ||||
|           await ref.read(authenticationProvider.notifier).login( | ||||
|             usernameController.text, | ||||
|             passwordController.text, | ||||
|             serverEndpointController.text.trim(), | ||||
|           ); | ||||
|             await ref.read(authenticationProvider.notifier).login( | ||||
|                   usernameController.text, | ||||
|                   passwordController.text, | ||||
|                   serverEndpointController.text.trim(), | ||||
|                 ); | ||||
|         if (isAuthenticated) { | ||||
|           // Resume backup (if enable) then navigate | ||||
|           if (ref.read(authenticationProvider).shouldChangePassword && | ||||
| @@ -283,61 +275,61 @@ class LoginForm extends HookConsumerWidget { | ||||
|               onSubmit: login, | ||||
|             ), | ||||
|  | ||||
|           // Note: This used to have an AnimatedSwitcher, but was removed | ||||
|           // because of https://github.com/flutter/flutter/issues/120874 | ||||
|           isLoading.value | ||||
|               ? const Padding( | ||||
|                   padding: EdgeInsets.only(top: 18.0), | ||||
|                   child: SizedBox( | ||||
|                     width: 24, | ||||
|                     height: 24, | ||||
|                     child: FittedBox( | ||||
|                       child: CircularProgressIndicator( | ||||
|                         strokeWidth: 2, | ||||
|             // Note: This used to have an AnimatedSwitcher, but was removed | ||||
|             // because of https://github.com/flutter/flutter/issues/120874 | ||||
|             isLoading.value | ||||
|                 ? const Padding( | ||||
|                     padding: EdgeInsets.only(top: 18.0), | ||||
|                     child: SizedBox( | ||||
|                       width: 24, | ||||
|                       height: 24, | ||||
|                       child: FittedBox( | ||||
|                         child: CircularProgressIndicator( | ||||
|                           strokeWidth: 2, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ) | ||||
|               : Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     const SizedBox(height: 18), | ||||
|                     LoginButton(onPressed: login), | ||||
|                     if (isOauthEnable.value) ...[ | ||||
|                       Padding( | ||||
|                         padding: const EdgeInsets.symmetric( | ||||
|                           horizontal: 16.0, | ||||
|                   ) | ||||
|                 : Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                     mainAxisAlignment: MainAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       const SizedBox(height: 18), | ||||
|                       LoginButton(onPressed: login), | ||||
|                       if (isOauthEnable.value) ...[ | ||||
|                         Padding( | ||||
|                           padding: const EdgeInsets.symmetric( | ||||
|                             horizontal: 16.0, | ||||
|                           ), | ||||
|                           child: Divider( | ||||
|                             color: | ||||
|                                 Brightness.dark == Theme.of(context).brightness | ||||
|                                     ? Colors.white | ||||
|                                     : Colors.black, | ||||
|                           ), | ||||
|                         ), | ||||
|                         child: Divider( | ||||
|                           color: | ||||
|                               Brightness.dark == Theme.of(context).brightness | ||||
|                                   ? Colors.white | ||||
|                                   : Colors.black, | ||||
|                         OAuthLoginButton( | ||||
|                           serverEndpointController: serverEndpointController, | ||||
|                           buttonLabel: oAuthButtonLabel.value, | ||||
|                           isLoading: isLoading, | ||||
|                           onPressed: oAuthLogin, | ||||
|                         ), | ||||
|                       ), | ||||
|                       OAuthLoginButton( | ||||
|                         serverEndpointController: serverEndpointController, | ||||
|                         buttonLabel: oAuthButtonLabel.value, | ||||
|                         isLoading: isLoading, | ||||
|                         onPressed: oAuthLogin, | ||||
|                       ), | ||||
|                       ], | ||||
|                     ], | ||||
|                   ], | ||||
|                 ), | ||||
|               const SizedBox(height: 12), | ||||
|               TextButton.icon( | ||||
|                 icon: const Icon(Icons.arrow_back), | ||||
|                 onPressed: () => serverEndpoint.value = null, | ||||
|                 label: const Text('Back'), | ||||
|               ), | ||||
|                   ), | ||||
|             const SizedBox(height: 12), | ||||
|             TextButton.icon( | ||||
|               icon: const Icon(Icons.arrow_back), | ||||
|               onPressed: () => serverEndpoint.value = null, | ||||
|               label: const Text('Back'), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|     final serverSelectionOrLogin = serverEndpoint.value == null | ||||
|       ? buildSelectServer() | ||||
|       : buildLogin(); | ||||
|  | ||||
|     final serverSelectionOrLogin = | ||||
|         serverEndpoint.value == null ? buildSelectServer() : buildLogin(); | ||||
|  | ||||
|     return LayoutBuilder( | ||||
|       builder: (context, constraints) { | ||||
| @@ -545,7 +537,6 @@ class OAuthLoginButton extends ConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|  | ||||
|     return ElevatedButton.icon( | ||||
|       style: ElevatedButton.styleFrom( | ||||
|         backgroundColor: Theme.of(context).primaryColor.withAlpha(230), | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
|  | ||||
| class ThumbnailWithInfo extends StatelessWidget { | ||||
|   const ThumbnailWithInfo({ | ||||
| @@ -19,7 +18,6 @@ class ThumbnailWithInfo extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     var isDarkMode = Theme.of(context).brightness == Brightness.dark; | ||||
|     var textAndIconColor = isDarkMode ? Colors.grey[100] : Colors.grey[700]; | ||||
|     return GestureDetector( | ||||
| @@ -51,7 +49,8 @@ class ThumbnailWithInfo extends StatelessWidget { | ||||
|                           fit: BoxFit.cover, | ||||
|                           imageUrl: imageUrl!, | ||||
|                           httpHeaders: { | ||||
|                             "Authorization": "Bearer ${box.get(accessTokenKey)}" | ||||
|                             "Authorization": | ||||
|                                 "Bearer ${Store.get(StoreKey.accessToken)}" | ||||
|                           }, | ||||
|                           errorWidget: (context, url, error) => | ||||
|                               const Icon(Icons.image_not_supported_outlined), | ||||
|   | ||||
| @@ -1,15 +1,14 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/search/ui/search_bar.dart'; | ||||
| import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; | ||||
| import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; | ||||
| import 'package:immich_mobile/utils/capitalize_first_letter.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| @@ -22,7 +21,6 @@ class SearchPage extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     var box = Hive.box(userInfoBox); | ||||
|     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; | ||||
|     AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation = | ||||
|         ref.watch(getCuratedLocationProvider); | ||||
| @@ -64,7 +62,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     itemBuilder: ((context, index) { | ||||
|                       var locationInfo = curatedLocations[index]; | ||||
|                       var thumbnailRequestUrl = | ||||
|                           '${box.get(serverEndpointKey)}/asset/thumbnail/${locationInfo.id}'; | ||||
|                           '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${locationInfo.id}'; | ||||
|                       return ThumbnailWithInfo( | ||||
|                         imageUrl: thumbnailRequestUrl, | ||||
|                         textInfo: locationInfo.city, | ||||
| @@ -113,7 +111,7 @@ class SearchPage extends HookConsumerWidget { | ||||
|                     itemBuilder: ((context, index) { | ||||
|                       var curatedObjectInfo = objects[index]; | ||||
|                       var thumbnailRequestUrl = | ||||
|                           '${box.get(serverEndpointKey)}/asset/thumbnail/${curatedObjectInfo.id}'; | ||||
|                           '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${curatedObjectInfo.id}'; | ||||
|  | ||||
|                       return ThumbnailWithInfo( | ||||
|                         imageUrl: thumbnailRequestUrl, | ||||
|   | ||||
| @@ -1,59 +1,63 @@ | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
|  | ||||
| enum AppSettingsEnum<T> { | ||||
|   loadPreview<bool>("loadPreview", true), | ||||
|   loadOriginal<bool>("loadOriginal", false), | ||||
|   themeMode<String>("themeMode", "system"), // "light","dark","system" | ||||
|   tilesPerRow<int>("tilesPerRow", 4), | ||||
|   dynamicLayout<bool>("dynamicLayout", false), | ||||
|   groupAssetsBy<int>("groupBy", 0), | ||||
|   loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true), | ||||
|   loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false), | ||||
|   themeMode<String>( | ||||
|     StoreKey.themeMode, | ||||
|     "themeMode", | ||||
|     "system", | ||||
|   ), // "light","dark","system" | ||||
|   tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4), | ||||
|   dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false), | ||||
|   groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0), | ||||
|   uploadErrorNotificationGracePeriod<int>( | ||||
|     StoreKey.uploadErrorNotificationGracePeriod, | ||||
|     "uploadErrorNotificationGracePeriod", | ||||
|     2, | ||||
|   ), | ||||
|   backgroundBackupTotalProgress<bool>("backgroundBackupTotalProgress", true), | ||||
|   backgroundBackupSingleProgress<bool>("backgroundBackupSingleProgress", false), | ||||
|   storageIndicator<bool>("storageIndicator", true), | ||||
|   thumbnailCacheSize<int>("thumbnailCacheSize", 10000), | ||||
|   imageCacheSize<int>("imageCacheSize", 350), | ||||
|   albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200), | ||||
|   useExperimentalAssetGrid<bool>("useExperimentalAssetGrid", false), | ||||
|   selectedAlbumSortOrder<int>("selectedAlbumSortOrder", 0); | ||||
|   backgroundBackupTotalProgress<bool>( | ||||
|     StoreKey.backgroundBackupTotalProgress, | ||||
|     "backgroundBackupTotalProgress", | ||||
|     true, | ||||
|   ), | ||||
|   backgroundBackupSingleProgress<bool>( | ||||
|     StoreKey.backgroundBackupSingleProgress, | ||||
|     "backgroundBackupSingleProgress", | ||||
|     false, | ||||
|   ), | ||||
|   storageIndicator<bool>(StoreKey.storageIndicator, "storageIndicator", true), | ||||
|   thumbnailCacheSize<int>( | ||||
|     StoreKey.thumbnailCacheSize, | ||||
|     "thumbnailCacheSize", | ||||
|     10000, | ||||
|   ), | ||||
|   imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350), | ||||
|   albumThumbnailCacheSize<int>( | ||||
|     StoreKey.albumThumbnailCacheSize, | ||||
|     "albumThumbnailCacheSize", | ||||
|     200, | ||||
|   ), | ||||
|   selectedAlbumSortOrder<int>( | ||||
|     StoreKey.selectedAlbumSortOrder, | ||||
|     "selectedAlbumSortOrder", | ||||
|     0, | ||||
|   ), | ||||
|   ; | ||||
|  | ||||
|   const AppSettingsEnum(this.hiveKey, this.defaultValue); | ||||
|   const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); | ||||
|  | ||||
|   final StoreKey<T> storeKey; | ||||
|   final String hiveKey; | ||||
|   final T defaultValue; | ||||
| } | ||||
|  | ||||
| class AppSettingsService { | ||||
|   late final Box hiveBox; | ||||
|  | ||||
|   AppSettingsService() { | ||||
|     hiveBox = Hive.box(userSettingInfoBox); | ||||
|   T getSetting<T>(AppSettingsEnum<T> setting) { | ||||
|     return Store.get(setting.storeKey, setting.defaultValue); | ||||
|   } | ||||
|  | ||||
|   T getSetting<T>(AppSettingsEnum<T> settingType) { | ||||
|     if (!hiveBox.containsKey(settingType.hiveKey)) { | ||||
|       return _setDefault(settingType); | ||||
|     } | ||||
|  | ||||
|     var result = hiveBox.get(settingType.hiveKey); | ||||
|  | ||||
|     if (result is! T) { | ||||
|       return _setDefault(settingType); | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   setSetting<T>(AppSettingsEnum<T> settingType, T value) { | ||||
|     hiveBox.put(settingType.hiveKey, value); | ||||
|   } | ||||
|  | ||||
|   T _setDefault<T>(AppSettingsEnum<T> settingType) { | ||||
|     hiveBox.put(settingType.hiveKey, settingType.defaultValue); | ||||
|     return settingType.defaultValue; | ||||
|   void setSetting<T>(AppSettingsEnum<T> setting, T value) { | ||||
|     Store.put(setting.storeKey, value); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,7 +13,6 @@ class AuthGuard extends AutoRouteGuard { | ||||
|   void onNavigation(NavigationResolver resolver, StackRouter router) async { | ||||
|     try { | ||||
|       var res = await _apiService.authenticationApi.validateAccessToken(); | ||||
|  | ||||
|       if (res != null && res.authStatus) { | ||||
|         resolver.next(true); | ||||
|       } else { | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'package:immich_mobile/shared/models/exif_info.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/utils/hash.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| @@ -40,7 +39,7 @@ class Asset { | ||||
|         width = local.width, | ||||
|         fileName = local.title!, | ||||
|         deviceId = Store.get(StoreKey.deviceIdHash), | ||||
|         ownerId = Store.get<User>(StoreKey.currentUser)!.isarId, | ||||
|         ownerId = Store.get(StoreKey.currentUser).isarId, | ||||
|         fileModifiedAt = local.modifiedDateTime.toUtc(), | ||||
|         updatedAt = local.modifiedDateTime.toUtc(), | ||||
|         isFavorite = local.isFavorite, | ||||
|   | ||||
							
								
								
									
										48
									
								
								mobile/lib/shared/models/logger_message.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								mobile/lib/shared/models/logger_message.model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| // ignore_for_file: constant_identifier_names | ||||
|  | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
|  | ||||
| part 'logger_message.model.g.dart'; | ||||
|  | ||||
| @Collection(inheritance: false) | ||||
| class LoggerMessage { | ||||
|   Id id = Isar.autoIncrement; | ||||
|   String message; | ||||
|   @Enumerated(EnumType.ordinal) | ||||
|   LogLevel level = LogLevel.INFO; | ||||
|   DateTime createdAt; | ||||
|   String? context1; | ||||
|   String? context2; | ||||
|  | ||||
|   LoggerMessage({ | ||||
|     required this.message, | ||||
|     required this.level, | ||||
|     required this.createdAt, | ||||
|     required this.context1, | ||||
|     required this.context2, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'InAppLoggerMessage(message: $message, level: $level, createdAt: $createdAt)'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Log levels according to dart logging [Level] | ||||
| enum LogLevel { | ||||
|   ALL, | ||||
|   FINEST, | ||||
|   FINER, | ||||
|   FINE, | ||||
|   CONFIG, | ||||
|   INFO, | ||||
|   WARNING, | ||||
|   SEVERE, | ||||
|   SHOUT, | ||||
|   OFF, | ||||
| } | ||||
|  | ||||
| extension LevelExtension on Level { | ||||
|   LogLevel toLogLevel() => LogLevel.values[Level.LEVELS.indexOf(this)]; | ||||
| } | ||||
							
								
								
									
										1092
									
								
								mobile/lib/shared/models/logger_message.model.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1092
									
								
								mobile/lib/shared/models/logger_message.model.g.dart
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,7 +1,6 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'dart:convert'; | ||||
|  | ||||
| part 'store.g.dart'; | ||||
|  | ||||
| @@ -26,12 +25,21 @@ class Store { | ||||
|     return _db.writeTxn(() => _db.storeValues.clear()); | ||||
|   } | ||||
|  | ||||
|   /// Returns the stored value for the given key, or the default value if null | ||||
|   static T? get<T>(StoreKey key, [T? defaultValue]) => | ||||
|       _cache[key.id] ?? defaultValue; | ||||
|   /// Returns the stored value for the given key or if null the [defaultValue] | ||||
|   /// Throws a [StoreKeyNotFoundException] if both are null | ||||
|   static T get<T>(StoreKey<T> key, [T? defaultValue]) { | ||||
|     final value = _cache[key.id] ?? defaultValue; | ||||
|     if (value == null) { | ||||
|       throw StoreKeyNotFoundException(key); | ||||
|     } | ||||
|     return value; | ||||
|   } | ||||
|  | ||||
|   /// Returns the stored value for the given key (possibly null) | ||||
|   static T? tryGet<T>(StoreKey<T> key) => _cache[key.id]; | ||||
|  | ||||
|   /// Stores the value synchronously in the cache and asynchronously in the DB | ||||
|   static Future<void> put<T>(StoreKey key, T value) { | ||||
|   static Future<void> put<T>(StoreKey<T> key, T value) { | ||||
|     _cache[key.id] = value; | ||||
|     return _db.writeTxn( | ||||
|       () async => _db.storeValues.put(await StoreValue._of(value, key)), | ||||
| @@ -39,7 +47,7 @@ class Store { | ||||
|   } | ||||
|  | ||||
|   /// Removes the value synchronously from the cache and asynchronously from the DB | ||||
|   static Future<void> delete(StoreKey key) { | ||||
|   static Future<void> delete<T>(StoreKey<T> key) { | ||||
|     _cache[key.id] = null; | ||||
|     return _db.writeTxn(() => _db.storeValues.delete(key.id)); | ||||
|   } | ||||
| @@ -58,7 +66,8 @@ class Store { | ||||
|   static void _onChangeListener(List<StoreValue>? data) { | ||||
|     if (data != null) { | ||||
|       for (StoreValue value in data) { | ||||
|         _cache[value.id] = value._extract(StoreKey.values[value.id]); | ||||
|         _cache[value.id] = | ||||
|             value._extract(StoreKey.values.firstWhere((e) => e.id == value.id)); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -72,76 +81,113 @@ class StoreValue { | ||||
|   int? intValue; | ||||
|   String? strValue; | ||||
|  | ||||
|   dynamic _extract(StoreKey key) { | ||||
|   T? _extract<T>(StoreKey<T> key) { | ||||
|     switch (key.type) { | ||||
|       case int: | ||||
|         return key.fromDb == null | ||||
|             ? intValue | ||||
|             : key.fromDb!.call(Store._db, intValue!); | ||||
|         return intValue as T?; | ||||
|       case bool: | ||||
|         return intValue == null ? null : intValue! == 1; | ||||
|         return intValue == null ? null : (intValue! == 1) as T; | ||||
|       case DateTime: | ||||
|         return intValue == null | ||||
|             ? null | ||||
|             : DateTime.fromMicrosecondsSinceEpoch(intValue!); | ||||
|             : DateTime.fromMicrosecondsSinceEpoch(intValue!) as T; | ||||
|       case String: | ||||
|         return key.fromJson != null | ||||
|             ? key.fromJson!.call(json.decode(strValue!)) | ||||
|             : strValue; | ||||
|         return strValue as T?; | ||||
|       default: | ||||
|         if (key.fromDb != null) { | ||||
|           return key.fromDb!.call(Store._db, intValue!); | ||||
|         } | ||||
|     } | ||||
|     throw TypeError(); | ||||
|   } | ||||
|  | ||||
|   static Future<StoreValue> _of(dynamic value, StoreKey key) async { | ||||
|   static Future<StoreValue> _of<T>(T? value, StoreKey<T> key) async { | ||||
|     int? i; | ||||
|     String? s; | ||||
|     switch (key.type) { | ||||
|       case int: | ||||
|         i = (key.toDb == null ? value : await key.toDb!.call(Store._db, value)); | ||||
|         i = value as int?; | ||||
|         break; | ||||
|       case bool: | ||||
|         i = value == null ? null : (value ? 1 : 0); | ||||
|         i = value == null ? null : (value == true ? 1 : 0); | ||||
|         break; | ||||
|       case DateTime: | ||||
|         i = value == null ? null : (value as DateTime).microsecondsSinceEpoch; | ||||
|         break; | ||||
|       case String: | ||||
|         s = key.fromJson == null ? value : json.encode(value.toJson()); | ||||
|         s = value as String?; | ||||
|         break; | ||||
|       default: | ||||
|         if (key.toDb != null) { | ||||
|           i = await key.toDb!.call(Store._db, value); | ||||
|           break; | ||||
|         } | ||||
|         throw TypeError(); | ||||
|     } | ||||
|     return StoreValue(key.id, intValue: i, strValue: s); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class StoreKeyNotFoundException implements Exception { | ||||
|   final StoreKey key; | ||||
|   StoreKeyNotFoundException(this.key); | ||||
|   @override | ||||
|   String toString() => "Key '${key.name}' not found in Store"; | ||||
| } | ||||
|  | ||||
| /// Key for each possible value in the `Store`. | ||||
| /// Defines the data type (int, String, JSON) for each value | ||||
| enum StoreKey { | ||||
|   userRemoteId(0), | ||||
|   assetETag(1), | ||||
|   currentUser(2, type: int, fromDb: _getUser, toDb: _toUser), | ||||
|   deviceIdHash(3, type: int), | ||||
|   deviceId(4), | ||||
|   backupFailedSince(5, type: DateTime), | ||||
|   backupRequireWifi(6, type: bool), | ||||
|   backupRequireCharging(7, type: bool), | ||||
|   backupTriggerDelay(8, type: int); | ||||
| /// Defines the data type for each value | ||||
| enum StoreKey<T> { | ||||
|   userRemoteId<String>(0, type: String), | ||||
|   assetETag<String>(1, type: String), | ||||
|   currentUser<User>(2, type: User, fromDb: _getUser, toDb: _toUser), | ||||
|   deviceIdHash<int>(3, type: int), | ||||
|   deviceId<String>(4, type: String), | ||||
|   backupFailedSince<DateTime>(5, type: DateTime), | ||||
|   backupRequireWifi<bool>(6, type: bool), | ||||
|   backupRequireCharging<bool>(7, type: bool), | ||||
|   backupTriggerDelay<int>(8, type: int), | ||||
|   githubReleaseInfo<String>(9, type: String), | ||||
|   serverUrl<String>(10, type: String), | ||||
|   accessToken<String>(11, type: String), | ||||
|   serverEndpoint<String>(12, type: String), | ||||
|   // user settings from [AppSettingsEnum] below: | ||||
|   loadPreview<bool>(100, type: bool), | ||||
|   loadOriginal<bool>(101, type: bool), | ||||
|   themeMode<String>(102, type: String), | ||||
|   tilesPerRow<int>(103, type: int), | ||||
|   dynamicLayout<bool>(104, type: bool), | ||||
|   groupAssetsBy<int>(105, type: int), | ||||
|   uploadErrorNotificationGracePeriod<int>(106, type: int), | ||||
|   backgroundBackupTotalProgress<bool>(107, type: bool), | ||||
|   backgroundBackupSingleProgress<bool>(108, type: bool), | ||||
|   storageIndicator<bool>(109, type: bool), | ||||
|   thumbnailCacheSize<int>(110, type: int), | ||||
|   imageCacheSize<int>(111, type: int), | ||||
|   albumThumbnailCacheSize<int>(112, type: int), | ||||
|   selectedAlbumSortOrder<int>(113, type: int), | ||||
|   ; | ||||
|  | ||||
|   const StoreKey( | ||||
|     this.id, { | ||||
|     this.type = String, | ||||
|     required this.type, | ||||
|     this.fromDb, | ||||
|     this.toDb, | ||||
|     // ignore: unused_element | ||||
|     this.fromJson, | ||||
|   }); | ||||
|   final int id; | ||||
|   final Type type; | ||||
|   final dynamic Function(Isar, int)? fromDb; | ||||
|   final Future<int> Function(Isar, dynamic)? toDb; | ||||
|   final Function(dynamic)? fromJson; | ||||
|   final T? Function<T>(Isar, int)? fromDb; | ||||
|   final Future<int> Function<T>(Isar, T)? toDb; | ||||
| } | ||||
|  | ||||
| User? _getUser(Isar db, int i) => db.users.getSync(i); | ||||
| Future<int> _toUser(Isar db, dynamic u) { | ||||
|   User user = (u as User); | ||||
|   return db.users.put(user); | ||||
| T? _getUser<T>(Isar db, int i) { | ||||
|   final User? u = db.users.getSync(i); | ||||
|   return u as T?; | ||||
| } | ||||
|  | ||||
| Future<int> _toUser<T>(Isar db, T u) { | ||||
|   if (u is User) { | ||||
|     return db.users.put(u); | ||||
|   } | ||||
|   throw TypeError(); | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
|  | ||||
| @@ -13,10 +12,10 @@ class ReleaseInfoNotifier extends StateNotifier<String> { | ||||
|   final log = Logger('ReleaseInfoNotifier'); | ||||
|   void checkGithubReleaseInfo() async { | ||||
|     final Client client = Client(); | ||||
|     var box = Hive.box(hiveGithubReleaseInfoBox); | ||||
|  | ||||
|     try { | ||||
|       String? localReleaseVersion = box.get(githubReleaseInfoKey); | ||||
|       final String? localReleaseVersion = | ||||
|           Store.tryGet(StoreKey.githubReleaseInfo); | ||||
|       final res = await client.get( | ||||
|         Uri.parse( | ||||
|           "https://api.github.com/repos/immich-app/immich/releases/latest", | ||||
| @@ -48,9 +47,7 @@ class ReleaseInfoNotifier extends StateNotifier<String> { | ||||
|   } | ||||
|  | ||||
|   void acknowledgeNewVersion() { | ||||
|     var box = Hive.box(hiveGithubReleaseInfoBox); | ||||
|  | ||||
|     box.put(githubReleaseInfoKey, state); | ||||
|     Store.put(StoreKey.githubReleaseInfo, state); | ||||
|     VersionAnnouncementOverlayController.appLoader.hide(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/foundation.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/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/providers/asset.provider.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| @@ -58,9 +57,9 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> { | ||||
|     var authenticationState = ref.read(authenticationProvider); | ||||
|  | ||||
|     if (authenticationState.isAuthenticated) { | ||||
|       var accessToken = Hive.box(userInfoBox).get(accessTokenKey); | ||||
|       final accessToken = Store.get(StoreKey.accessToken); | ||||
|       try { | ||||
|         var endpoint = Uri.parse(Hive.box(userInfoBox).get(serverEndpointKey)); | ||||
|         final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint)); | ||||
|  | ||||
|         debugPrint("Attempting to connect to websocket"); | ||||
|         // Configure socket transports must be specified | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/utils/url_helper.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| @@ -19,13 +18,9 @@ class ApiService { | ||||
|   late DeviceInfoApi deviceInfoApi; | ||||
|  | ||||
|   ApiService() { | ||||
|     if (Hive.isBoxOpen(userInfoBox)) { | ||||
|       final endpoint = Hive.box(userInfoBox).get(serverEndpointKey) as String?; | ||||
|       if (endpoint != null && endpoint.isNotEmpty) { | ||||
|         setEndpoint(endpoint); | ||||
|       } | ||||
|     } else { | ||||
|       debugPrint("Cannot init ApiServer endpoint, userInfoBox not open yet."); | ||||
|     final endpoint = Store.tryGet(StoreKey.serverEndpoint); | ||||
|     if (endpoint != null && endpoint.isNotEmpty) { | ||||
|       setEndpoint(endpoint); | ||||
|     } | ||||
|   } | ||||
|   String? _authToken; | ||||
| @@ -49,7 +44,7 @@ class ApiService { | ||||
|     setEndpoint(endpoint); | ||||
|  | ||||
|     // Save in hivebox for next startup | ||||
|     Hive.box(userInfoBox).put(serverEndpointKey, endpoint); | ||||
|     Store.put(StoreKey.serverEndpoint, endpoint); | ||||
|     return endpoint; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/exif_info.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/db.provider.dart'; | ||||
| import 'package:immich_mobile/shared/services/api.service.dart'; | ||||
| @@ -44,7 +43,7 @@ class AssetService { | ||||
|         .where() | ||||
|         .remoteIdIsNotNull() | ||||
|         .filter() | ||||
|         .ownerIdEqualTo(Store.get<User>(StoreKey.currentUser)!.isarId) | ||||
|         .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) | ||||
|         .count(); | ||||
|     final List<AssetResponseDto>? dtos = | ||||
|         await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0); | ||||
| @@ -63,7 +62,7 @@ class AssetService { | ||||
|     required bool hasCache, | ||||
|   }) async { | ||||
|     try { | ||||
|       final etag = hasCache ? Store.get(StoreKey.assetETag) : null; | ||||
|       final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null; | ||||
|       final Pair<List<AssetResponseDto>, String?>? remote = | ||||
|           await _apiService.assetApi.getAllAssetsWithETag(eTag: etag); | ||||
|       if (remote == null) { | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/logger_message.model.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:share_plus/share_plus.dart'; | ||||
|  | ||||
| /// [ImmichLogger] is a custom logger that is built on top of the [logging] package. | ||||
| /// The logs are written to a Hive box and onto console, using `debugPrint` method. | ||||
| /// The logs are written to the database and onto console, using `debugPrint` method. | ||||
| /// | ||||
| /// The logs are deleted when exceeding the `maxLogEntries` (default 200) property | ||||
| /// in the class. | ||||
| @@ -17,48 +17,61 @@ import 'package:share_plus/share_plus.dart'; | ||||
| /// Logs can be shared by calling the `shareLogs` method, which will open a share dialog | ||||
| /// and generate a csv file. | ||||
| class ImmichLogger { | ||||
|   static final ImmichLogger _instance = ImmichLogger._internal(); | ||||
|   final maxLogEntries = 200; | ||||
|   final Box<ImmichLoggerMessage> _box = Hive.box(immichLoggerBox); | ||||
|   final Isar _db = Isar.getInstance()!; | ||||
|   final List<LoggerMessage> _msgBuffer = []; | ||||
|   Timer? _timer; | ||||
|  | ||||
|   List<ImmichLoggerMessage> get messages => | ||||
|       _box.values.toList().reversed.toList(); | ||||
|   factory ImmichLogger() => _instance; | ||||
|  | ||||
|   ImmichLogger() { | ||||
|   ImmichLogger._internal() { | ||||
|     _removeOverflowMessages(); | ||||
|   } | ||||
|  | ||||
|   init() { | ||||
|     Logger.root.level = Level.INFO; | ||||
|     Logger.root.onRecord.listen(_writeLogToHiveBox); | ||||
|     Logger.root.onRecord.listen(_writeLogToDatabase); | ||||
|   } | ||||
|  | ||||
|   _removeOverflowMessages() { | ||||
|     if (_box.length > maxLogEntries) { | ||||
|       var numberOfEntryToBeDeleted = _box.length - maxLogEntries; | ||||
|       for (var i = 0; i < numberOfEntryToBeDeleted; i++) { | ||||
|         _box.deleteAt(0); | ||||
|       } | ||||
|   List<LoggerMessage> get messages { | ||||
|     final inDb = | ||||
|         _db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync(); | ||||
|     return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb; | ||||
|   } | ||||
|  | ||||
|   void _removeOverflowMessages() { | ||||
|     final msgCount = _db.loggerMessages.countSync(); | ||||
|     if (msgCount > maxLogEntries) { | ||||
|       final numberOfEntryToBeDeleted = msgCount - maxLogEntries; | ||||
|       _db.loggerMessages.where().limit(numberOfEntryToBeDeleted).deleteAll(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   _writeLogToHiveBox(LogRecord record) { | ||||
|     final Box<ImmichLoggerMessage> box = Hive.box(immichLoggerBox); | ||||
|     var formattedMessage = record.message; | ||||
|  | ||||
|   void _writeLogToDatabase(LogRecord record) { | ||||
|     debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); | ||||
|     box.add( | ||||
|       ImmichLoggerMessage( | ||||
|         message: formattedMessage, | ||||
|         level: record.level.name, | ||||
|         createdAt: record.time, | ||||
|         context1: record.loggerName, | ||||
|         context2: record.stackTrace?.toString(), | ||||
|       ), | ||||
|     final lm = LoggerMessage( | ||||
|       message: record.message, | ||||
|       level: record.level.toLogLevel(), | ||||
|       createdAt: record.time, | ||||
|       context1: record.loggerName, | ||||
|       context2: record.stackTrace?.toString(), | ||||
|     ); | ||||
|     _msgBuffer.add(lm); | ||||
|  | ||||
|     // delayed batch writing to database: increases performance when logging | ||||
|     // messages in quick succession and reduces NAND wear | ||||
|     _timer ??= Timer(const Duration(seconds: 5), _flushBufferToDatabase); | ||||
|   } | ||||
|  | ||||
|   void _flushBufferToDatabase() { | ||||
|     _timer = null; | ||||
|     _db.writeTxnSync(() => _db.loggerMessages.putAllSync(_msgBuffer)); | ||||
|     _msgBuffer.clear(); | ||||
|   } | ||||
|  | ||||
|   void clearLogs() { | ||||
|     _box.clear(); | ||||
|     _timer?.cancel(); | ||||
|     _timer = null; | ||||
|     _msgBuffer.clear(); | ||||
|     _db.writeTxn(() => _db.loggerMessages.clear()); | ||||
|   } | ||||
|  | ||||
|   Future<void> shareLogs() async { | ||||
| @@ -93,4 +106,12 @@ class ImmichLogger { | ||||
|     // Clean up temp file | ||||
|     await logFile.delete(); | ||||
|   } | ||||
|  | ||||
|   /// Flush pending log messages to persistent storage | ||||
|   void flush() { | ||||
|     if (_timer != null) { | ||||
|       _timer!.cancel(); | ||||
|       _flushBufferToDatabase(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -241,7 +241,7 @@ class SyncService { | ||||
|     } | ||||
|  | ||||
|     if (album.shared || dto.shared) { | ||||
|       final userId = Store.get<User>(StoreKey.currentUser)!.isarId; | ||||
|       final userId = Store.get(StoreKey.currentUser).isarId; | ||||
|       final foreign = | ||||
|           await album.assets.filter().not().ownerIdEqualTo(userId).findAll(); | ||||
|       existing.addAll(foreign); | ||||
|   | ||||
| @@ -42,7 +42,7 @@ class UserService { | ||||
|     if (self) { | ||||
|       return _db.users.where().findAll(); | ||||
|     } | ||||
|     final int userId = Store.get<User>(StoreKey.currentUser)!.isarId; | ||||
|     final int userId = Store.get(StoreKey.currentUser).isarId; | ||||
|     return _db.users.where().isarIdNotEqualTo(userId).findAll(); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/utils/image_url_builder.dart'; | ||||
| import 'package:photo_manager/photo_manager.dart'; | ||||
|  | ||||
| @@ -84,7 +83,7 @@ class ImmichImage extends StatelessWidget { | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|     final String? token = Hive.box(userInfoBox).get(accessTokenKey); | ||||
|     final String? token = Store.get(StoreKey.accessToken); | ||||
|     final String thumbnailRequestUrl = getThumbnailUrl(asset); | ||||
|     return CachedNetworkImage( | ||||
|       imageUrl: thumbnailRequestUrl, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/shared/models/logger_message.model.dart'; | ||||
| import 'package:immich_mobile/shared/services/immich_logger.service.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
|  | ||||
| @@ -31,29 +32,29 @@ class AppLogPage extends HookConsumerWidget { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     Widget buildLeadingIcon(String level) { | ||||
|     Widget buildLeadingIcon(LogLevel level) { | ||||
|       switch (level) { | ||||
|         case "INFO": | ||||
|         case LogLevel.INFO: | ||||
|           return colorStatusIndicator(Theme.of(context).primaryColor); | ||||
|         case "SEVERE": | ||||
|         case LogLevel.SEVERE: | ||||
|           return colorStatusIndicator(Colors.redAccent); | ||||
|  | ||||
|         case "WARNING": | ||||
|         case LogLevel.WARNING: | ||||
|           return colorStatusIndicator(Colors.orangeAccent); | ||||
|         default: | ||||
|           return colorStatusIndicator(Colors.grey); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     getTileColor(String level) { | ||||
|     getTileColor(LogLevel level) { | ||||
|       switch (level) { | ||||
|         case "INFO": | ||||
|         case LogLevel.INFO: | ||||
|           return Colors.transparent; | ||||
|         case "SEVERE": | ||||
|         case LogLevel.SEVERE: | ||||
|           return Theme.of(context).brightness == Brightness.dark | ||||
|               ? Colors.redAccent.withOpacity(0.25) | ||||
|               : Colors.redAccent.withOpacity(0.075); | ||||
|         case "WARNING": | ||||
|         case LogLevel.WARNING: | ||||
|           return Theme.of(context).brightness == Brightness.dark | ||||
|               ? Colors.orangeAccent.withOpacity(0.25) | ||||
|               : Colors.orangeAccent.withOpacity(0.075); | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart' hide Store; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||
|  | ||||
| class SplashScreenPage extends HookConsumerWidget { | ||||
| @@ -17,23 +15,23 @@ class SplashScreenPage extends HookConsumerWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final apiService = ref.watch(apiServiceProvider); | ||||
|     HiveSavedLoginInfo? loginInfo = | ||||
|         Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey); | ||||
|     final serverUrl = Store.tryGet(StoreKey.serverUrl); | ||||
|     final accessToken = Store.tryGet(StoreKey.accessToken); | ||||
|  | ||||
|     void performLoggingIn() async { | ||||
|       bool isSuccess = false; | ||||
|       if (loginInfo != null) { | ||||
|       if (accessToken != null && serverUrl != null) { | ||||
|         try { | ||||
|           // Resolve API server endpoint from user provided serverUrl | ||||
|           await apiService.resolveAndSetEndpoint(loginInfo.serverUrl); | ||||
|           await apiService.resolveAndSetEndpoint(serverUrl); | ||||
|         } catch (e) { | ||||
|           // okay, try to continue anyway if offline | ||||
|         } | ||||
|  | ||||
|         isSuccess = | ||||
|             await ref.read(authenticationProvider.notifier).setSuccessLoginInfo( | ||||
|                   accessToken: loginInfo.accessToken, | ||||
|                   serverUrl: loginInfo.serverUrl, | ||||
|                   accessToken: accessToken, | ||||
|                   serverUrl: serverUrl, | ||||
|                 ); | ||||
|       } | ||||
|       if (isSuccess) { | ||||
| @@ -51,7 +49,7 @@ class SplashScreenPage extends HookConsumerWidget { | ||||
|  | ||||
|     useEffect( | ||||
|       () { | ||||
|         if (loginInfo != null) { | ||||
|         if (serverUrl != null && accessToken != null) { | ||||
|           performLoggingIn(); | ||||
|         } else { | ||||
|           AutoRouter.of(context).replace(const LoginRoute()); | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:openapi/api.dart'; | ||||
|  | ||||
| import '../constants/hive_box.dart'; | ||||
|  | ||||
| String getThumbnailUrl( | ||||
|   final Asset asset, { | ||||
|   ThumbnailFormat type = ThumbnailFormat.WEBP, | ||||
| @@ -48,8 +46,7 @@ String getAlbumThumbNailCacheKey( | ||||
| } | ||||
|  | ||||
| String getImageUrl(final Asset asset) { | ||||
|   final box = Hive.box(userInfoBox); | ||||
|   return '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}?isThumb=false'; | ||||
|   return '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}?isThumb=false'; | ||||
| } | ||||
|  | ||||
| String getImageCacheKey(final Asset asset) { | ||||
| @@ -60,7 +57,5 @@ String _getThumbnailUrl( | ||||
|   final String id, { | ||||
|   ThumbnailFormat type = ThumbnailFormat.WEBP, | ||||
| }) { | ||||
|   final box = Hive.box(userInfoBox); | ||||
|  | ||||
|   return '${box.get(serverEndpointKey)}/asset/thumbnail/$id?format=${type.value}'; | ||||
|   return '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$id?format=${type.value}'; | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| // ignore_for_file: deprecated_member_use_from_same_package | ||||
|  | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:flutter/cupertino.dart'; | ||||
| import 'package:hive/hive.dart'; | ||||
| import 'package:immich_mobile/constants/hive_box.dart'; | ||||
| @@ -8,6 +10,9 @@ import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; | ||||
| import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; | ||||
| import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; | ||||
| import 'package:immich_mobile/shared/models/store.dart'; | ||||
| import 'package:immich_mobile/shared/services/asset_cache.service.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
| @@ -23,11 +28,37 @@ Future<void> migrateHiveToStoreIfNecessary() async { | ||||
|     duplicatedAssetsBox, | ||||
|     _migrateDuplicatedAssetsBox, | ||||
|   ); | ||||
|   await _migrateHiveBoxIfNecessary( | ||||
|     hiveGithubReleaseInfoBox, | ||||
|     _migrateReleaseInfoBox, | ||||
|   ); | ||||
|  | ||||
|   await _migrateHiveBoxIfNecessary(hiveLoginInfoBox, _migrateLoginInfoBox); | ||||
|   await _migrateHiveBoxIfNecessary( | ||||
|     immichLoggerBox, | ||||
|     (Box<ImmichLoggerMessage> box) => box.deleteFromDisk(), | ||||
|   ); | ||||
|   await _migrateHiveBoxIfNecessary(userSettingInfoBox, _migrateAppSettingsBox); | ||||
| } | ||||
|  | ||||
| FutureOr<void> _migrateReleaseInfoBox(Box box) => | ||||
|     _migrateKey(box, githubReleaseInfoKey, StoreKey.githubReleaseInfo); | ||||
|  | ||||
| Future<void> _migrateLoginInfoBox(Box<HiveSavedLoginInfo> box) async { | ||||
|   final HiveSavedLoginInfo? info = box.get(savedLoginInfoKey); | ||||
|   if (info != null) { | ||||
|     await Store.put(StoreKey.serverUrl, info.serverUrl); | ||||
|     await Store.put(StoreKey.accessToken, info.accessToken); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> _migrateHiveUserInfoBox(Box box) async { | ||||
|   await _migrateKey(box, userIdKey, StoreKey.userRemoteId); | ||||
|   await _migrateKey(box, assetEtagKey, StoreKey.assetETag); | ||||
|   if (Store.tryGet(StoreKey.deviceId) == null) { | ||||
|     await _migrateKey(box, deviceIdKey, StoreKey.deviceId); | ||||
|   } | ||||
|   await _migrateKey(box, serverEndpointKey, StoreKey.serverEndpoint); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async { | ||||
| @@ -35,16 +66,15 @@ Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async { | ||||
|   await _migrateKey(box, backupRequireWifi, StoreKey.backupRequireWifi); | ||||
|   await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging); | ||||
|   await _migrateKey(box, backupTriggerDelay, StoreKey.backupTriggerDelay); | ||||
|   return box.deleteFromDisk(); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async { | ||||
|   final Isar? db = Isar.getInstance(); | ||||
|   if (db == null) { | ||||
|     throw Exception("_migrateBackupInfoBox could not load database"); | ||||
|   } | ||||
| FutureOr<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) { | ||||
|   final HiveBackupAlbums? infos = box.get(backupInfoKey); | ||||
|   if (infos != null) { | ||||
|     final Isar? db = Isar.getInstance(); | ||||
|     if (db == null) { | ||||
|       throw Exception("_migrateBackupInfoBox could not load database"); | ||||
|     } | ||||
|     List<BackupAlbum> albums = []; | ||||
|     for (int i = 0; i < infos.selectedAlbumIds.length; i++) { | ||||
|       final album = BackupAlbum( | ||||
| @@ -62,48 +92,49 @@ Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async { | ||||
|       ); | ||||
|       albums.add(album); | ||||
|     } | ||||
|     await db.writeTxn(() => db.backupAlbums.putAll(albums)); | ||||
|   } else { | ||||
|     debugPrint("_migrateBackupInfoBox deletes empty box"); | ||||
|     return db.writeTxn(() => db.backupAlbums.putAll(albums)); | ||||
|   } | ||||
|   return box.deleteFromDisk(); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) async { | ||||
|   final Isar? db = Isar.getInstance(); | ||||
|   if (db == null) { | ||||
|     throw Exception("_migrateBackupInfoBox could not load database"); | ||||
|   } | ||||
| FutureOr<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) { | ||||
|   final HiveDuplicatedAssets? duplicatedAssets = box.get(duplicatedAssetsKey); | ||||
|   if (duplicatedAssets != null) { | ||||
|     final Isar? db = Isar.getInstance(); | ||||
|     if (db == null) { | ||||
|       throw Exception("_migrateBackupInfoBox could not load database"); | ||||
|     } | ||||
|     final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds | ||||
|         .map((id) => DuplicatedAsset(id)) | ||||
|         .toList(); | ||||
|     await db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds)); | ||||
|   } else { | ||||
|     debugPrint("_migrateDuplicatedAssetsBox deletes empty box"); | ||||
|     return db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> _migrateAppSettingsBox(Box box) async { | ||||
|   for (AppSettingsEnum s in AppSettingsEnum.values) { | ||||
|     await _migrateKey(box, s.hiveKey, s.storeKey); | ||||
|   } | ||||
|   return box.deleteFromDisk(); | ||||
| } | ||||
|  | ||||
| Future<void> _migrateHiveBoxIfNecessary<T>( | ||||
|   String boxName, | ||||
|   Future<void> Function(Box<T>) migrate, | ||||
|   FutureOr<void> Function(Box<T>) migrate, | ||||
| ) async { | ||||
|   try { | ||||
|     if (await Hive.boxExists(boxName)) { | ||||
|       await migrate(await Hive.openBox<T>(boxName)); | ||||
|       final box = await Hive.openBox<T>(boxName); | ||||
|       await migrate(box); | ||||
|       await box.deleteFromDisk(); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     debugPrint("Error while migrating $boxName $e"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| _migrateKey(Box box, String hiveKey, StoreKey key) async { | ||||
|   final String? value = box.get(hiveKey); | ||||
| FutureOr<void> _migrateKey<T>(Box box, String hiveKey, StoreKey<T> key) { | ||||
|   final T? value = box.get(hiveKey); | ||||
|   if (value != null) { | ||||
|     await Store.put(key, value); | ||||
|     await box.delete(hiveKey); | ||||
|     return Store.put(key, value); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user