Browse Source

refactor(mobile): migrate all Hive boxes to Isar database (#2036)

Fynn Petersen-Frey 2 years ago
parent
commit
eccde8fa07
33 changed files with 1539 additions and 382 deletions
  1. 7 9
      mobile/integration_test/test_utils/general_helper.dart
  2. 7 16
      mobile/lib/main.dart
  3. 1 1
      mobile/lib/modules/album/services/album.service.dart
  4. 4 4
      mobile/lib/modules/album/ui/album_thumbnail_listtile.dart
  5. 3 5
      mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
  6. 4 7
      mobile/lib/modules/asset_viewer/views/video_viewer_page.dart
  7. 3 15
      mobile/lib/modules/backup/background_service/background.service.dart
  8. 11 17
      mobile/lib/modules/backup/providers/backup.provider.dart
  9. 7 9
      mobile/lib/modules/backup/services/backup.service.dart
  10. 2 3
      mobile/lib/modules/home/ui/home_page_app_bar.dart
  11. 3 4
      mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart
  12. 3 19
      mobile/lib/modules/login/providers/authentication.provider.dart
  13. 57 66
      mobile/lib/modules/login/ui/login_form.dart
  14. 3 4
      mobile/lib/modules/search/ui/thumbnail_with_info.dart
  15. 4 6
      mobile/lib/modules/search/views/search_page.dart
  16. 46 42
      mobile/lib/modules/settings/services/app_settings.service.dart
  17. 0 1
      mobile/lib/routing/auth_guard.dart
  18. 1 2
      mobile/lib/shared/models/asset.dart
  19. 48 0
      mobile/lib/shared/models/logger_message.model.dart
  20. 1092 0
      mobile/lib/shared/models/logger_message.model.g.dart
  21. 87 41
      mobile/lib/shared/models/store.dart
  22. 4 7
      mobile/lib/shared/providers/release_info.provider.dart
  23. 3 4
      mobile/lib/shared/providers/websocket.provider.dart
  24. 5 10
      mobile/lib/shared/services/api.service.dart
  25. 2 3
      mobile/lib/shared/services/asset.service.dart
  26. 51 30
      mobile/lib/shared/services/immich_logger.service.dart
  27. 1 1
      mobile/lib/shared/services/sync.service.dart
  28. 1 1
      mobile/lib/shared/services/user.service.dart
  29. 2 3
      mobile/lib/shared/ui/immich_image.dart
  30. 9 8
      mobile/lib/shared/views/app_log_page.dart
  31. 9 11
      mobile/lib/shared/views/splash_screen.dart
  32. 3 8
      mobile/lib/utils/image_url_builder.dart
  33. 56 25
      mobile/lib/utils/migration.dart

+ 7 - 9
mobile/integration_test/test_utils/general_helper.dart

@@ -2,7 +2,6 @@ import 'dart:async';
 
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:hive/hive.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:integration_test/integration_test.dart';
 import 'package:integration_test/integration_test.dart';
 import 'package:isar/isar.dart';
 import 'package:isar/isar.dart';
@@ -35,9 +34,7 @@ class ImmichTestHelper {
   }
   }
 
 
   static Future<void> loadApp(WidgetTester tester) async {
   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)
     // Clear all data from Isar (reuse existing instance if available)
     final db = Isar.getInstance() ?? await app.loadDb();
     final db = Isar.getInstance() ?? await app.loadDb();
     await Store.clear();
     await Store.clear();
