diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index a8c860c908..ac0b14ef4d 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -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 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 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); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 448e3208ad..b64a48b80c 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -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 openBoxes() async { - await Future.wait([ - Hive.openBox(immichLoggerBox), - Hive.openBox(userInfoBox), - Hive.openBox(hiveLoginInfoBox), - Hive.openBox(hiveGithubReleaseInfoBox), - Hive.openBox(userSettingInfoBox), - EasyLocalization.ensureInitialized(), - ]); -} - Future 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 initApp() async { } // Initialize Immich Logger Service - ImmichLogger().init(); + ImmichLogger(); var log = Logger("ImmichErrorLogger"); @@ -108,6 +97,7 @@ Future loadDb() async { UserSchema, BackupAlbumSchema, DuplicatedAssetSchema, + LoggerMessageSchema, ], directory: dir.path, maxSizeMiB: 256, @@ -174,6 +164,7 @@ class ImmichAppState extends ConsumerState 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(); diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart index 2034546cab..4feabcf033 100644 --- a/mobile/lib/modules/album/services/album.service.dart +++ b/mobile/lib/modules/album/services/album.service.dart @@ -265,7 +265,7 @@ class AlbumService { Future deleteAlbum(Album album) async { try { - final userId = Store.get(StoreKey.currentUser)!.isarId; + final userId = Store.get(StoreKey.currentUser).isarId; if (album.owner.value?.isarId == userId) { await _apiService.albumApi.deleteAlbum(album.remoteId!); } diff --git a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart index f53ae043d0..83dae248cb 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart @@ -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), diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index ae1e502ef7..f96f1f0624 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -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 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 diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index 0dfa7ea802..d9c7f0838e 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -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, diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index a19423f781..319c2890f7 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -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 _onAssetsChanged() async { final Isar db = await loadDb(); - await Hive.initFlutter(); - Hive.registerAdapter(HiveSavedLoginInfoAdapter()); - - await Future.wait([ - Hive.openBox(userInfoBox), - Hive.openBox(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; } diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index 78b20033ac..337af7135b 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -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 { 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 { 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 { Future _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 { 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 diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index b8f0bce837..88921c3f03 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -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?> 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 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 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; diff --git a/mobile/lib/modules/home/ui/home_page_app_bar.dart b/mobile/lib/modules/home/ui/home_page_app_bar.dart index 4d37ff3264..0352001d6d 100644 --- a/mobile/lib/modules/home/ui/home_page_app_bar.dart +++ b/mobile/lib/modules/home/ui/home_page_app_bar.dart @@ -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: () { diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart index ad05bc7821..0a3e8680aa 100644 --- a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart +++ b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart @@ -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; diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index 78c3f9357b..77c083fea3 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -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 { 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(hiveLoginInfoBox).delete(savedLoginInfoKey) + Store.delete(StoreKey.accessToken), ]); state = state.copyWith(isAuthenticated: false); @@ -157,14 +153,13 @@ class AuthenticationNotifier extends StateNotifier { } 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 { deviceId: deviceInfo["deviceId"], deviceType: deviceInfo["deviceType"], ); - - // Save login info to local storage - Hive.box(hiveLoginInfoBox).put( - savedLoginInfoKey, - HiveSavedLoginInfo( - email: "", - password: "", - serverUrl: serverUrl, - accessToken: accessToken, - ), - ); } // Register device info diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index f1db78b7d8..12441be464 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -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(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), diff --git a/mobile/lib/modules/search/ui/thumbnail_with_info.dart b/mobile/lib/modules/search/ui/thumbnail_with_info.dart index de88716d11..c1b22511aa 100644 --- a/mobile/lib/modules/search/ui/thumbnail_with_info.dart +++ b/mobile/lib/modules/search/ui/thumbnail_with_info.dart @@ -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), diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index f5a1e57ee2..bb721a36cd 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -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> 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, diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart index 9990622f64..ce374ef359 100644 --- a/mobile/lib/modules/settings/services/app_settings.service.dart +++ b/mobile/lib/modules/settings/services/app_settings.service.dart @@ -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 { - loadPreview("loadPreview", true), - loadOriginal("loadOriginal", false), - themeMode("themeMode", "system"), // "light","dark","system" - tilesPerRow("tilesPerRow", 4), - dynamicLayout("dynamicLayout", false), - groupAssetsBy("groupBy", 0), + loadPreview(StoreKey.loadPreview, "loadPreview", true), + loadOriginal(StoreKey.loadOriginal, "loadOriginal", false), + themeMode( + StoreKey.themeMode, + "themeMode", + "system", + ), // "light","dark","system" + tilesPerRow(StoreKey.tilesPerRow, "tilesPerRow", 4), + dynamicLayout(StoreKey.dynamicLayout, "dynamicLayout", false), + groupAssetsBy(StoreKey.groupAssetsBy, "groupBy", 0), uploadErrorNotificationGracePeriod( + StoreKey.uploadErrorNotificationGracePeriod, "uploadErrorNotificationGracePeriod", 2, ), - backgroundBackupTotalProgress("backgroundBackupTotalProgress", true), - backgroundBackupSingleProgress("backgroundBackupSingleProgress", false), - storageIndicator("storageIndicator", true), - thumbnailCacheSize("thumbnailCacheSize", 10000), - imageCacheSize("imageCacheSize", 350), - albumThumbnailCacheSize("albumThumbnailCacheSize", 200), - useExperimentalAssetGrid("useExperimentalAssetGrid", false), - selectedAlbumSortOrder("selectedAlbumSortOrder", 0); + backgroundBackupTotalProgress( + StoreKey.backgroundBackupTotalProgress, + "backgroundBackupTotalProgress", + true, + ), + backgroundBackupSingleProgress( + StoreKey.backgroundBackupSingleProgress, + "backgroundBackupSingleProgress", + false, + ), + storageIndicator(StoreKey.storageIndicator, "storageIndicator", true), + thumbnailCacheSize( + StoreKey.thumbnailCacheSize, + "thumbnailCacheSize", + 10000, + ), + imageCacheSize(StoreKey.imageCacheSize, "imageCacheSize", 350), + albumThumbnailCacheSize( + StoreKey.albumThumbnailCacheSize, + "albumThumbnailCacheSize", + 200, + ), + selectedAlbumSortOrder( + StoreKey.selectedAlbumSortOrder, + "selectedAlbumSortOrder", + 0, + ), + ; - const AppSettingsEnum(this.hiveKey, this.defaultValue); + const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); + final StoreKey storeKey; final String hiveKey; final T defaultValue; } class AppSettingsService { - late final Box hiveBox; - - AppSettingsService() { - hiveBox = Hive.box(userSettingInfoBox); + T getSetting(AppSettingsEnum setting) { + return Store.get(setting.storeKey, setting.defaultValue); } - T getSetting(AppSettingsEnum 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(AppSettingsEnum settingType, T value) { - hiveBox.put(settingType.hiveKey, value); - } - - T _setDefault(AppSettingsEnum settingType) { - hiveBox.put(settingType.hiveKey, settingType.defaultValue); - return settingType.defaultValue; + void setSetting(AppSettingsEnum setting, T value) { + Store.put(setting.storeKey, value); } } diff --git a/mobile/lib/routing/auth_guard.dart b/mobile/lib/routing/auth_guard.dart index 0003c5336e..3fd7be7c62 100644 --- a/mobile/lib/routing/auth_guard.dart +++ b/mobile/lib/routing/auth_guard.dart @@ -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 { diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index 1e6ef07089..d4fbdf1835 100644 --- a/mobile/lib/shared/models/asset.dart +++ b/mobile/lib/shared/models/asset.dart @@ -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(StoreKey.currentUser)!.isarId, + ownerId = Store.get(StoreKey.currentUser).isarId, fileModifiedAt = local.modifiedDateTime.toUtc(), updatedAt = local.modifiedDateTime.toUtc(), isFavorite = local.isFavorite, diff --git a/mobile/lib/shared/models/logger_message.model.dart b/mobile/lib/shared/models/logger_message.model.dart new file mode 100644 index 0000000000..cb1d45a580 --- /dev/null +++ b/mobile/lib/shared/models/logger_message.model.dart @@ -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)]; +} diff --git a/mobile/lib/shared/models/logger_message.model.g.dart b/mobile/lib/shared/models/logger_message.model.g.dart new file mode 100644 index 0000000000..1d763754e0 Binary files /dev/null and b/mobile/lib/shared/models/logger_message.model.g.dart differ diff --git a/mobile/lib/shared/models/store.dart b/mobile/lib/shared/models/store.dart index 858d13ad7b..69a620131f 100644 --- a/mobile/lib/shared/models/store.dart +++ b/mobile/lib/shared/models/store.dart @@ -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(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(StoreKey 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(StoreKey key) => _cache[key.id]; /// Stores the value synchronously in the cache and asynchronously in the DB - static Future put(StoreKey key, T value) { + static Future put(StoreKey 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 delete(StoreKey key) { + static Future delete(StoreKey key) { _cache[key.id] = null; return _db.writeTxn(() => _db.storeValues.delete(key.id)); } @@ -58,7 +66,8 @@ class Store { static void _onChangeListener(List? 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(StoreKey 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 _of(dynamic value, StoreKey key) async { + static Future _of(T? value, StoreKey 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 { + userRemoteId(0, type: String), + assetETag(1, type: String), + currentUser(2, type: User, fromDb: _getUser, toDb: _toUser), + deviceIdHash(3, type: int), + deviceId(4, type: String), + backupFailedSince(5, type: DateTime), + backupRequireWifi(6, type: bool), + backupRequireCharging(7, type: bool), + backupTriggerDelay(8, type: int), + githubReleaseInfo(9, type: String), + serverUrl(10, type: String), + accessToken(11, type: String), + serverEndpoint(12, type: String), + // user settings from [AppSettingsEnum] below: + loadPreview(100, type: bool), + loadOriginal(101, type: bool), + themeMode(102, type: String), + tilesPerRow(103, type: int), + dynamicLayout(104, type: bool), + groupAssetsBy(105, type: int), + uploadErrorNotificationGracePeriod(106, type: int), + backgroundBackupTotalProgress(107, type: bool), + backgroundBackupSingleProgress(108, type: bool), + storageIndicator(109, type: bool), + thumbnailCacheSize(110, type: int), + imageCacheSize(111, type: int), + albumThumbnailCacheSize(112, type: int), + selectedAlbumSortOrder(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 Function(Isar, dynamic)? toDb; - final Function(dynamic)? fromJson; + final T? Function(Isar, int)? fromDb; + final Future Function(Isar, T)? toDb; } -User? _getUser(Isar db, int i) => db.users.getSync(i); -Future _toUser(Isar db, dynamic u) { - User user = (u as User); - return db.users.put(user); +T? _getUser(Isar db, int i) { + final User? u = db.users.getSync(i); + return u as T?; +} + +Future _toUser(Isar db, T u) { + if (u is User) { + return db.users.put(u); + } + throw TypeError(); } diff --git a/mobile/lib/shared/providers/release_info.provider.dart b/mobile/lib/shared/providers/release_info.provider.dart index d10a7a07a0..ded511c009 100644 --- a/mobile/lib/shared/providers/release_info.provider.dart +++ b/mobile/lib/shared/providers/release_info.provider.dart @@ -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 { 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 { } void acknowledgeNewVersion() { - var box = Hive.box(hiveGithubReleaseInfoBox); - - box.put(githubReleaseInfoKey, state); + Store.put(StoreKey.githubReleaseInfo, state); VersionAnnouncementOverlayController.appLoader.hide(); } } diff --git a/mobile/lib/shared/providers/websocket.provider.dart b/mobile/lib/shared/providers/websocket.provider.dart index 93ffc205c8..7741b8c766 100644 --- a/mobile/lib/shared/providers/websocket.provider.dart +++ b/mobile/lib/shared/providers/websocket.provider.dart @@ -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 { 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 diff --git a/mobile/lib/shared/services/api.service.dart b/mobile/lib/shared/services/api.service.dart index 237c99a6e1..3af10cac6b 100644 --- a/mobile/lib/shared/services/api.service.dart +++ b/mobile/lib/shared/services/api.service.dart @@ -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; } diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart index 8ede38ca9e..3867a0ed42 100644 --- a/mobile/lib/shared/services/asset.service.dart +++ b/mobile/lib/shared/services/asset.service.dart @@ -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(StoreKey.currentUser)!.isarId) + .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) .count(); final List? 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, String?>? remote = await _apiService.assetApi.getAllAssetsWithETag(eTag: etag); if (remote == null) { diff --git a/mobile/lib/shared/services/immich_logger.service.dart b/mobile/lib/shared/services/immich_logger.service.dart index 75e7cf8d7e..2d0adfce36 100644 --- a/mobile/lib/shared/services/immich_logger.service.dart +++ b/mobile/lib/shared/services/immich_logger.service.dart @@ -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 _box = Hive.box(immichLoggerBox); + final Isar _db = Isar.getInstance()!; + final List _msgBuffer = []; + Timer? _timer; - List 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 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 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 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(); + } + } } diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index f9935004eb..248055e8be 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -241,7 +241,7 @@ class SyncService { } if (album.shared || dto.shared) { - final userId = Store.get(StoreKey.currentUser)!.isarId; + final userId = Store.get(StoreKey.currentUser).isarId; final foreign = await album.assets.filter().not().ownerIdEqualTo(userId).findAll(); existing.addAll(foreign); diff --git a/mobile/lib/shared/services/user.service.dart b/mobile/lib/shared/services/user.service.dart index 4684b30530..757dde5b8d 100644 --- a/mobile/lib/shared/services/user.service.dart +++ b/mobile/lib/shared/services/user.service.dart @@ -42,7 +42,7 @@ class UserService { if (self) { return _db.users.where().findAll(); } - final int userId = Store.get(StoreKey.currentUser)!.isarId; + final int userId = Store.get(StoreKey.currentUser).isarId; return _db.users.where().isarIdNotEqualTo(userId).findAll(); } diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart index cb64b7a45e..95308c5e61 100644 --- a/mobile/lib/shared/ui/immich_image.dart +++ b/mobile/lib/shared/ui/immich_image.dart @@ -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, diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart index ff3b2e71c2..5505ff3937 100644 --- a/mobile/lib/shared/views/app_log_page.dart +++ b/mobile/lib/shared/views/app_log_page.dart @@ -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); diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index 2e116b8f67..b03fd781c8 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -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(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()); diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 237247a9e0..ea249bb984 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -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}'; } diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 87bc375cd3..b2fa92d3e4 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -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 migrateHiveToStoreIfNecessary() async { duplicatedAssetsBox, _migrateDuplicatedAssetsBox, ); + await _migrateHiveBoxIfNecessary( + hiveGithubReleaseInfoBox, + _migrateReleaseInfoBox, + ); + + await _migrateHiveBoxIfNecessary(hiveLoginInfoBox, _migrateLoginInfoBox); + await _migrateHiveBoxIfNecessary( + immichLoggerBox, + (Box box) => box.deleteFromDisk(), + ); + await _migrateHiveBoxIfNecessary(userSettingInfoBox, _migrateAppSettingsBox); +} + +FutureOr _migrateReleaseInfoBox(Box box) => + _migrateKey(box, githubReleaseInfoKey, StoreKey.githubReleaseInfo); + +Future _migrateLoginInfoBox(Box 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 _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 _migrateHiveBackgroundBackupInfoBox(Box box) async { @@ -35,16 +66,15 @@ Future _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 _migrateBackupInfoBox(Box box) async { - final Isar? db = Isar.getInstance(); - if (db == null) { - throw Exception("_migrateBackupInfoBox could not load database"); - } +FutureOr _migrateBackupInfoBox(Box 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 albums = []; for (int i = 0; i < infos.selectedAlbumIds.length; i++) { final album = BackupAlbum( @@ -62,48 +92,49 @@ Future _migrateBackupInfoBox(Box 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 _migrateDuplicatedAssetsBox(Box box) async { - final Isar? db = Isar.getInstance(); - if (db == null) { - throw Exception("_migrateBackupInfoBox could not load database"); - } +FutureOr _migrateDuplicatedAssetsBox(Box 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 _migrateAppSettingsBox(Box box) async { + for (AppSettingsEnum s in AppSettingsEnum.values) { + await _migrateKey(box, s.hiveKey, s.storeKey); } - return box.deleteFromDisk(); } Future _migrateHiveBoxIfNecessary( String boxName, - Future Function(Box) migrate, + FutureOr Function(Box) migrate, ) async { try { if (await Hive.boxExists(boxName)) { - await migrate(await Hive.openBox(boxName)); + final box = await Hive.openBox(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 _migrateKey(Box box, String hiveKey, StoreKey key) { + final T? value = box.get(hiveKey); if (value != null) { - await Store.put(key, value); - await box.delete(hiveKey); + return Store.put(key, value); } }