diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index 6149c7d160..1f4f3e2605 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -1,4 +1,5 @@ import 'package:immich_mobile/domain/models/config/album_config.dart'; +import 'package:immich_mobile/domain/models/config/backup_config.dart'; import 'package:immich_mobile/domain/models/config/cleanup_config.dart'; import 'package:immich_mobile/domain/models/config/image_config.dart'; import 'package:immich_mobile/domain/models/config/map_config.dart'; @@ -14,6 +15,7 @@ class AppConfig { final ImageConfig image; final ViewerConfig viewer; final AlbumConfig album; + final BackupConfig backup; const AppConfig({ this.theme = const .new(), @@ -23,6 +25,7 @@ class AppConfig { this.image = const .new(), this.viewer = const .new(), this.album = const .new(), + this.backup = const .new(), }); AppConfig copyWith({ @@ -33,6 +36,7 @@ class AppConfig { ImageConfig? image, ViewerConfig? viewer, AlbumConfig? album, + BackupConfig? backup, }) => .new( theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, @@ -41,6 +45,7 @@ class AppConfig { image: image ?? this.image, viewer: viewer ?? this.viewer, album: album ?? this.album, + backup: backup ?? this.backup, ); @override @@ -53,12 +58,13 @@ class AppConfig { other.timeline == timeline && other.image == image && other.viewer == viewer && - other.album == album); + other.album == album && + other.backup == backup); @override - int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, album); + int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, album, backup); @override String toString() => - 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, album: $album)'; + 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, album: $album, backup: $backup)'; } diff --git a/mobile/lib/domain/models/config/backup_config.dart b/mobile/lib/domain/models/config/backup_config.dart new file mode 100644 index 0000000000..19f91a4ed7 --- /dev/null +++ b/mobile/lib/domain/models/config/backup_config.dart @@ -0,0 +1,52 @@ +class BackupConfig { + final bool enabled; + final bool useCellularForVideos; + final bool useCellularForPhotos; + final bool requireCharging; + final int triggerDelay; + final bool syncAlbums; + + const BackupConfig({ + this.enabled = false, + this.useCellularForVideos = false, + this.useCellularForPhotos = false, + this.requireCharging = false, + this.triggerDelay = 30, + this.syncAlbums = false, + }); + + BackupConfig copyWith({ + bool? enabled, + bool? useCellularForVideos, + bool? useCellularForPhotos, + bool? requireCharging, + int? triggerDelay, + bool? syncAlbums, + }) => BackupConfig( + enabled: enabled ?? this.enabled, + useCellularForVideos: useCellularForVideos ?? this.useCellularForVideos, + useCellularForPhotos: useCellularForPhotos ?? this.useCellularForPhotos, + requireCharging: requireCharging ?? this.requireCharging, + triggerDelay: triggerDelay ?? this.triggerDelay, + syncAlbums: syncAlbums ?? this.syncAlbums, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BackupConfig && + other.enabled == enabled && + other.useCellularForVideos == useCellularForVideos && + other.useCellularForPhotos == useCellularForPhotos && + other.requireCharging == requireCharging && + other.triggerDelay == triggerDelay && + other.syncAlbums == syncAlbums); + + @override + int get hashCode => + Object.hash(enabled, useCellularForVideos, useCellularForPhotos, requireCharging, triggerDelay, syncAlbums); + + @override + String toString() => + 'BackupConfig(enabled: $enabled, useCellularForVideos: $useCellularForVideos, useCellularForPhotos: $useCellularForPhotos, requireCharging: $requireCharging, triggerDelay: $triggerDelay, syncAlbums: $syncAlbums)'; +} diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 777c2f3e19..3ad553a17d 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -62,6 +62,14 @@ enum MetadataKey { albumIsReverse(.appConfig, 'album.isReverse', true), albumIsGrid(.appConfig, 'album.isGrid', false), + // Backup + backupEnabled(.appConfig, 'backup.enabled', false), + backupUseCellularForVideos(.appConfig, 'backup.useCellularForVideos', false), + backupUseCellularForPhotos(.appConfig, 'backup.useCellularForPhotos', false), + backupRequireCharging(.appConfig, 'backup.requireCharging', false), + backupTriggerDelay(.appConfig, 'backup.triggerDelay', 30), + backupSyncAlbums(.appConfig, 'backup.syncAlbums', false), + // Timeline timelineTilesPerRow(.appConfig, 'timeline.tilesPerRow', 4), timelineGroupAssetsBy( diff --git a/mobile/lib/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart index 0dc48de3b1..d6d9e2902b 100644 --- a/mobile/lib/domain/models/setting.model.dart +++ b/mobile/lib/domain/models/setting.model.dart @@ -1,8 +1,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; enum Setting { - advancedTroubleshooting(StoreKey.advancedTroubleshooting, false), - enableBackup(StoreKey.enableBackup, false); + advancedTroubleshooting(StoreKey.advancedTroubleshooting, false); const Setting(this.storeKey, this.defaultValue); diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 6de2ee26f5..398e77445a 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -6,26 +6,25 @@ enum StoreKey { version._(0), currentUser._(2), deviceId._(4), - backupRequireCharging._(7), - backupTriggerDelay._(8), serverUrl._(10), accessToken._(11), serverEndpoint._(12), advancedTroubleshooting._(114), enableHapticFeedback._(126), - syncAlbums._(131), manageLocalMediaAndroid._(137), // Read-only Mode settings readonlyModeEnabled._(138), - // Experimental stuff - enableBackup._(1003), - useWifiForUploadVideos._(1004), - useWifiForUploadPhotos._(1005), syncMigrationStatus._(1013), // Legacy keys that have been migrated to the new metadata store + legacyBackupRequireCharging._(7), + legacyBackupTriggerDelay._(8), + legacySyncAlbums._(131), + legacyEnableBackup._(1003), + legacyUseWifiForUploadVideos._(1004), + legacyUseWifiForUploadPhotos._(1005), legacySelectedAlbumSortOrder._(113), legacySelectedAlbumSortReverse._(123), legacyAlbumGridView._(140), diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index d4da3e31a4..bb84d7957f 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -11,15 +11,14 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; @@ -39,16 +38,15 @@ class BackgroundWorkerFgService { Future saveNotificationMessage(String title, String body) => _foregroundHostApi.saveNotificationMessage(title, body); - Future configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure( - BackgroundWorkerSettings( - minimumDelaySeconds: - minimumDelaySeconds ?? - Store.get(AppSettingsEnum.backupTriggerDelay.storeKey, AppSettingsEnum.backupTriggerDelay.defaultValue), - requiresCharging: - requireCharging ?? - Store.get(AppSettingsEnum.backupRequireCharging.storeKey, AppSettingsEnum.backupRequireCharging.defaultValue), - ), - ); + Future configure({int? minimumDelaySeconds, bool? requireCharging}) { + final backup = MetadataRepository.instance.appConfig.backup; + return _foregroundHostApi.configure( + BackgroundWorkerSettings( + minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay, + requiresCharging: requireCharging ?? backup.requireCharging, + ), + ); + } Future disable() => _foregroundHostApi.disable(); } @@ -71,7 +69,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { BackgroundWorkerFlutterApi.setUp(this); } - bool get _isBackupEnabled => _ref?.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup) ?? false; + bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled; Future init() async { try { diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index db368e9286..761e8f5515 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -139,6 +139,14 @@ extension on MetadataDomain { isReverse: repo._read(.albumIsReverse), isGrid: repo._read(.albumIsGrid), ), + backup: .new( + enabled: repo._read(.backupEnabled), + useCellularForVideos: repo._read(.backupUseCellularForVideos), + useCellularForPhotos: repo._read(.backupUseCellularForPhotos), + requireCharging: repo._read(.backupRequireCharging), + triggerDelay: repo._read(.backupTriggerDelay), + syncAlbums: repo._read(.backupSyncAlbums), + ), ); case .systemConfig: repo._systemConfig = .new( diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index 1732385675..ff63365136 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -8,13 +8,13 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:logging/logging.dart'; @@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); @@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState a.backupSelection == BackupSelection.selected) @@ -101,7 +101,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index 79891d7002..4e8a185955 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -3,14 +3,12 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:logging/logging.dart'; @@ -21,18 +19,20 @@ class DriftBackupOptionsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { bool hasPopped = false; - final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; - final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; + final previousBackup = ref.read(metadataProvider).appConfig.backup; + final previousCellularForVideos = previousBackup.useCellularForVideos; + final previousCellularForPhotos = previousBackup.useCellularForPhotos; return PopScope( onPopInvokedWithResult: (didPop, result) async { // There is an issue with Flutter where the pop event // can be triggered multiple times, so we guard it with _hasPopped - final currentWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; - final currentWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; + final currentBackup = ref.read(metadataProvider).appConfig.backup; + final currentCellularForVideos = currentBackup.useCellularForVideos; + final currentCellularForPhotos = currentBackup.useCellularForPhotos; - if (currentWifiReqForVideos == previousWifiReqForVideos && - currentWifiReqForPhotos == previousWifiReqForPhotos) { + if (currentCellularForVideos == previousCellularForVideos && + currentCellularForPhotos == previousCellularForPhotos) { return; } @@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { } await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); - final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled; if (!isBackupEnabled) { return; } diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index 6bb9df4b00..06e4e89d76 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -91,8 +91,9 @@ class HeaderSettingsPage extends HookConsumerWidget { headersMap[key] = value; } + final apiService = ref.read(apiServiceProvider); await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap); - await ref.read(apiServiceProvider).updateHeaders(); + await apiService.updateHeaders(); } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 594f47999c..7b49d98307 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; @@ -340,7 +341,7 @@ class SplashScreenPageState extends ConsumerState { await backgroundManager.hashAssets(); } - if (Store.get(StoreKey.syncAlbums, false)) { + if (MetadataRepository.instance.appConfig.backup.syncAlbums) { await backgroundManager.syncLinkedAlbum(); } } catch (e) { @@ -369,7 +370,7 @@ class SplashScreenPageState extends ConsumerState { } Future _resumeBackup(DriftBackupNotifier notifier) async { - final isEnableBackup = Store.get(StoreKey.enableBackup, false); + final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled; if (isEnableBackup) { final currentUser = Store.tryGet(StoreKey.currentUser); diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index 7c92dc01d8..708d3a9879 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; class BackupToggleButton extends ConsumerStatefulWidget { final VoidCallback onStart; @@ -31,7 +31,7 @@ class BackupToggleButtonState extends ConsumerState with Sin end: 1, ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); - _isEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + _isEnabled = ref.read(metadataProvider).appConfig.backup.enabled; } @override @@ -41,7 +41,7 @@ class BackupToggleButtonState extends ConsumerState with Sin } Future _onToggle(bool value) async { - await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.enableBackup, value); + await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value); setState(() { _isEnabled = value; diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index a5f67215a8..e162044fde 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -5,16 +5,15 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:logging/logging.dart'; enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden } @@ -106,7 +105,7 @@ class AppLifeCycleNotifier extends StateNotifier { await Future.delayed(const Duration(milliseconds: 500)); final backgroundManager = _ref.read(backgroundSyncProvider); - final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); + final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums; try { bool syncSuccess = false; @@ -136,7 +135,7 @@ class AppLifeCycleNotifier extends StateNotifier { } Future _resumeBackup() async { - final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); + final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled; if (isEnableBackup) { final currentUser = Store.tryGet(StoreKey.currentUser); diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index ae97909349..2c69dbb9ed 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -130,7 +131,7 @@ class AuthNotifier extends StateNotifier { await _apiService.updateHeaders(); final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headerMap = _ref.read(metadataProvider).systemConfig.network.customHeaders; + final headerMap = MetadataRepository.instance.systemConfig.network.customHeaders; final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap); await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 60afcec2d2..f9166e6f16 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -188,7 +189,7 @@ class WebsocketNotifier extends StateNotifier { return; } - final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false); + final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; try { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { @@ -209,7 +210,7 @@ class WebsocketNotifier extends StateNotifier { return; } - final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false); + final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; try { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) { diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 31f631d86f..28bce32bb5 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -5,13 +5,7 @@ enum AppSettingsEnum { advancedTroubleshooting(StoreKey.advancedTroubleshooting, null, false), manageLocalMediaAndroid(StoreKey.manageLocalMediaAndroid, null, false), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), - syncAlbums(StoreKey.syncAlbums, null, false), - enableBackup(StoreKey.enableBackup, null, false), - useCellularForUploadVideos(StoreKey.useWifiForUploadVideos, null, false), - useCellularForUploadPhotos(StoreKey.useWifiForUploadPhotos, null, false), - readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false), - backupRequireCharging(StoreKey.backupRequireCharging, null, false), - backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30); + readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 14f67972fa..7d470ecd7a 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -1,19 +1,19 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/network.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -25,7 +25,6 @@ final authServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(networkServiceProvider), ref.watch(backgroundSyncProvider), - ref.watch(appSettingsServiceProvider), ), ); @@ -35,7 +34,6 @@ class AuthService { final ApiService _apiService; final NetworkService _networkService; final BackgroundSyncManager _backgroundSyncManager; - final AppSettingsService _appSettingsService; final _log = Logger("AuthService"); AuthService( @@ -44,7 +42,6 @@ class AuthService { this._apiService, this._networkService, this._backgroundSyncManager, - this._appSettingsService, ); /// Validates the provided server URL by resolving and setting the endpoint. @@ -103,7 +100,7 @@ class AuthService { _log.severe("Error clearing local data", error, stackTrace); }); - await _appSettingsService.setSetting(AppSettingsEnum.enableBackup, false); + await MetadataRepository.instance.write(MetadataKey.backupEnabled, false); } } diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index d54a677c24..10615f7c03 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -13,14 +13,13 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; @@ -31,7 +30,6 @@ final backgroundUploadServiceProvider = Provider((ref) { ref.watch(storageRepositoryProvider), ref.watch(localAssetRepository), ref.watch(backupRepositoryProvider), - ref.watch(appSettingsServiceProvider), ref.watch(assetMediaRepositoryProvider), ); @@ -103,7 +101,6 @@ class BackgroundUploadService { this._storageRepository, this._localAssetRepository, this._backupRepository, - this._appSettingsService, this._assetMediaRepository, ) { _uploadRepository.onUploadStatus = _onUploadCallback; @@ -114,7 +111,6 @@ class BackgroundUploadService { final StorageRepository _storageRepository; final DriftLocalAssetRepository _localAssetRepository; final DriftBackupRepository _backupRepository; - final AppSettingsService _appSettingsService; final AssetMediaRepository _assetMediaRepository; final Logger _logger = Logger('BackgroundUploadService'); @@ -361,15 +357,10 @@ class BackgroundUploadService { } bool _shouldRequireWiFi(LocalAsset asset) { - bool requiresWiFi = true; - - if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) { - requiresWiFi = false; - } else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) { - requiresWiFi = false; - } - - return requiresWiFi; + final backup = MetadataRepository.instance.appConfig.backup; + if (asset.isVideo && backup.useCellularForVideos) return false; + if (!asset.isVideo && backup.useCellularForPhotos) return false; + return true; } Future buildUploadTask( diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index ce02c9c56b..5a79cf8f93 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -11,14 +11,13 @@ import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; @@ -39,7 +38,6 @@ final foregroundUploadServiceProvider = Provider((ref) { ref.watch(storageRepositoryProvider), ref.watch(backupRepositoryProvider), ref.watch(connectivityApiProvider), - ref.watch(appSettingsServiceProvider), ref.watch(assetMediaRepositoryProvider), ); }); @@ -55,7 +53,6 @@ class ForegroundUploadService { this._storageRepository, this._backupRepository, this._connectivityApi, - this._appSettingsService, this._assetMediaRepository, ); @@ -63,7 +60,6 @@ class ForegroundUploadService { final StorageRepository _storageRepository; final DriftBackupRepository _backupRepository; final ConnectivityApi _connectivityApi; - final AppSettingsService _appSettingsService; final AssetMediaRepository _assetMediaRepository; final Logger _logger = Logger('ForegroundUploadService'); @@ -453,14 +449,9 @@ class ForegroundUploadService { } bool _shouldRequireWiFi(LocalAsset asset) { - bool requiresWiFi = true; - - if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) { - requiresWiFi = false; - } else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) { - requiresWiFi = false; - } - - return requiresWiFi; + final backup = MetadataRepository.instance.appConfig.backup; + if (asset.isVideo && backup.useCellularForVideos) return false; + if (!asset.isVideo && backup.useCellularForPhotos) return false; + return true; } } diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index c44f5a401c..5c902b56e7 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -118,6 +118,13 @@ Future _migrateTo26(Drift drift) async { await _migrateAlbumSortMode(migrator); await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse); await migrator.migrateBool(StoreKey.legacyAlbumGridView, MetadataKey.albumIsGrid); + // Backup + await migrator.migrateBool(StoreKey.legacyEnableBackup, MetadataKey.backupEnabled); + await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, MetadataKey.backupUseCellularForVideos); + await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, MetadataKey.backupUseCellularForPhotos); + await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, MetadataKey.backupRequireCharging); + await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, MetadataKey.backupTriggerDelay); + await migrator.migrateBool(StoreKey.legacySyncAlbums, MetadataKey.backupSyncAlbums); await migrator.complete(); } diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index cb429c9f48..27cabe04e2 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -6,13 +6,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -193,18 +192,14 @@ class _BackupIndicator extends ConsumerWidget { } Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) { - final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup); + final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled)); final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none)); final isDarkTheme = context.isDarkTheme; final iconColor = isDarkTheme ? Colors.white : Colors.black; final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); - return StreamBuilder( - stream: backupStateStream, - initialData: false, - builder: (ctx, snapshot) { - final backupEnabled = snapshot.data ?? false; - + return Builder( + builder: (ctx) { if (!backupEnabled) { return _BadgeLabel( Icon( diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index fb3b9c5977..f8d0172dbd 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -15,6 +15,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; @@ -178,7 +179,7 @@ class LoginForm extends HookConsumerWidget { await backgroundManager.syncRemote(); await backgroundManager.hashAssets(); - if (Store.get(StoreKey.syncAlbums, false)) { + if (MetadataRepository.instance.appConfig.backup.syncAlbums) { await backgroundManager.syncLinkedAlbum(); } } diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index 2c179c42ea..cdce5a856e 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -4,18 +4,17 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; +import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -31,8 +30,8 @@ class DriftBackupSettings extends ConsumerWidget { title: "network_requirements".t(context: context), icon: Icons.cell_tower, ), - const _UseWifiForUploadVideosButton(), - const _UseWifiForUploadPhotosButton(), + const _UseCellularForVideosButton(), + const _UseCellularForPhotosButton(), if (CurrentPlatform.isAndroid) ...[ const Divider(), SettingGroupTitle( @@ -96,64 +95,58 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> @override Widget build(BuildContext context) { + final albumSyncEnable = ref.watch(appConfigProvider.select((c) => c.backup.syncAlbums)); return Padding( padding: const EdgeInsets.only(left: 8.0), child: ListView( shrinkWrap: true, children: [ - StreamBuilder( - stream: Store.watch(StoreKey.syncAlbums), - initialData: Store.tryGet(StoreKey.syncAlbums) ?? false, - builder: (context, snapshot) { - final albumSyncEnable = snapshot.data ?? false; - return Column( - children: [ - SettingListTile( - title: "sync_albums".t(context: context), - subtitle: "sync_upload_album_setting_subtitle".t(context: context), - trailing: Switch( - value: albumSyncEnable, - onChanged: (bool newValue) async { - await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue); + Column( + children: [ + SettingListTile( + title: "sync_albums".t(context: context), + subtitle: "sync_upload_album_setting_subtitle".t(context: context), + trailing: Switch( + value: albumSyncEnable, + onChanged: (bool newValue) async { + await ref.read(metadataProvider).write(MetadataKey.backupSyncAlbums, newValue); - if (newValue == true) { - await _manageLinkedAlbums(); - } - }, - ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: albumSyncEnable ? 1.0 : 0.0, - child: albumSyncEnable - ? SettingListTile( - onTap: _manualSyncAlbums, - contentPadding: const EdgeInsets.only(left: 32, right: 16), - title: "organize_into_albums".t(context: context), - subtitle: "organize_into_albums_description".t(context: context), - trailing: isAlbumSyncInProgress - ? const SizedBox( - width: 32, - height: 32, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ) - : IconButton( - onPressed: _manualSyncAlbums, - icon: const Icon(Icons.sync_rounded), - color: context.colorScheme.onSurface.withValues(alpha: 0.7), - iconSize: 20, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), - ), - ) - : const SizedBox.shrink(), - ), - ), - ], - ); - }, + if (newValue == true) { + await _manageLinkedAlbums(); + } + }, + ), + ), + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: albumSyncEnable ? 1.0 : 0.0, + child: albumSyncEnable + ? SettingListTile( + onTap: _manualSyncAlbums, + contentPadding: const EdgeInsets.only(left: 32, right: 16), + title: "organize_into_albums".t(context: context), + subtitle: "organize_into_albums_description".t(context: context), + trailing: isAlbumSyncInProgress + ? const SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ) + : IconButton( + onPressed: _manualSyncAlbums, + icon: const Icon(Icons.sync_rounded), + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + iconSize: 20, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + ), + ) + : const SizedBox.shrink(), + ), + ), + ], ), ], ), @@ -161,60 +154,34 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> } } -class _SettingsSwitchTile extends ConsumerStatefulWidget { - final AppSettingsEnum appSettingsEnum; +class _BackupSwitchTile extends ConsumerWidget { + final MetadataKey metadataKey; + final bool Function(AppConfig) selector; final String titleKey; final String subtitleKey; - final void Function(bool?)? onChanged; + final void Function(bool)? onChanged; - const _SettingsSwitchTile({ - required this.appSettingsEnum, + const _BackupSwitchTile({ + required this.metadataKey, + required this.selector, required this.titleKey, required this.subtitleKey, this.onChanged, }); @override - ConsumerState createState() => _SettingsSwitchTileState(); -} - -class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> { - late final Stream valueStream; - late final StreamSubscription subscription; - - @override - void initState() { - super.initState(); - valueStream = Store.watch(widget.appSettingsEnum.storeKey).asBroadcastStream(); - subscription = valueStream.listen((value) { - widget.onChanged?.call(value); - }); - } - - @override - void dispose() { - subscription.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.watch(appConfigProvider.select(selector)); return Padding( padding: const EdgeInsets.only(left: 8.0), child: SettingListTile( - title: widget.titleKey.t(context: context), - subtitle: widget.subtitleKey.t(context: context), - trailing: StreamBuilder( - stream: valueStream, - initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue, - builder: (context, snapshot) { - final value = snapshot.data ?? false; - return Switch( - value: value, - onChanged: (bool newValue) async { - await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue); - }, - ); + title: titleKey.t(context: context), + subtitle: subtitleKey.t(context: context), + trailing: Switch( + value: value, + onChanged: (bool newValue) async { + await ref.read(metadataProvider).write(metadataKey, newValue); + onChanged?.call(newValue); }, ), ), @@ -222,26 +189,28 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> { } } -class _UseWifiForUploadVideosButton extends ConsumerWidget { - const _UseWifiForUploadVideosButton(); +class _UseCellularForVideosButton extends StatelessWidget { + const _UseCellularForVideosButton(); @override - Widget build(BuildContext context, WidgetRef ref) { - return const _SettingsSwitchTile( - appSettingsEnum: AppSettingsEnum.useCellularForUploadVideos, + Widget build(BuildContext context) { + return _BackupSwitchTile( + metadataKey: MetadataKey.backupUseCellularForVideos, + selector: (c) => c.backup.useCellularForVideos, titleKey: "videos", subtitleKey: "network_requirement_videos_upload", ); } } -class _UseWifiForUploadPhotosButton extends ConsumerWidget { - const _UseWifiForUploadPhotosButton(); +class _UseCellularForPhotosButton extends StatelessWidget { + const _UseCellularForPhotosButton(); @override - Widget build(BuildContext context, WidgetRef ref) { - return const _SettingsSwitchTile( - appSettingsEnum: AppSettingsEnum.useCellularForUploadPhotos, + Widget build(BuildContext context) { + return _BackupSwitchTile( + metadataKey: MetadataKey.backupUseCellularForPhotos, + selector: (c) => c.backup.useCellularForPhotos, titleKey: "photos", subtitleKey: "network_requirement_photos_upload", ); @@ -253,29 +222,22 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return _SettingsSwitchTile( - appSettingsEnum: AppSettingsEnum.backupRequireCharging, + final fgService = ref.read(backgroundWorkerFgServiceProvider); + return _BackupSwitchTile( + metadataKey: MetadataKey.backupRequireCharging, + selector: (c) => c.backup.requireCharging, titleKey: "charging", subtitleKey: "charging_requirement_mobile_backup", onChanged: (value) { - ref.read(backgroundWorkerFgServiceProvider).configure(requireCharging: value ?? false); + fgService.configure(requireCharging: value); }, ); } } -class _BackupDelaySlider extends ConsumerStatefulWidget { +class _BackupDelaySlider extends ConsumerWidget { const _BackupDelaySlider(); - @override - ConsumerState<_BackupDelaySlider> createState() => _BackupDelaySliderState(); -} - -class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> { - late final Stream valueStream; - late final StreamSubscription subscription; - late int currentValue; - static int backupDelayToSliderValue(int ms) => switch (ms) { 5 => 0, 30 => 1, @@ -298,30 +260,9 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> { }; @override - void initState() { - super.initState(); - final initialValue = - Store.tryGet(AppSettingsEnum.backupTriggerDelay.storeKey) ?? AppSettingsEnum.backupTriggerDelay.defaultValue; - currentValue = backupDelayToSliderValue(initialValue); - - valueStream = Store.watch(AppSettingsEnum.backupTriggerDelay.storeKey).asBroadcastStream(); - subscription = valueStream.listen((value) { - if (mounted && value != null) { - setState(() { - currentValue = backupDelayToSliderValue(value); - }); - } - }); - } - - @override - void dispose() { - subscription.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final triggerDelay = ref.watch(appConfigProvider.select((c) => c.backup.triggerDelay)); + final currentValue = backupDelayToSliderValue(triggerDelay); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -336,14 +277,13 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> { ), Slider( value: currentValue.toDouble(), - onChanged: (double v) { - setState(() { - currentValue = v.toInt(); - }); + onChanged: (double v) async { + final seconds = backupDelayToSeconds(v.toInt()); + await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds); }, onChangeEnd: (double v) async { - final milliseconds = backupDelayToSeconds(v.toInt()); - await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.backupTriggerDelay, milliseconds); + final seconds = backupDelayToSeconds(v.toInt()); + await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds); }, max: 3.0, min: 0.0, diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 0a55f8d5c7..bb439b3d72 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -9,7 +9,7 @@ import 'package:mocktail/mocktail.dart'; import '../../infrastructure/repository.mock.dart'; const _kAccessToken = '#ThisIsAToken'; -const _kEnableBackup = false; +const _kAdvancedTroubleshooting = false; const _kVersion = 2; void main() { @@ -22,13 +22,13 @@ void main() { mockDriftStoreRepo = MockDriftStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); - registerFallbackValue(StoreKey.backupTriggerDelay); - registerFallbackValue(StoreKey.enableBackup); + registerFallbackValue(StoreKey.version); + registerFallbackValue(StoreKey.advancedTroubleshooting); when(() => mockDriftStoreRepo.getAll()).thenAnswer( (_) async => [ const StoreDto(StoreKey.accessToken, _kAccessToken), - const StoreDto(StoreKey.enableBackup, _kEnableBackup), + const StoreDto(StoreKey.advancedTroubleshooting, _kAdvancedTroubleshooting), const StoreDto(StoreKey.version, _kVersion), ], ); @@ -46,7 +46,7 @@ void main() { test('Populates the internal cache on init', () { verify(() => mockDriftStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); - expect(sut.tryGet(StoreKey.enableBackup), _kEnableBackup); + expect(sut.tryGet(StoreKey.advancedTroubleshooting), _kAdvancedTroubleshooting); expect(sut.tryGet(StoreKey.version), _kVersion); // Other keys should be null expect(sut.tryGet(StoreKey.currentUser), isNull); @@ -147,7 +147,7 @@ void main() { await sut.clear(); verify(() => mockDriftStoreRepo.deleteAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), isNull); - expect(sut.tryGet(StoreKey.enableBackup), isNull); + expect(sut.tryGet(StoreKey.advancedTroubleshooting), isNull); expect(sut.tryGet(StoreKey.version), isNull); }); }); diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 672776b226..3e160c29ca 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -13,7 +13,7 @@ import '../../fixtures/user.stub.dart'; const _kTestAccessToken = "#TestToken"; const _kTestVersion = 10; -const _kTestBackupRequireCharging = false; +const _kTestAdvancedTroubleshooting = false; final _kTestUser = UserStub.admin; Future _populateStore(Drift db) async { @@ -21,8 +21,8 @@ Future _populateStore(Drift db) async { batch.insert( db.storeEntity, StoreEntityCompanion( - id: Value(StoreKey.backupRequireCharging.id), - intValue: const Value(_kTestBackupRequireCharging ? 1 : 0), + id: Value(StoreKey.advancedTroubleshooting.id), + intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0), stringValue: const Value(null), ), ); @@ -76,11 +76,11 @@ void main() { }); test('converts bool', () async { - bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); - expect(backupRequireCharging, isNull); - await sut.upsert(StoreKey.backupRequireCharging, _kTestBackupRequireCharging); - backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); - expect(backupRequireCharging, _kTestBackupRequireCharging); + bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); + expect(advancedTroubleshooting, isNull); + await sut.upsert(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting); + advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); + expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting); }); test('converts user', () async { @@ -98,11 +98,11 @@ void main() { }); test('delete()', () async { - bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); - expect(backupRequireCharging, isFalse); - await sut.delete(StoreKey.backupRequireCharging); - backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); - expect(backupRequireCharging, isNull); + bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); + expect(advancedTroubleshooting, isFalse); + await sut.delete(StoreKey.advancedTroubleshooting); + advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); + expect(advancedTroubleshooting, isNull); }); test('deleteAll()', () async { @@ -147,13 +147,13 @@ void main() { emitsInOrder([ [ const StoreDto(StoreKey.version, _kTestVersion), - const StoreDto(StoreKey.backupRequireCharging, _kTestBackupRequireCharging), const StoreDto(StoreKey.accessToken, _kTestAccessToken), + const StoreDto(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting), ], [ const StoreDto(StoreKey.version, _kTestVersion + 10), - const StoreDto(StoreKey.backupRequireCharging, _kTestBackupRequireCharging), const StoreDto(StoreKey.accessToken, _kTestAccessToken), + const StoreDto(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting), ], ]), ), diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 8bd813e8f6..584ea57027 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -21,7 +21,6 @@ void main() { late MockApiService apiService; late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; - late MockAppSettingService appSettingsService; late Drift db; setUp(() async { @@ -30,15 +29,12 @@ void main() { apiService = MockApiService(); networkService = MockNetworkService(); backgroundSyncManager = MockBackgroundSyncManager(); - appSettingsService = MockAppSettingService(); - sut = AuthService( authApiRepository, authRepository, apiService, networkService, backgroundSyncManager, - appSettingsService, ); registerFallbackValue(Uri()); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index 585ffcb499..dd19f2b1cc 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -11,12 +11,11 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; -import '../domain/service.mock.dart'; import '../fixtures/asset.stub.dart'; import '../infrastructure/repository.mock.dart'; import '../mocks/asset_entity.mock.dart'; @@ -28,13 +27,10 @@ void main() { late MockStorageRepository mockStorageRepository; late MockDriftLocalAssetRepository mockLocalAssetRepository; late MockDriftBackupRepository mockBackupRepository; - late MockAppSettingsService mockAppSettingsService; late MockAssetMediaRepository mockAssetMediaRepository; late Drift db; setUpAll(() async { - registerFallbackValue(AppSettingsEnum.useCellularForUploadPhotos); - TestWidgetsFlutterBinding.ensureInitialized(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( const MethodChannel('plugins.flutter.io/path_provider'), @@ -42,6 +38,7 @@ void main() { ); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); await StoreService.init(storeRepository: DriftStoreRepository(db)); + await MetadataRepository.ensureInitialized(db); await Store.put(StoreKey.serverEndpoint, 'http://test-server.com'); await Store.put(StoreKey.deviceId, 'test-device-id'); @@ -52,18 +49,13 @@ void main() { mockStorageRepository = MockStorageRepository(); mockLocalAssetRepository = MockDriftLocalAssetRepository(); mockBackupRepository = MockDriftBackupRepository(); - mockAppSettingsService = MockAppSettingsService(); mockAssetMediaRepository = MockAssetMediaRepository(); - when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)).thenReturn(false); - when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)).thenReturn(false); - sut = BackgroundUploadService( mockUploadRepository, mockStorageRepository, mockLocalAssetRepository, mockBackupRepository, - mockAppSettingsService, mockAssetMediaRepository, ); @@ -179,7 +171,6 @@ void main() { mockStorageRepository, mockLocalAssetRepository, mockBackupRepository, - mockAppSettingsService, mockAssetMediaRepository, ); addTearDown(() => sutWithV24.dispose()); @@ -230,7 +221,6 @@ void main() { mockStorageRepository, mockLocalAssetRepository, mockBackupRepository, - mockAppSettingsService, mockAssetMediaRepository, ); addTearDown(() => sutAndroid.dispose()); @@ -271,7 +261,6 @@ void main() { mockStorageRepository, mockLocalAssetRepository, mockBackupRepository, - mockAppSettingsService, mockAssetMediaRepository, ); addTearDown(() => sutWithV24.dispose()); @@ -312,7 +301,6 @@ void main() { mockStorageRepository, mockLocalAssetRepository, mockBackupRepository, - mockAppSettingsService, mockAssetMediaRepository, ); addTearDown(() => sutWithV24.dispose()); diff --git a/mobile/test/unit/repositories/metadata_repository_test.dart b/mobile/test/unit/repositories/metadata_repository_test.dart index 4c29ce3a01..21a436ea46 100644 --- a/mobile/test/unit/repositories/metadata_repository_test.dart +++ b/mobile/test/unit/repositories/metadata_repository_test.dart @@ -13,6 +13,7 @@ void main() { test('decode falls back to the default value when the raw input is unparseable', () { for (final key in MetadataKey.values) { + if (key.defaultValue is String) continue; expect( key.decode('not a valid encoding for any key'), key.defaultValue,