@@ -65,12 +62,13 @@ void immichWidgetTest(
 }
 }
 
 
 Future<void> pumpUntilFound(
 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;
   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) {
   while (found != true) {
     await tester.pump();
     await tester.pump();
     found = tester.any(finder);
     found = tester.any(finder);

+ 7 - 16
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/asset.dart';
 import 'package:immich_mobile/shared/models/exif_info.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/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/store.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/app_state.provider.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:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:permission_handler/permission_handler.dart';
 import 'package:permission_handler/permission_handler.dart';
-import 'constants/hive_box.dart';
 
 
 void main() async {
 void main() async {
-  await initApp();
+  WidgetsFlutterBinding.ensureInitialized();
   final db = await loadDb();
   final db = await loadDb();
+  await initApp();
   await migrateHiveToStoreIfNecessary();
   await migrateHiveToStoreIfNecessary();
   await migrateJsonCacheIfNecessary();
   await migrateJsonCacheIfNecessary();
   runApp(getMainWidget(db));
   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 {
 Future<void> initApp() async {
   await Hive.initFlutter();
   await Hive.initFlutter();
   Hive.registerAdapter(HiveSavedLoginInfoAdapter());
   Hive.registerAdapter(HiveSavedLoginInfoAdapter());
   Hive.registerAdapter(HiveBackupAlbumsAdapter());
   Hive.registerAdapter(HiveBackupAlbumsAdapter());
   Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
   Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
   Hive.registerAdapter(ImmichLoggerMessageAdapter());
   Hive.registerAdapter(ImmichLoggerMessageAdapter());
-
-  await openBoxes();
+  await EasyLocalization.ensureInitialized();
 
 
   if (kReleaseMode && Platform.isAndroid) {
   if (kReleaseMode && Platform.isAndroid) {
     try {
     try {
@@ -82,7 +71,7 @@ Future<void> initApp() async {
   }
   }
 
 
   // Initialize Immich Logger Service
   // Initialize Immich Logger Service
-  ImmichLogger().init();
+  ImmichLogger();
 
 
   var log = Logger("ImmichErrorLogger");
   var log = Logger("ImmichErrorLogger");
 
 
@@ -108,6 +97,7 @@ Future<Isar> loadDb() async {
       UserSchema,
       UserSchema,
       BackupAlbumSchema,
       BackupAlbumSchema,
       DuplicatedAssetSchema,
       DuplicatedAssetSchema,
+      LoggerMessageSchema,
     ],
     ],
     directory: dir.path,
     directory: dir.path,
     maxSizeMiB: 256,
     maxSizeMiB: 256,
@@ -174,6 +164,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
       case AppLifecycleState.inactive:
       case AppLifecycleState.inactive:
         debugPrint("[APP STATE] inactive");
         debugPrint("[APP STATE] inactive");
         ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
         ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
+        ImmichLogger().flush();
         ref.watch(websocketProvider.notifier).disconnect();
         ref.watch(websocketProvider.notifier).disconnect();
         ref.watch(backupProvider.notifier).cancelBackup();
         ref.watch(backupProvider.notifier).cancelBackup();
 
 

+ 1 - 1
mobile/lib/modules/album/services/album.service.dart

@@ -265,7 +265,7 @@ class AlbumService {
 
 
   Future<bool> deleteAlbum(Album album) async {
   Future<bool> deleteAlbum(Album album) async {
     try {
     try {
-      final userId = Store.get<User>(StoreKey.currentUser)!.isarId;
+      final userId = Store.get(StoreKey.currentUser).isarId;
       if (album.owner.value?.isarId == userId) {
       if (album.owner.value?.isarId == userId) {
         await _apiService.albumApi.deleteAlbum(album.remoteId!);
         await _apiService.albumApi.deleteAlbum(album.remoteId!);
       }
       }

+ 4 - 4
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:cached_network_image/cached_network_image.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.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/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.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:immich_mobile/utils/image_url_builder.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
 
 
@@ -21,7 +20,6 @@ class AlbumThumbnailListTile extends StatelessWidget {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    var box = Hive.box(userInfoBox);
     var cardSize = 68.0;
     var cardSize = 68.0;
     var isDarkMode = Theme.of(context).brightness == Brightness.dark;
     var isDarkMode = Theme.of(context).brightness == Brightness.dark;
 
 
@@ -50,7 +48,9 @@ class AlbumThumbnailListTile extends StatelessWidget {
           album,
           album,
           type: ThumbnailFormat.JPEG,
           type: ThumbnailFormat.JPEG,
         ),
         ),
-        httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
+        httpHeaders: {
+          "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
+        },
         cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
         cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
         errorWidget: (context, url, error) =>
         errorWidget: (context, url, error) =>
             const Icon(Icons.image_not_supported_outlined),
             const Icon(Icons.image_not_supported_outlined),

+ 3 - 5
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:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
-import 'package:flutter_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: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/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/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/exif_bottom_sheet.dart';
 import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.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/asset_viewer/views/video_viewer_page.dart';
 import 'package:immich_mobile/modules/favorite/providers/favorite_provider.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/shared/services/asset.service.dart';
 import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
 import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
@@ -47,7 +46,6 @@ class GalleryViewerPage extends HookConsumerWidget {
 
 
   @override
   @override
   Widget build(BuildContext context, WidgetRef ref) {
   Widget build(BuildContext context, WidgetRef ref) {
-    final Box<dynamic> box = Hive.box(userInfoBox);
     final settings = ref.watch(appSettingsServiceProvider);
     final settings = ref.watch(appSettingsServiceProvider);
     final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
     final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
     final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
     final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
@@ -57,7 +55,7 @@ class GalleryViewerPage extends HookConsumerWidget {
     final isPlayingMotionVideo = useState(false);
     final isPlayingMotionVideo = useState(false);
     final isPlayingVideo = useState(false);
     final isPlayingVideo = useState(false);
     late Offset localPosition;
     late Offset localPosition;
-    final authToken = 'Bearer ${box.get(accessTokenKey)}';
+    final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
 
 
     showAppBar.addListener(() {
     showAppBar.addListener(() {
       // Change to and from immersive mode, hiding navigation and app bar
       // Change to and from immersive mode, hiding navigation and app bar

+ 4 - 7
mobile/lib/modules/asset_viewer/views/video_viewer_page.dart

@@ -1,13 +1,12 @@
 import 'dart:io';
 import 'dart:io';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:hive/hive.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:chewie/chewie.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/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/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
 import 'package:immich_mobile/shared/models/asset.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:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:video_player/video_player.dart';
 import 'package:video_player/video_player.dart';
@@ -54,17 +53,15 @@ class VideoViewerPage extends HookConsumerWidget {
     }
     }
     final downloadAssetStatus =
     final downloadAssetStatus =
         ref.watch(imageViewerStateProvider).downloadAssetStatus;
         ref.watch(imageViewerStateProvider).downloadAssetStatus;
-    final box = Hive.box(userInfoBox);
-    final String jwtToken = box.get(accessTokenKey);
     final String videoUrl = isMotionVideo
     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(
     return Stack(
       children: [
       children: [
         VideoThumbnailPlayer(
         VideoThumbnailPlayer(
           url: videoUrl,
           url: videoUrl,
-          jwtToken: jwtToken,
+          jwtToken: Store.get(StoreKey.accessToken),
           isMotionVideo: isMotionVideo,
           isMotionVideo: isMotionVideo,
           onVideoEnded: onVideoEnded,
           onVideoEnded: onVideoEnded,
           onPaused: onPaused,
           onPaused: onPaused,

+ 3 - 15
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:easy_localization/easy_localization.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
-import 'package:hive_flutter/hive_flutter.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/main.dart';
 import 'package:immich_mobile/main.dart';
 import 'package:immich_mobile/modules/backup/background_service/localization.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/backup_album.model.dart';
 import 'package:immich_mobile/modules/backup/models/current_upload_asset.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/models/error_upload_asset.model.dart';
 import 'package:immich_mobile/modules/backup/services/backup.service.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/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
@@ -317,7 +314,6 @@ class BackgroundService {
           debugPrint(error.toString());
           debugPrint(error.toString());
           return false;
           return false;
         } finally {
         } finally {
-          await Hive.close();
           releaseLock();
           releaseLock();
         }
         }
       case "systemStop":
       case "systemStop":
@@ -332,17 +328,9 @@ class BackgroundService {
 
 
   Future<bool> _onAssetsChanged() async {
   Future<bool> _onAssetsChanged() async {
     final Isar db = await loadDb();
     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 apiService = ApiService();
-    apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
+    apiService.setAccessToken(Store.get(StoreKey.accessToken));
     BackupService backupService = BackupService(apiService, db);
     BackupService backupService = BackupService(apiService, db);
     AppSettingsService settingsService = AppSettingsService();
     AppSettingsService settingsService = AppSettingsService();
 
 
@@ -387,7 +375,7 @@ class BackgroundService {
           db.backupAlbums.deleteAllSync(toDelete);
           db.backupAlbums.deleteAllSync(toDelete);
           db.backupAlbums.putAllSync(toUpsert);
           db.backupAlbums.putAllSync(toUpsert);
         });
         });
-      } else if (Store.get(StoreKey.backupFailedSince) == null) {
+      } else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
         Store.put(StoreKey.backupFailedSince, DateTime.now());
         Store.put(StoreKey.backupFailedSince, DateTime.now());
         return false;
         return false;
       }
       }
@@ -529,7 +517,7 @@ class BackgroundService {
     } else if (value == 5) {
     } else if (value == 5) {
       return false;
       return false;
     }
     }
-    final DateTime? failedSince = Store.get(StoreKey.backupFailedSince);
+    final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince);
     if (failedSince == null) {
     if (failedSince == null) {
       return false;
       return false;
     }
     }

+ 11 - 17
mobile/lib/modules/backup/providers/backup.provider.dart

@@ -1,9 +1,7 @@
 import 'package:cancellation_token_http/http.dart';
 import 'package:cancellation_token_http/http.dart';
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter/widgets.dart';
-import 'package:hive_flutter/hive_flutter.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/modules/backup/models/available_album.model.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_album.model.dart';
 import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
@@ -42,9 +40,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
             progressInPercentage: 0,
             progressInPercentage: 0,
             cancelToken: CancellationToken(),
             cancelToken: CancellationToken(),
             backgroundBackup: false,
             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(
             serverInfo: ServerInfoResponseDto(
               diskAvailable: "0",
               diskAvailable: "0",
               diskAvailableRaw: 0,
               diskAvailableRaw: 0,
@@ -163,14 +162,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
             triggerMaxDelay: state.backupTriggerDelay * 10,
             triggerMaxDelay: state.backupTriggerDelay * 10,
           );
           );
       if (success) {
       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 {
       } else {
         state = state.copyWith(
         state = state.copyWith(
           backgroundBackup: wasEnabled,
           backgroundBackup: wasEnabled,
@@ -544,7 +541,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
 
   Future<void> _resumeBackup() async {
   Future<void> _resumeBackup() async {
     // Check if user is login
     // Check if user is login
-    final accessKey = Hive.box(userInfoBox).get(accessTokenKey);
+    final accessKey = Store.tryGet(StoreKey.accessToken);
 
 
     // User has been logged out return
     // User has been logged out return
     if (accessKey == null || !_authState.isAuthenticated) {
     if (accessKey == null || !_authState.isAuthenticated) {
@@ -603,9 +600,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
       backupProgress: BackUpProgressEnum.inBackground,
       backupProgress: BackUpProgressEnum.inBackground,
       selectedBackupAlbums: selectedAlbums,
       selectedBackupAlbums: selectedAlbums,
       excludedBackupAlbums: excludedAlbums,
       excludedBackupAlbums: excludedAlbums,
-      backupRequireWifi: Store.get(StoreKey.backupRequireWifi),
-      backupRequireCharging: Store.get(StoreKey.backupRequireCharging),
-      backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay),
     );
     );
     // assumes the background service is currently running
     // assumes the background service is currently running
     // if true, waits until it has stopped to start the backup
     // if true, waits until it has stopped to start the backup

+ 7 - 9
mobile/lib/modules/backup/services/backup.service.dart

@@ -5,13 +5,12 @@ import 'dart:io';
 import 'package:cancellation_token_http/http.dart';
 import 'package:cancellation_token_http/http.dart';
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:hive/hive.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/modules/backup/models/backup_album.model.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/current_upload_asset.model.dart';
 import 'package:immich_mobile/modules/backup/models/duplicated_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/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/api.provider.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
@@ -38,7 +37,7 @@ class BackupService {
   BackupService(this._apiService, this._db);
   BackupService(this._apiService, this._db);
 
 
   Future<List<String>?> getDeviceBackupAsset() async {
   Future<List<String>?> getDeviceBackupAsset() async {
-    String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
+    final String deviceId = Store.get(StoreKey.deviceId);
 
 
     try {
     try {
       return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
       return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
@@ -173,7 +172,7 @@ class BackupService {
     }
     }
     final Set<String> existing = {};
     final Set<String> existing = {};
     try {
     try {
-      final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
+      final String deviceId = Store.get(StoreKey.deviceId);
       final CheckExistingAssetsResponseDto? duplicates =
       final CheckExistingAssetsResponseDto? duplicates =
           await _apiService.assetApi.checkExistingAssets(
           await _apiService.assetApi.checkExistingAssets(
         CheckExistingAssetsDto(
         CheckExistingAssetsDto(
@@ -204,8 +203,8 @@ class BackupService {
     Function(CurrentUploadAsset) setCurrentUploadAssetCb,
     Function(CurrentUploadAsset) setCurrentUploadAssetCb,
     Function(ErrorUploadAsset) errorCb,
     Function(ErrorUploadAsset) errorCb,
   ) async {
   ) 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;
     File? file;
     bool anyErrors = false;
     bool anyErrors = false;
     final List<String> duplicatedAssetIds = [];
     final List<String> duplicatedAssetIds = [];
@@ -236,15 +235,14 @@ class BackupService {
             ),
             ),
           );
           );
 
 
-          var box = Hive.box(userInfoBox);
-
           var req = MultipartRequest(
           var req = MultipartRequest(
             'POST',
             'POST',
             Uri.parse('$savedEndpoint/asset/upload'),
             Uri.parse('$savedEndpoint/asset/upload'),
             onProgress: ((bytes, totalBytes) =>
             onProgress: ((bytes, totalBytes) =>
                 uploadProgressCb(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['deviceAssetId'] = entity.id;
           req.fields['deviceId'] = deviceId;
           req.fields['deviceId'] = deviceId;

+ 2 - 3
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:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:hive/hive.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.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/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/modules/backup/models/backup_state.model.dart';
 import 'package:immich_mobile/shared/models/server_info_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/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/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/ui/transparent_image.dart';
 import 'package:immich_mobile/shared/ui/transparent_image.dart';
 
 
@@ -47,7 +46,7 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
           },
           },
         );
         );
       } else {
       } else {
-        String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
+        final String? endpoint = Store.get(StoreKey.serverEndpoint);
         var dummy = Random().nextInt(1024);
         var dummy = Random().nextInt(1024);
         return InkWell(
         return InkWell(
           onTap: () {
           onTap: () {

+ 3 - 4
mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart

@@ -1,14 +1,13 @@
 import 'dart:math';
 import 'dart:math';
 
 
 import 'package:flutter/material.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:hooks_riverpod/hooks_riverpod.dart';
 import 'package:image_picker/image_picker.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/home/providers/upload_profile_image.provider.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.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/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/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/transparent_image.dart';
 import 'package:immich_mobile/shared/ui/transparent_image.dart';
 
 
@@ -19,7 +18,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
 
 
   @override
   @override
   Widget build(BuildContext context, WidgetRef ref) {
   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);
     AuthenticationState authState = ref.watch(authenticationProvider);
     final uploadProfileImageStatus =
     final uploadProfileImageStatus =
         ref.watch(uploadProfileImageProvider).status;
         ref.watch(uploadProfileImageProvider).status;

+ 3 - 19
mobile/lib/modules/login/providers/authentication.provider.dart

@@ -2,12 +2,9 @@ import 'dart:io';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
-import 'package:hive/hive.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/shared/models/store.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/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/modules/backup/services/backup.service.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
@@ -91,11 +88,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
     try {
     try {
       await Future.wait([
       await Future.wait([
         _apiService.authenticationApi.logout(),
         _apiService.authenticationApi.logout(),
-        Hive.box(userInfoBox).delete(accessTokenKey),
         Store.delete(StoreKey.assetETag),
         Store.delete(StoreKey.assetETag),
         Store.delete(StoreKey.userRemoteId),
         Store.delete(StoreKey.userRemoteId),
         Store.delete(StoreKey.currentUser),
         Store.delete(StoreKey.currentUser),
-        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey)
+        Store.delete(StoreKey.accessToken),
       ]);
       ]);
 
 
       state = state.copyWith(isAuthenticated: false);
       state = state.copyWith(isAuthenticated: false);
@@ -157,14 +153,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
     }
     }
 
 
     if (userResponseDto != null) {
     if (userResponseDto != null) {
-      var userInfoHiveBox = await Hive.openBox(userInfoBox);
       var deviceInfo = await _deviceInfoService.getDeviceInfo();
       var deviceInfo = await _deviceInfoService.getDeviceInfo();
-      userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
-      userInfoHiveBox.put(accessTokenKey, accessToken);
       Store.put(StoreKey.deviceId, deviceInfo["deviceId"]);
       Store.put(StoreKey.deviceId, deviceInfo["deviceId"]);
       Store.put(StoreKey.deviceIdHash, fastHash(deviceInfo["deviceId"]));
       Store.put(StoreKey.deviceIdHash, fastHash(deviceInfo["deviceId"]));
       Store.put(StoreKey.userRemoteId, userResponseDto.id);
       Store.put(StoreKey.userRemoteId, userResponseDto.id);
       Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
       Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
+      Store.put(StoreKey.serverUrl, serverUrl);
+      Store.put(StoreKey.accessToken, accessToken);
 
 
       state = state.copyWith(
       state = state.copyWith(
         isAuthenticated: true,
         isAuthenticated: true,
@@ -178,17 +173,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
         deviceId: deviceInfo["deviceId"],
         deviceId: deviceInfo["deviceId"],
         deviceType: deviceInfo["deviceType"],
         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
     // Register device info

+ 57 - 66
mobile/lib/modules/login/ui/login_form.dart

@@ -1,14 +1,12 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.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: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/login/providers/oauth.provider.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/routing/router.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/api.provider.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -63,8 +61,7 @@ class LoginForm extends HookConsumerWidget {
 
 
       try {
       try {
         isLoadingServer.value = true;
         isLoadingServer.value = true;
-        final endpoint =
-            await apiService.resolveAndSetEndpoint(serverUrl);
+        final endpoint = await apiService.resolveAndSetEndpoint(serverUrl);
 
 
         final loginConfig = await apiService.oAuthApi.generateConfig(
         final loginConfig = await apiService.oAuthApi.generateConfig(
           OAuthConfigDto(redirectUri: serverUrl),
           OAuthConfigDto(redirectUri: serverUrl),
@@ -104,15 +101,10 @@ class LoginForm extends HookConsumerWidget {
 
 
     useEffect(
     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;
         return null;
       },
       },
       [],
       [],
@@ -133,11 +125,11 @@ class LoginForm extends HookConsumerWidget {
 
 
       try {
       try {
         final isAuthenticated =
         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) {
         if (isAuthenticated) {
           // Resume backup (if enable) then navigate
           // Resume backup (if enable) then navigate
           if (ref.read(authenticationProvider).shouldChangePassword &&
           if (ref.read(authenticationProvider).shouldChangePassword &&
@@ -283,61 +275,61 @@ class LoginForm extends HookConsumerWidget {
               onSubmit: login,
               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(
     return LayoutBuilder(
       builder: (context, constraints) {
       builder: (context, constraints) {
@@ -545,7 +537,6 @@ class OAuthLoginButton extends ConsumerWidget {
 
 
   @override
   @override
   Widget build(BuildContext context, WidgetRef ref) {
   Widget build(BuildContext context, WidgetRef ref) {
-
     return ElevatedButton.icon(
     return ElevatedButton.icon(
       style: ElevatedButton.styleFrom(
       style: ElevatedButton.styleFrom(
         backgroundColor: Theme.of(context).primaryColor.withAlpha(230),
         backgroundColor: Theme.of(context).primaryColor.withAlpha(230),

+ 3 - 4
mobile/lib/modules/search/ui/thumbnail_with_info.dart

@@ -1,7 +1,6 @@
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package: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 {
 class ThumbnailWithInfo extends StatelessWidget {
   const ThumbnailWithInfo({
   const ThumbnailWithInfo({
@@ -19,7 +18,6 @@ class ThumbnailWithInfo extends StatelessWidget {
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
-    var box = Hive.box(userInfoBox);
     var isDarkMode = Theme.of(context).brightness == Brightness.dark;
     var isDarkMode = Theme.of(context).brightness == Brightness.dark;
     var textAndIconColor = isDarkMode ? Colors.grey[100] : Colors.grey[700];
     var textAndIconColor = isDarkMode ? Colors.grey[100] : Colors.grey[700];
     return GestureDetector(
     return GestureDetector(
@@ -51,7 +49,8 @@ class ThumbnailWithInfo extends StatelessWidget {
                           fit: BoxFit.cover,
                           fit: BoxFit.cover,
                           imageUrl: imageUrl!,
                           imageUrl: imageUrl!,
                           httpHeaders: {
                           httpHeaders: {
-                            "Authorization": "Bearer ${box.get(accessTokenKey)}"
+                            "Authorization":
+                                "Bearer ${Store.get(StoreKey.accessToken)}"
                           },
                           },
                           errorWidget: (context, url, error) =>
                           errorWidget: (context, url, error) =>
                               const Icon(Icons.image_not_supported_outlined),
                               const Icon(Icons.image_not_supported_outlined),

+ 4 - 6
mobile/lib/modules/search/views/search_page.dart

@@ -1,15 +1,14 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.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: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/providers/search_page_state.provider.dart';
 import 'package:immich_mobile/modules/search/ui/search_bar.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/search_suggestion_list.dart';
 import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
 import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
 import 'package:immich_mobile/routing/router.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/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/utils/capitalize_first_letter.dart';
 import 'package:immich_mobile/utils/capitalize_first_letter.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
@@ -22,7 +21,6 @@ class SearchPage extends HookConsumerWidget {
 
 
   @override
   @override
   Widget build(BuildContext context, WidgetRef ref) {
   Widget build(BuildContext context, WidgetRef ref) {
-    var box = Hive.box(userInfoBox);
     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
     AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
     AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
         ref.watch(getCuratedLocationProvider);
         ref.watch(getCuratedLocationProvider);
@@ -64,7 +62,7 @@ class SearchPage extends HookConsumerWidget {
                     itemBuilder: ((context, index) {
                     itemBuilder: ((context, index) {
                       var locationInfo = curatedLocations[index];
                       var locationInfo = curatedLocations[index];
                       var thumbnailRequestUrl =
                       var thumbnailRequestUrl =
-                          '${box.get(serverEndpointKey)}/asset/thumbnail/${locationInfo.id}';
+                          '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${locationInfo.id}';
                       return ThumbnailWithInfo(
                       return ThumbnailWithInfo(
                         imageUrl: thumbnailRequestUrl,
                         imageUrl: thumbnailRequestUrl,
                         textInfo: locationInfo.city,
                         textInfo: locationInfo.city,
@@ -113,7 +111,7 @@ class SearchPage extends HookConsumerWidget {
                     itemBuilder: ((context, index) {
                     itemBuilder: ((context, index) {
                       var curatedObjectInfo = objects[index];
                       var curatedObjectInfo = objects[index];
                       var thumbnailRequestUrl =
                       var thumbnailRequestUrl =
-                          '${box.get(serverEndpointKey)}/asset/thumbnail/${curatedObjectInfo.id}';
+                          '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${curatedObjectInfo.id}';
 
 
                       return ThumbnailWithInfo(
                       return ThumbnailWithInfo(
                         imageUrl: thumbnailRequestUrl,
                         imageUrl: thumbnailRequestUrl,

+ 46 - 42
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<T> {
 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>(
   uploadErrorNotificationGracePeriod<int>(
+    StoreKey.uploadErrorNotificationGracePeriod,
     "uploadErrorNotificationGracePeriod",
     "uploadErrorNotificationGracePeriod",
     2,
     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 String hiveKey;
   final T defaultValue;
   final T defaultValue;
 }
 }
 
 
 class AppSettingsService {
 class AppSettingsService {
-  late final Box hiveBox;
-
-  AppSettingsService() {
-    hiveBox = Hive.box(userSettingInfoBox);
-  }
-
-  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 getSetting<T>(AppSettingsEnum<T> setting) {
+    return Store.get(setting.storeKey, setting.defaultValue);
   }
   }
 
 
-  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);
   }
   }
 }
 }

+ 0 - 1
mobile/lib/routing/auth_guard.dart

@@ -13,7 +13,6 @@ class AuthGuard extends AutoRouteGuard {
   void onNavigation(NavigationResolver resolver, StackRouter router) async {
   void onNavigation(NavigationResolver resolver, StackRouter router) async {
     try {
     try {
       var res = await _apiService.authenticationApi.validateAccessToken();
       var res = await _apiService.authenticationApi.validateAccessToken();
-
       if (res != null && res.authStatus) {
       if (res != null && res.authStatus) {
         resolver.next(true);
         resolver.next(true);
       } else {
       } else {

+ 1 - 2
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/exif_info.dart';
 import 'package:immich_mobile/shared/models/store.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:immich_mobile/utils/hash.dart';
 import 'package:isar/isar.dart';
 import 'package:isar/isar.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
@@ -40,7 +39,7 @@ class Asset {
         width = local.width,
         width = local.width,
         fileName = local.title!,
         fileName = local.title!,
         deviceId = Store.get(StoreKey.deviceIdHash),
         deviceId = Store.get(StoreKey.deviceIdHash),
-        ownerId = Store.get<User>(StoreKey.currentUser)!.isarId,
+        ownerId = Store.get(StoreKey.currentUser).isarId,
         fileModifiedAt = local.modifiedDateTime.toUtc(),
         fileModifiedAt = local.modifiedDateTime.toUtc(),
         updatedAt = local.modifiedDateTime.toUtc(),
         updatedAt = local.modifiedDateTime.toUtc(),
         isFavorite = local.isFavorite,
         isFavorite = local.isFavorite,

+ 48 - 0
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)];
+}

+ 1092 - 0
mobile/lib/shared/models/logger_message.model.g.dart

@@ -0,0 +1,1092 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'logger_message.model.dart';
+
+// **************************************************************************
+// IsarCollectionGenerator
+// **************************************************************************
+
+// coverage:ignore-file
+// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters
+
+extension GetLoggerMessageCollection on Isar {
+  IsarCollection<LoggerMessage> get loggerMessages => this.collection();
+}
+
+const LoggerMessageSchema = CollectionSchema(
+  name: r'LoggerMessage',
+  id: -1606100856208753787,
+  properties: {
+    r'context1': PropertySchema(
+      id: 0,
+      name: r'context1',
+      type: IsarType.string,
+    ),
+    r'context2': PropertySchema(
+      id: 1,
+      name: r'context2',
+      type: IsarType.string,
+    ),
+    r'createdAt': PropertySchema(
+      id: 2,
+      name: r'createdAt',
+      type: IsarType.dateTime,
+    ),
+    r'level': PropertySchema(
+      id: 3,
+      name: r'level',
+      type: IsarType.byte,
+      enumMap: _LoggerMessagelevelEnumValueMap,
+    ),
+    r'message': PropertySchema(
+      id: 4,
+      name: r'message',
+      type: IsarType.string,
+    )
+  },
+  estimateSize: _loggerMessageEstimateSize,
+  serialize: _loggerMessageSerialize,
+  deserialize: _loggerMessageDeserialize,
+  deserializeProp: _loggerMessageDeserializeProp,
+  idName: r'id',
+  indexes: {},
+  links: {},
+  embeddedSchemas: {},
+  getId: _loggerMessageGetId,
+  getLinks: _loggerMessageGetLinks,
+  attach: _loggerMessageAttach,
+  version: '3.0.5',
+);
+
+int _loggerMessageEstimateSize(
+  LoggerMessage object,
+  List<int> offsets,
+  Map<Type, List<int>> allOffsets,
+) {
+  var bytesCount = offsets.last;
+  {
+    final value = object.context1;
+    if (value != null) {
+      bytesCount += 3 + value.length * 3;
+    }
+  }
+  {
+    final value = object.context2;
+    if (value != null) {
+      bytesCount += 3 + value.length * 3;
+    }
+  }
+  bytesCount += 3 + object.message.length * 3;
+  return bytesCount;
+}
+
+void _loggerMessageSerialize(
+  LoggerMessage object,
+  IsarWriter writer,
+  List<int> offsets,
+  Map<Type, List<int>> allOffsets,
+) {
+  writer.writeString(offsets[0], object.context1);
+  writer.writeString(offsets[1], object.context2);
+  writer.writeDateTime(offsets[2], object.createdAt);
+  writer.writeByte(offsets[3], object.level.index);
+  writer.writeString(offsets[4], object.message);
+}
+
+LoggerMessage _loggerMessageDeserialize(
+  Id id,
+  IsarReader reader,
+  List<int> offsets,
+  Map<Type, List<int>> allOffsets,
+) {
+  final object = LoggerMessage(
+    context1: reader.readStringOrNull(offsets[0]),
+    context2: reader.readStringOrNull(offsets[1]),
+    createdAt: reader.readDateTime(offsets[2]),
+    level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[3])] ??
+        LogLevel.ALL,
+    message: reader.readString(offsets[4]),
+  );
+  object.id = id;
+  return object;
+}
+
+P _loggerMessageDeserializeProp<P>(
+  IsarReader reader,
+  int propertyId,
+  int offset,
+  Map<Type, List<int>> allOffsets,
+) {
+  switch (propertyId) {
+    case 0:
+      return (reader.readStringOrNull(offset)) as P;
+    case 1:
+      return (reader.readStringOrNull(offset)) as P;
+    case 2:
+      return (reader.readDateTime(offset)) as P;
+    case 3:
+      return (_LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offset)] ??
+          LogLevel.ALL) as P;
+    case 4:
+      return (reader.readString(offset)) as P;
+    default:
+      throw IsarError('Unknown property with id $propertyId');
+  }
+}
+
+const _LoggerMessagelevelEnumValueMap = {
+  'ALL': 0,
+  'FINEST': 1,
+  'FINER': 2,
+  'FINE': 3,
+  'CONFIG': 4,
+  'INFO': 5,
+  'WARNING': 6,
+  'SEVERE': 7,
+  'SHOUT': 8,
+  'OFF': 9,
+};
+const _LoggerMessagelevelValueEnumMap = {
+  0: LogLevel.ALL,
+  1: LogLevel.FINEST,
+  2: LogLevel.FINER,
+  3: LogLevel.FINE,
+  4: LogLevel.CONFIG,
+  5: LogLevel.INFO,
+  6: LogLevel.WARNING,
+  7: LogLevel.SEVERE,
+  8: LogLevel.SHOUT,
+  9: LogLevel.OFF,
+};
+
+Id _loggerMessageGetId(LoggerMessage object) {
+  return object.id;
+}
+
+List<IsarLinkBase<dynamic>> _loggerMessageGetLinks(LoggerMessage object) {
+  return [];
+}
+
+void _loggerMessageAttach(
+    IsarCollection<dynamic> col, Id id, LoggerMessage object) {
+  object.id = id;
+}
+
+extension LoggerMessageQueryWhereSort
+    on QueryBuilder<LoggerMessage, LoggerMessage, QWhere> {
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterWhere> anyId() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addWhereClause(const IdWhereClause.any());
+    });
+  }
+}
+
+extension LoggerMessageQueryWhere
+    on QueryBuilder<LoggerMessage, LoggerMessage, QWhereClause> {
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterWhereClause> idEqualTo(
+      Id id) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addWhereClause(IdWhereClause.between(
+        lower: id,
+        upper: id,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterWhereClause> idNotEqualTo(
+      Id id) {
+    return QueryBuilder.apply(this, (query) {
+      if (query.whereSort == Sort.asc) {
+        return query
+            .addWhereClause(
+              IdWhereClause.lessThan(upper: id, includeUpper: false),
+            )
+            .addWhereClause(
+              IdWhereClause.greaterThan(lower: id, includeLower: false),
+            );
+      } else {
+        return query
+            .addWhereClause(
+              IdWhereClause.greaterThan(lower: id, includeLower: false),
+            )
+            .addWhereClause(
+              IdWhereClause.lessThan(upper: id, includeUpper: false),
+            );
+      }
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterWhereClause> idGreaterThan(
+      Id id,
+      {bool include = false}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addWhereClause(
+        IdWhereClause.greaterThan(lower: id, includeLower: include),
+      );
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterWhereClause> idLessThan(
+      Id id,
+      {bool include = false}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addWhereClause(
+        IdWhereClause.lessThan(upper: id, includeUpper: include),
+      );
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterWhereClause> idBetween(
+    Id lowerId,
+    Id upperId, {
+    bool includeLower = true,
+    bool includeUpper = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addWhereClause(IdWhereClause.between(
+        lower: lowerId,
+        includeLower: includeLower,
+        upper: upperId,
+        includeUpper: includeUpper,
+      ));
+    });
+  }
+}
+
+extension LoggerMessageQueryFilter
+    on QueryBuilder<LoggerMessage, LoggerMessage, QFilterCondition> {
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1IsNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNull(
+        property: r'context1',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1IsNotNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNotNull(
+        property: r'context1',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1EqualTo(
+    String? value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'context1',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1GreaterThan(
+    String? value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'context1',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1LessThan(
+    String? value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'context1',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1Between(
+    String? lower,
+    String? upper, {
+    bool includeLower = true,
+    bool includeUpper = true,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.between(
+        property: r'context1',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1StartsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.startsWith(
+        property: r'context1',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1EndsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.endsWith(
+        property: r'context1',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1Contains(String value, {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.contains(
+        property: r'context1',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1Matches(String pattern, {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.matches(
+        property: r'context1',
+        wildcard: pattern,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1IsEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'context1',
+        value: '',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context1IsNotEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        property: r'context1',
+        value: '',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2IsNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNull(
+        property: r'context2',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2IsNotNull() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(const FilterCondition.isNotNull(
+        property: r'context2',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2EqualTo(
+    String? value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'context2',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2GreaterThan(
+    String? value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'context2',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2LessThan(
+    String? value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'context2',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2Between(
+    String? lower,
+    String? upper, {
+    bool includeLower = true,
+    bool includeUpper = true,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.between(
+        property: r'context2',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2StartsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.startsWith(
+        property: r'context2',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2EndsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.endsWith(
+        property: r'context2',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2Contains(String value, {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.contains(
+        property: r'context2',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2Matches(String pattern, {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.matches(
+        property: r'context2',
+        wildcard: pattern,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2IsEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'context2',
+        value: '',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      context2IsNotEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        property: r'context2',
+        value: '',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      createdAtEqualTo(DateTime value) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'createdAt',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      createdAtGreaterThan(
+    DateTime value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'createdAt',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      createdAtLessThan(
+    DateTime value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'createdAt',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      createdAtBetween(
+    DateTime lower,
+    DateTime upper, {
+    bool includeLower = true,
+    bool includeUpper = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.between(
+        property: r'createdAt',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition> idEqualTo(
+      Id value) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'id',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      idGreaterThan(
+    Id value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'id',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition> idLessThan(
+    Id value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'id',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition> idBetween(
+    Id lower,
+    Id upper, {
+    bool includeLower = true,
+    bool includeUpper = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.between(
+        property: r'id',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      levelEqualTo(LogLevel value) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'level',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      levelGreaterThan(
+    LogLevel value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'level',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      levelLessThan(
+    LogLevel value, {
+    bool include = false,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'level',
+        value: value,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      levelBetween(
+    LogLevel lower,
+    LogLevel upper, {
+    bool includeLower = true,
+    bool includeUpper = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.between(
+        property: r'level',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageEqualTo(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'message',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageGreaterThan(
+    String value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        include: include,
+        property: r'message',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageLessThan(
+    String value, {
+    bool include = false,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.lessThan(
+        include: include,
+        property: r'message',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageBetween(
+    String lower,
+    String upper, {
+    bool includeLower = true,
+    bool includeUpper = true,
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.between(
+        property: r'message',
+        lower: lower,
+        includeLower: includeLower,
+        upper: upper,
+        includeUpper: includeUpper,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageStartsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.startsWith(
+        property: r'message',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageEndsWith(
+    String value, {
+    bool caseSensitive = true,
+  }) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.endsWith(
+        property: r'message',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageContains(String value, {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.contains(
+        property: r'message',
+        value: value,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageMatches(String pattern, {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.matches(
+        property: r'message',
+        wildcard: pattern,
+        caseSensitive: caseSensitive,
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageIsEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.equalTo(
+        property: r'message',
+        value: '',
+      ));
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterFilterCondition>
+      messageIsNotEmpty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addFilterCondition(FilterCondition.greaterThan(
+        property: r'message',
+        value: '',
+      ));
+    });
+  }
+}
+
+extension LoggerMessageQueryObject
+    on QueryBuilder<LoggerMessage, LoggerMessage, QFilterCondition> {}
+
+extension LoggerMessageQueryLinks
+    on QueryBuilder<LoggerMessage, LoggerMessage, QFilterCondition> {}
+
+extension LoggerMessageQuerySortBy
+    on QueryBuilder<LoggerMessage, LoggerMessage, QSortBy> {
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByContext1() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context1', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy>
+      sortByContext1Desc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context1', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByContext2() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context2', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy>
+      sortByContext2Desc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context2', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByCreatedAt() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'createdAt', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy>
+      sortByCreatedAtDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'createdAt', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByLevel() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'level', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByLevelDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'level', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByMessage() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'message', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> sortByMessageDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'message', Sort.desc);
+    });
+  }
+}
+
+extension LoggerMessageQuerySortThenBy
+    on QueryBuilder<LoggerMessage, LoggerMessage, QSortThenBy> {
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByContext1() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context1', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy>
+      thenByContext1Desc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context1', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByContext2() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context2', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy>
+      thenByContext2Desc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'context2', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByCreatedAt() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'createdAt', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy>
+      thenByCreatedAtDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'createdAt', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenById() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'id', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByIdDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'id', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByLevel() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'level', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByLevelDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'level', Sort.desc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByMessage() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'message', Sort.asc);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QAfterSortBy> thenByMessageDesc() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addSortBy(r'message', Sort.desc);
+    });
+  }
+}
+
+extension LoggerMessageQueryWhereDistinct
+    on QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> {
+  QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> distinctByContext1(
+      {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'context1', caseSensitive: caseSensitive);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> distinctByContext2(
+      {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'context2', caseSensitive: caseSensitive);
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> distinctByCreatedAt() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'createdAt');
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> distinctByLevel() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'level');
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LoggerMessage, QDistinct> distinctByMessage(
+      {bool caseSensitive = true}) {
+    return QueryBuilder.apply(this, (query) {
+      return query.addDistinctBy(r'message', caseSensitive: caseSensitive);
+    });
+  }
+}
+
+extension LoggerMessageQueryProperty
+    on QueryBuilder<LoggerMessage, LoggerMessage, QQueryProperty> {
+  QueryBuilder<LoggerMessage, int, QQueryOperations> idProperty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'id');
+    });
+  }
+
+  QueryBuilder<LoggerMessage, String?, QQueryOperations> context1Property() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'context1');
+    });
+  }
+
+  QueryBuilder<LoggerMessage, String?, QQueryOperations> context2Property() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'context2');
+    });
+  }
+
+  QueryBuilder<LoggerMessage, DateTime, QQueryOperations> createdAtProperty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'createdAt');
+    });
+  }
+
+  QueryBuilder<LoggerMessage, LogLevel, QQueryOperations> levelProperty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'level');
+    });
+  }
+
+  QueryBuilder<LoggerMessage, String, QQueryOperations> messageProperty() {
+    return QueryBuilder.apply(this, (query) {
+      return query.addPropertyName(r'message');
+    });
+  }
+}

+ 87 - 41
mobile/lib/shared/models/store.dart

@@ -1,7 +1,6 @@
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:isar/isar.dart';
 import 'package:isar/isar.dart';
-import 'dart:convert';
 
 
 part 'store.g.dart';
 part 'store.g.dart';
 
 
@@ -26,12 +25,21 @@ class Store {
     return _db.writeTxn(() => _db.storeValues.clear());
     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
   /// 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;
     _cache[key.id] = value;
     return _db.writeTxn(
     return _db.writeTxn(
       () async => _db.storeValues.put(await StoreValue._of(value, key)),
       () 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
   /// 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;
     _cache[key.id] = null;
     return _db.writeTxn(() => _db.storeValues.delete(key.id));
     return _db.writeTxn(() => _db.storeValues.delete(key.id));
   }
   }
@@ -58,7 +66,8 @@ class Store {
   static void _onChangeListener(List<StoreValue>? data) {
   static void _onChangeListener(List<StoreValue>? data) {
     if (data != null) {
     if (data != null) {
       for (StoreValue value in data) {
       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;
   int? intValue;
   String? strValue;
   String? strValue;
 
 
-  dynamic _extract(StoreKey key) {
+  T? _extract<T>(StoreKey<T> key) {
     switch (key.type) {
     switch (key.type) {
       case int:
       case int:
-        return key.fromDb == null
-            ? intValue
-            : key.fromDb!.call(Store._db, intValue!);
+        return intValue as T?;
       case bool:
       case bool:
-        return intValue == null ? null : intValue! == 1;
+        return intValue == null ? null : (intValue! == 1) as T;
       case DateTime:
       case DateTime:
         return intValue == null
         return intValue == null
             ? null
             ? null
-            : DateTime.fromMicrosecondsSinceEpoch(intValue!);
+            : DateTime.fromMicrosecondsSinceEpoch(intValue!) as T;
       case String:
       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;
     int? i;
     String? s;
     String? s;
     switch (key.type) {
     switch (key.type) {
       case int:
       case int:
-        i = (key.toDb == null ? value : await key.toDb!.call(Store._db, value));
+        i = value as int?;
         break;
         break;
       case bool:
       case bool:
-        i = value == null ? null : (value ? 1 : 0);
+        i = value == null ? null : (value == true ? 1 : 0);
         break;
         break;
       case DateTime:
       case DateTime:
         i = value == null ? null : (value as DateTime).microsecondsSinceEpoch;
         i = value == null ? null : (value as DateTime).microsecondsSinceEpoch;
         break;
         break;
       case String:
       case String:
-        s = key.fromJson == null ? value : json.encode(value.toJson());
+        s = value as String?;
         break;
         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);
     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`.
 /// 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(
   const StoreKey(
     this.id, {
     this.id, {
-    this.type = String,
+    required this.type,
     this.fromDb,
     this.fromDb,
     this.toDb,
     this.toDb,
-    // ignore: unused_element
-    this.fromJson,
   });
   });
   final int id;
   final int id;
   final Type type;
   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;
+}
+
+T? _getUser<T>(Isar db, int i) {
+  final User? u = db.users.getSync(i);
+  return u as T?;
 }
 }
 
 
-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);
+Future<int> _toUser<T>(Isar db, T u) {
+  if (u is User) {
+    return db.users.put(u);
+  }
+  throw TypeError();
 }
 }

+ 4 - 7
mobile/lib/shared/providers/release_info.provider.dart

@@ -1,10 +1,9 @@
 import 'dart:convert';
 import 'dart:convert';
 
 
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:hive_flutter/hive_flutter.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:http/http.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:immich_mobile/shared/views/version_announcement_overlay.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 
 
@@ -13,10 +12,10 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
   final log = Logger('ReleaseInfoNotifier');
   final log = Logger('ReleaseInfoNotifier');
   void checkGithubReleaseInfo() async {
   void checkGithubReleaseInfo() async {
     final Client client = Client();
     final Client client = Client();
-    var box = Hive.box(hiveGithubReleaseInfoBox);
 
 
     try {
     try {
-      String? localReleaseVersion = box.get(githubReleaseInfoKey);
+      final String? localReleaseVersion =
+          Store.tryGet(StoreKey.githubReleaseInfo);
       final res = await client.get(
       final res = await client.get(
         Uri.parse(
         Uri.parse(
           "https://api.github.com/repos/immich-app/immich/releases/latest",
           "https://api.github.com/repos/immich-app/immich/releases/latest",
@@ -48,9 +47,7 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
   }
   }
 
 
   void acknowledgeNewVersion() {
   void acknowledgeNewVersion() {
-    var box = Hive.box(hiveGithubReleaseInfoBox);
-
-    box.put(githubReleaseInfoKey, state);
+    Store.put(StoreKey.githubReleaseInfo, state);
     VersionAnnouncementOverlayController.appLoader.hide();
     VersionAnnouncementOverlayController.appLoader.hide();
   }
   }
 }
 }

+ 3 - 4
mobile/lib/shared/providers/websocket.provider.dart

@@ -1,11 +1,10 @@
 import 'dart:convert';
 import 'dart:convert';
 
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
-import 'package:hive/hive.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/constants/hive_box.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.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/asset.dart';
+import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
@@ -58,9 +57,9 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
     var authenticationState = ref.read(authenticationProvider);
     var authenticationState = ref.read(authenticationProvider);
 
 
     if (authenticationState.isAuthenticated) {
     if (authenticationState.isAuthenticated) {
-      var accessToken = Hive.box(userInfoBox).get(accessTokenKey);
+      final accessToken = Store.get(StoreKey.accessToken);
       try {
       try {
-        var endpoint = Uri.parse(Hive.box(userInfoBox).get(serverEndpointKey));
+        final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
 
 
         debugPrint("Attempting to connect to websocket");
         debugPrint("Attempting to connect to websocket");
         // Configure socket transports must be specified
         // Configure socket transports must be specified

+ 5 - 10
mobile/lib/shared/services/api.service.dart

@@ -1,8 +1,7 @@
 import 'dart:convert';
 import 'dart:convert';
 
 
 import 'package:flutter/material.dart';
 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:immich_mobile/utils/url_helper.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
 import 'package:http/http.dart';
 import 'package:http/http.dart';
@@ -19,13 +18,9 @@ class ApiService {
   late DeviceInfoApi deviceInfoApi;
   late DeviceInfoApi deviceInfoApi;
 
 
   ApiService() {
   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;
   String? _authToken;
@@ -49,7 +44,7 @@ class ApiService {
     setEndpoint(endpoint);
     setEndpoint(endpoint);
 
 
     // Save in hivebox for next startup
     // Save in hivebox for next startup
-    Hive.box(userInfoBox).put(serverEndpointKey, endpoint);
+    Store.put(StoreKey.serverEndpoint, endpoint);
     return endpoint;
     return endpoint;
   }
   }
 
 

+ 2 - 3
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/asset.dart';
 import 'package:immich_mobile/shared/models/exif_info.dart';
 import 'package:immich_mobile/shared/models/exif_info.dart';
 import 'package:immich_mobile/shared/models/store.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/api.provider.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
@@ -44,7 +43,7 @@ class AssetService {
         .where()
         .where()
         .remoteIdIsNotNull()
         .remoteIdIsNotNull()
         .filter()
         .filter()
-        .ownerIdEqualTo(Store.get<User>(StoreKey.currentUser)!.isarId)
+        .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
         .count();
         .count();
     final List<AssetResponseDto>? dtos =
     final List<AssetResponseDto>? dtos =
         await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0);
         await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0);
@@ -63,7 +62,7 @@ class AssetService {
     required bool hasCache,
     required bool hasCache,
   }) async {
   }) async {
     try {
     try {
-      final etag = hasCache ? Store.get(StoreKey.assetETag) : null;
+      final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null;
       final Pair<List<AssetResponseDto>, String?>? remote =
       final Pair<List<AssetResponseDto>, String?>? remote =
           await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
           await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
       if (remote == null) {
       if (remote == null) {

+ 51 - 30
mobile/lib/shared/services/immich_logger.service.dart

@@ -1,15 +1,15 @@
+import 'dart:async';
 import 'dart:io';
 import 'dart:io';
 
 
 import 'package:flutter/widgets.dart';
 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:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:share_plus/share_plus.dart';
 import 'package:share_plus/share_plus.dart';
 
 
 /// [ImmichLogger] is a custom logger that is built on top of the [logging] package.
 /// [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
 /// The logs are deleted when exceeding the `maxLogEntries` (default 200) property
 /// in the class.
 /// 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
 /// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
 /// and generate a csv file.
 /// and generate a csv file.
 class ImmichLogger {
 class ImmichLogger {
+  static final ImmichLogger _instance = ImmichLogger._internal();
   final maxLogEntries = 200;
   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();
     _removeOverflowMessages();
+    Logger.root.level = Level.INFO;
+    Logger.root.onRecord.listen(_writeLogToDatabase);
   }
   }
 
 
-  init() {
-    Logger.root.level = Level.INFO;
-    Logger.root.onRecord.listen(_writeLogToHiveBox);
+  List<LoggerMessage> get messages {
+    final inDb =
+        _db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync();
+    return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb;
   }
   }
 
 
-  _removeOverflowMessages() {
-    if (_box.length > maxLogEntries) {
-      var numberOfEntryToBeDeleted = _box.length - maxLogEntries;
-      for (var i = 0; i < numberOfEntryToBeDeleted; i++) {
-        _box.deleteAt(0);
-      }
+  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}');
     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() {
   void clearLogs() {
-    _box.clear();
+    _timer?.cancel();
+    _timer = null;
+    _msgBuffer.clear();
+    _db.writeTxn(() => _db.loggerMessages.clear());
   }
   }
 
 
   Future<void> shareLogs() async {
   Future<void> shareLogs() async {
@@ -93,4 +106,12 @@ class ImmichLogger {
     // Clean up temp file
     // Clean up temp file
     await logFile.delete();
     await logFile.delete();
   }
   }
+
+  /// Flush pending log messages to persistent storage
+  void flush() {
+    if (_timer != null) {
+      _timer!.cancel();
+      _flushBufferToDatabase();
+    }
+  }
 }
 }

+ 1 - 1
mobile/lib/shared/services/sync.service.dart

@@ -241,7 +241,7 @@ class SyncService {
     }
     }
 
 
     if (album.shared || dto.shared) {
     if (album.shared || dto.shared) {
-      final userId = Store.get<User>(StoreKey.currentUser)!.isarId;
+      final userId = Store.get(StoreKey.currentUser).isarId;
       final foreign =
       final foreign =
           await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
           await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
       existing.addAll(foreign);
       existing.addAll(foreign);

+ 1 - 1
mobile/lib/shared/services/user.service.dart

@@ -42,7 +42,7 @@ class UserService {
     if (self) {
     if (self) {
       return _db.users.where().findAll();
       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();
     return _db.users.where().isarIdNotEqualTo(userId).findAll();
   }
   }
 
 

+ 2 - 3
mobile/lib/shared/ui/immich_image.dart

@@ -1,9 +1,8 @@
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
-import 'package: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/asset.dart';
+import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 import 'package:photo_manager/photo_manager.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);
     final String thumbnailRequestUrl = getThumbnailUrl(asset);
     return CachedNetworkImage(
     return CachedNetworkImage(
       imageUrl: thumbnailRequestUrl,
       imageUrl: thumbnailRequestUrl,

+ 9 - 8
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/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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:immich_mobile/shared/services/immich_logger.service.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
 
 
@@ -31,29 +32,29 @@ class AppLogPage extends HookConsumerWidget {
       );
       );
     }
     }
 
 
-    Widget buildLeadingIcon(String level) {
+    Widget buildLeadingIcon(LogLevel level) {
       switch (level) {
       switch (level) {
-        case "INFO":
+        case LogLevel.INFO:
           return colorStatusIndicator(Theme.of(context).primaryColor);
           return colorStatusIndicator(Theme.of(context).primaryColor);
-        case "SEVERE":
+        case LogLevel.SEVERE:
           return colorStatusIndicator(Colors.redAccent);
           return colorStatusIndicator(Colors.redAccent);
 
 
-        case "WARNING":
+        case LogLevel.WARNING:
           return colorStatusIndicator(Colors.orangeAccent);
           return colorStatusIndicator(Colors.orangeAccent);
         default:
         default:
           return colorStatusIndicator(Colors.grey);
           return colorStatusIndicator(Colors.grey);
       }
       }
     }
     }
 
 
-    getTileColor(String level) {
+    getTileColor(LogLevel level) {
       switch (level) {
       switch (level) {
-        case "INFO":
+        case LogLevel.INFO:
           return Colors.transparent;
           return Colors.transparent;
-        case "SEVERE":
+        case LogLevel.SEVERE:
           return Theme.of(context).brightness == Brightness.dark
           return Theme.of(context).brightness == Brightness.dark
               ? Colors.redAccent.withOpacity(0.25)
               ? Colors.redAccent.withOpacity(0.25)
               : Colors.redAccent.withOpacity(0.075);
               : Colors.redAccent.withOpacity(0.075);
-        case "WARNING":
+        case LogLevel.WARNING:
           return Theme.of(context).brightness == Brightness.dark
           return Theme.of(context).brightness == Brightness.dark
               ? Colors.orangeAccent.withOpacity(0.25)
               ? Colors.orangeAccent.withOpacity(0.25)
               : Colors.orangeAccent.withOpacity(0.075);
               : Colors.orangeAccent.withOpacity(0.075);

+ 9 - 11
mobile/lib/shared/views/splash_screen.dart

@@ -1,14 +1,12 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.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: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/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/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/routing/router.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/api.provider.dart';
 
 
 class SplashScreenPage extends HookConsumerWidget {
 class SplashScreenPage extends HookConsumerWidget {
@@ -17,23 +15,23 @@ class SplashScreenPage extends HookConsumerWidget {
   @override
   @override
   Widget build(BuildContext context, WidgetRef ref) {
   Widget build(BuildContext context, WidgetRef ref) {
     final apiService = ref.watch(apiServiceProvider);
     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 {
     void performLoggingIn() async {
       bool isSuccess = false;
       bool isSuccess = false;
-      if (loginInfo != null) {
+      if (accessToken != null && serverUrl != null) {
         try {
         try {
           // Resolve API server endpoint from user provided serverUrl
           // Resolve API server endpoint from user provided serverUrl
-          await apiService.resolveAndSetEndpoint(loginInfo.serverUrl);
+          await apiService.resolveAndSetEndpoint(serverUrl);
         } catch (e) {
         } catch (e) {
           // okay, try to continue anyway if offline
           // okay, try to continue anyway if offline
         }
         }
 
 
         isSuccess =
         isSuccess =
             await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
             await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
-                  accessToken: loginInfo.accessToken,
-                  serverUrl: loginInfo.serverUrl,
+                  accessToken: accessToken,
+                  serverUrl: serverUrl,
                 );
                 );
       }
       }
       if (isSuccess) {
       if (isSuccess) {
@@ -51,7 +49,7 @@ class SplashScreenPage extends HookConsumerWidget {
 
 
     useEffect(
     useEffect(
       () {
       () {
-        if (loginInfo != null) {
+        if (serverUrl != null && accessToken != null) {
           performLoggingIn();
           performLoggingIn();
         } else {
         } else {
           AutoRouter.of(context).replace(const LoginRoute());
           AutoRouter.of(context).replace(const LoginRoute());

+ 3 - 8
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/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/models/store.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
 
 
-import '../constants/hive_box.dart';
-
 String getThumbnailUrl(
 String getThumbnailUrl(
   final Asset asset, {
   final Asset asset, {
   ThumbnailFormat type = ThumbnailFormat.WEBP,
   ThumbnailFormat type = ThumbnailFormat.WEBP,
@@ -48,8 +46,7 @@ String getAlbumThumbNailCacheKey(
 }
 }
 
 
 String getImageUrl(final Asset asset) {
 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) {
 String getImageCacheKey(final Asset asset) {
@@ -60,7 +57,5 @@ String _getThumbnailUrl(
   final String id, {
   final String id, {
   ThumbnailFormat type = ThumbnailFormat.WEBP,
   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}';
 }
 }

+ 56 - 25
mobile/lib/utils/migration.dart

@@ -1,5 +1,7 @@
 // ignore_for_file: deprecated_member_use_from_same_package
 // ignore_for_file: deprecated_member_use_from_same_package
 
 
+import 'dart:async';
+
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:hive/hive.dart';
 import 'package:hive/hive.dart';
 import 'package:immich_mobile/constants/hive_box.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/duplicated_asset.model.dart';
 import 'package:immich_mobile/modules/backup/models/hive_backup_albums.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/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/models/store.dart';
 import 'package:immich_mobile/shared/services/asset_cache.service.dart';
 import 'package:immich_mobile/shared/services/asset_cache.service.dart';
 import 'package:isar/isar.dart';
 import 'package:isar/isar.dart';
@@ -23,11 +28,37 @@ Future<void> migrateHiveToStoreIfNecessary() async {
     duplicatedAssetsBox,
     duplicatedAssetsBox,
     _migrateDuplicatedAssetsBox,
     _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 {
 Future<void> _migrateHiveUserInfoBox(Box box) async {
   await _migrateKey(box, userIdKey, StoreKey.userRemoteId);
   await _migrateKey(box, userIdKey, StoreKey.userRemoteId);
   await _migrateKey(box, assetEtagKey, StoreKey.assetETag);
   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 {
 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, backupRequireWifi, StoreKey.backupRequireWifi);
   await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging);
   await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging);
   await _migrateKey(box, backupTriggerDelay, StoreKey.backupTriggerDelay);
   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);
   final HiveBackupAlbums? infos = box.get(backupInfoKey);
   if (infos != null) {
   if (infos != null) {
+    final Isar? db = Isar.getInstance();
+    if (db == null) {
+      throw Exception("_migrateBackupInfoBox could not load database");
+    }
     List<BackupAlbum> albums = [];
     List<BackupAlbum> albums = [];
     for (int i = 0; i < infos.selectedAlbumIds.length; i++) {
     for (int i = 0; i < infos.selectedAlbumIds.length; i++) {
       final album = BackupAlbum(
       final album = BackupAlbum(
@@ -62,48 +92,49 @@ Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async {
       );
       );
       albums.add(album);
       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);
   final HiveDuplicatedAssets? duplicatedAssets = box.get(duplicatedAssetsKey);
   if (duplicatedAssets != null) {
   if (duplicatedAssets != null) {
+    final Isar? db = Isar.getInstance();
+    if (db == null) {
+      throw Exception("_migrateBackupInfoBox could not load database");
+    }
     final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds
     final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds
         .map((id) => DuplicatedAsset(id))
         .map((id) => DuplicatedAsset(id))
         .toList();
         .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>(
 Future<void> _migrateHiveBoxIfNecessary<T>(
   String boxName,
   String boxName,
-  Future<void> Function(Box<T>) migrate,
+  FutureOr<void> Function(Box<T>) migrate,
 ) async {
 ) async {
   try {
   try {
     if (await Hive.boxExists(boxName)) {
     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) {
   } catch (e) {
     debugPrint("Error while migrating $boxName $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) {
   if (value != null) {
-    await Store.put(key, value);
-    await box.delete(hiveKey);
+    return Store.put(key, value);
   }
   }
 }
 }