瀏覽代碼

feat(mobile): use app without storage permission (#5014)

* feat(mobile): use app without storage permission

* address review feedback
Fynn Petersen-Frey 1 年之前
父節點
當前提交
5145c33ed4

+ 2 - 4
mobile/assets/i18n/en-US.json

@@ -80,7 +80,6 @@
   "backup_controller_page_failed": "Failed ({})",
   "backup_controller_page_failed": "Failed ({})",
   "backup_controller_page_filename": "File name: {} [{}]",
   "backup_controller_page_filename": "File name: {} [{}]",
   "backup_controller_page_id": "ID: {}",
   "backup_controller_page_id": "ID: {}",
-  "backup_controller_page_info": "Backup Information",
   "backup_controller_page_none_selected": "None selected",
   "backup_controller_page_none_selected": "None selected",
   "backup_controller_page_remainder": "Remainder",
   "backup_controller_page_remainder": "Remainder",
   "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
   "backup_controller_page_remainder_sub": "Remaining photos and videos to back up from selection",
@@ -96,7 +95,6 @@
   "backup_controller_page_turn_off": "Turn off foreground backup",
   "backup_controller_page_turn_off": "Turn off foreground backup",
   "backup_controller_page_turn_on": "Turn on foreground backup",
   "backup_controller_page_turn_on": "Turn on foreground backup",
   "backup_controller_page_uploading_file_info": "Uploading file info",
   "backup_controller_page_uploading_file_info": "Uploading file info",
-  "backup_err_only_album": "Cannot remove the only album",
   "backup_info_card_assets": "assets",
   "backup_info_card_assets": "assets",
   "backup_manual_cancelled": "Cancelled",
   "backup_manual_cancelled": "Cancelled",
   "backup_manual_failed": "Failed",
   "backup_manual_failed": "Failed",
@@ -253,11 +251,11 @@
   "permission_onboarding_get_started": "Get started",
   "permission_onboarding_get_started": "Get started",
   "permission_onboarding_go_to_settings": "Go to settings",
   "permission_onboarding_go_to_settings": "Go to settings",
   "permission_onboarding_grant_permission": "Grant permission",
   "permission_onboarding_grant_permission": "Grant permission",
-  "permission_onboarding_log_out": "Log out",
+  "permission_onboarding_back": "Back",
   "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
   "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_granted": "Permission granted! You are all set.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
   "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
-  "permission_onboarding_request": "Immich requires permission to view your photos and videos.",
+  "permission_onboarding_request": "Immich requires permission to access your photos and videos.",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_app_logs": "Logs",
   "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
   "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
   "profile_drawer_documentation": "Documentation",
   "profile_drawer_documentation": "Documentation",

+ 2 - 0
mobile/lib/modules/album/providers/album.provider.dart

@@ -25,6 +25,8 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
         _albumService.refreshRemoteAlbums(isShared: false),
         _albumService.refreshRemoteAlbums(isShared: false),
       ]);
       ]);
 
 
+  Future<void> getDeviceAlbums() => _albumService.refreshDeviceAlbums();
+
   Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
   Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
 
 
   Future<Album?> createAlbum(
   Future<Album?> createAlbum(

+ 4 - 0
mobile/lib/modules/album/services/album.service.dart

@@ -67,6 +67,10 @@ class AlbumService {
       final List<String> selectedIds =
       final List<String> selectedIds =
           await _backupService.selectedAlbumsQuery().idProperty().findAll();
           await _backupService.selectedAlbumsQuery().idProperty().findAll();
       if (selectedIds.isEmpty) {
       if (selectedIds.isEmpty) {
+        final numLocal = await _db.albums.where().localIdIsNotNull().count();
+        if (numLocal > 0) {
+          _syncService.removeAllLocalAlbumsAndAssets();
+        }
         return false;
         return false;
       }
       }
       final List<AssetPathEntity> onDevice =
       final List<AssetPathEntity> onDevice =

+ 14 - 31
mobile/lib/modules/backup/providers/backup.provider.dart

@@ -89,7 +89,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
 
     state = state
     state = state
         .copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
         .copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
-    _updateBackupAssetCount();
   }
   }
 
 
   void addExcludedAlbumForBackup(AvailableAlbum album) {
   void addExcludedAlbumForBackup(AvailableAlbum album) {
@@ -98,7 +97,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     }
     }
     state = state
     state = state
         .copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
         .copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
-    _updateBackupAssetCount();
   }
   }
 
 
   void removeAlbumForBackup(AvailableAlbum album) {
   void removeAlbumForBackup(AvailableAlbum album) {
@@ -107,7 +105,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     currentSelectedAlbums.removeWhere((a) => a == album);
     currentSelectedAlbums.removeWhere((a) => a == album);
 
 
     state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
     state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
-    _updateBackupAssetCount();
   }
   }
 
 
   void removeExcludedAlbumForBackup(AvailableAlbum album) {
   void removeExcludedAlbumForBackup(AvailableAlbum album) {
@@ -116,7 +113,20 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     currentExcludedAlbums.removeWhere((a) => a == album);
     currentExcludedAlbums.removeWhere((a) => a == album);
 
 
     state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
     state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
-    _updateBackupAssetCount();
+  }
+
+  Future<void> backupAlbumSelectionDone() {
+    if (state.selectedBackupAlbums.isEmpty) {
+      // disable any backup
+      cancelBackup();
+      setAutoBackup(false);
+      configureBackgroundBackup(
+        enabled: false,
+        onError: (msg) {},
+        onBatteryInfo: () {},
+      );
+    }
+    return _updateBackupAssetCount();
   }
   }
 
 
   void setAutoBackup(bool enabled) {
   void setAutoBackup(bool enabled) {
@@ -249,30 +259,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     final List<BackupAlbum> selectedBackupAlbums =
     final List<BackupAlbum> selectedBackupAlbums =
         await _backupService.selectedAlbumsQuery().findAll();
         await _backupService.selectedAlbumsQuery().findAll();
 
 
-    // First time backup - set isAll album is the default one for backup.
-    if (selectedBackupAlbums.isEmpty) {
-      log.info("First time backup; setup 'Recent(s)' album as default");
-
-      // Get album that contains all assets
-      final list = await PhotoManager.getAssetPathList(
-        hasAll: true,
-        onlyAll: true,
-        type: RequestType.common,
-      );
-
-      if (list.isEmpty) {
-        return;
-      }
-      AssetPathEntity albumHasAllAssets = list.first;
-
-      final ba = BackupAlbum(
-        albumHasAllAssets.id,
-        DateTime.fromMillisecondsSinceEpoch(0),
-        BackupSelection.select,
-      );
-      await _db.writeTxn(() => _db.backupAlbums.put(ba));
-    }
-
     // Generate AssetPathEntity from id to add to local state
     // Generate AssetPathEntity from id to add to local state
     final Set<AvailableAlbum> selectedAlbums = {};
     final Set<AvailableAlbum> selectedAlbums = {};
     for (final BackupAlbum ba in selectedBackupAlbums) {
     for (final BackupAlbum ba in selectedBackupAlbums) {
@@ -362,7 +348,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
         allUniqueAssets: {},
         allUniqueAssets: {},
         selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
         selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
       );
       );
-      return;
     } else {
     } else {
       state = state.copyWith(
       state = state.copyWith(
         allAssetsInDatabase: allAssetsInDatabase,
         allAssetsInDatabase: allAssetsInDatabase,
@@ -373,8 +358,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 
 
     // Save to persistent storage
     // Save to persistent storage
     await _updatePersistentAlbumsSelection();
     await _updatePersistentAlbumsSelection();
-
-    return;
   }
   }
 
 
   /// Get all necessary information for calculating the available albums,
   /// Get all necessary information for calculating the available albums,

+ 4 - 27
mobile/lib/modules/backup/ui/album_info_card.dart

@@ -82,19 +82,9 @@ class AlbumInfoCard extends HookConsumerWidget {
         HapticFeedback.selectionClick();
         HapticFeedback.selectionClick();
 
 
         if (isSelected) {
         if (isSelected) {
-          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
-            ImmichToast.show(
-              context: context,
-              msg: "backup_err_only_album".tr(),
-              toastType: ToastType.error,
-              gravity: ToastGravity.BOTTOM,
-            );
-            return;
-          }
-
-          ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo);
+          ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
         } else {
         } else {
-          ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo);
+          ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
         }
         }
       },
       },
       onDoubleTap: () {
       onDoubleTap: () {
@@ -103,23 +93,10 @@ class AlbumInfoCard extends HookConsumerWidget {
         if (isExcluded) {
         if (isExcluded) {
           // Remove from exclude album list
           // Remove from exclude album list
           ref
           ref
-              .watch(backupProvider.notifier)
+              .read(backupProvider.notifier)
               .removeExcludedAlbumForBackup(albumInfo);
               .removeExcludedAlbumForBackup(albumInfo);
         } else {
         } else {
           // Add to exclude album list
           // Add to exclude album list
-          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
-              ref
-                  .watch(backupProvider)
-                  .selectedBackupAlbums
-                  .contains(albumInfo)) {
-            ImmichToast.show(
-              context: context,
-              msg: "backup_err_only_album".tr(),
-              toastType: ToastType.error,
-              gravity: ToastGravity.BOTTOM,
-            );
-            return;
-          }
 
 
           if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
           if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
             ImmichToast.show(
             ImmichToast.show(
@@ -132,7 +109,7 @@ class AlbumInfoCard extends HookConsumerWidget {
           }
           }
 
 
           ref
           ref
-              .watch(backupProvider.notifier)
+              .read(backupProvider.notifier)
               .addExcludedAlbumForBackup(albumInfo);
               .addExcludedAlbumForBackup(albumInfo);
         }
         }
       },
       },

+ 4 - 28
mobile/lib/modules/backup/ui/album_info_list_tile.dart

@@ -1,4 +1,3 @@
-import 'package:easy_localization/easy_localization.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:flutter_hooks/flutter_hooks.dart';
@@ -74,23 +73,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
         if (isExcluded) {
         if (isExcluded) {
           // Remove from exclude album list
           // Remove from exclude album list
           ref
           ref
-              .watch(backupProvider.notifier)
+              .read(backupProvider.notifier)
               .removeExcludedAlbumForBackup(albumInfo);
               .removeExcludedAlbumForBackup(albumInfo);
         } else {
         } else {
           // Add to exclude album list
           // Add to exclude album list
-          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
-              ref
-                  .watch(backupProvider)
-                  .selectedBackupAlbums
-                  .contains(albumInfo)) {
-            ImmichToast.show(
-              context: context,
-              msg: "backup_err_only_album".tr(),
-              toastType: ToastType.error,
-              gravity: ToastGravity.BOTTOM,
-            );
-            return;
-          }
 
 
           if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
           if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
             ImmichToast.show(
             ImmichToast.show(
@@ -103,7 +89,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
           }
           }
 
 
           ref
           ref
-              .watch(backupProvider.notifier)
+              .read(backupProvider.notifier)
               .addExcludedAlbumForBackup(albumInfo);
               .addExcludedAlbumForBackup(albumInfo);
         }
         }
       },
       },
@@ -113,19 +99,9 @@ class AlbumInfoListTile extends HookConsumerWidget {
         onTap: () {
         onTap: () {
           HapticFeedback.selectionClick();
           HapticFeedback.selectionClick();
           if (isSelected) {
           if (isSelected) {
-            if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
-              ImmichToast.show(
-                context: context,
-                msg: "backup_err_only_album".tr(),
-                toastType: ToastType.error,
-                gravity: ToastGravity.BOTTOM,
-              );
-              return;
-            }
-
-            ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo);
+            ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo);
           } else {
           } else {
-            ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo);
+            ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo);
           }
           }
         },
         },
         leading: ClipRRect(
         leading: ClipRRect(

+ 2 - 56
mobile/lib/modules/backup/views/backup_album_selection_page.dart

@@ -1,7 +1,6 @@
 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:flutter_hooks/flutter_hooks.dart';
-import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/immich_colors.dart';
 import 'package:immich_mobile/constants/immich_colors.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -9,7 +8,6 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
 import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
 import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
 import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.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/immich_toast.dart';
 
 
 class BackupAlbumSelectionPage extends HookConsumerWidget {
 class BackupAlbumSelectionPage extends HookConsumerWidget {
   const BackupAlbumSelectionPage({Key? key}) : super(key: key);
   const BackupAlbumSelectionPage({Key? key}) : super(key: key);
@@ -91,19 +89,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 
 
     buildSelectedAlbumNameChip() {
     buildSelectedAlbumNameChip() {
       return selectedBackupAlbums.map((album) {
       return selectedBackupAlbums.map((album) {
-        void removeSelection() {
-          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
-            ImmichToast.show(
-              context: context,
-              msg: "backup_err_only_album".tr(),
-              toastType: ToastType.error,
-              gravity: ToastGravity.BOTTOM,
-            );
-            return;
-          }
-
-          ref.watch(backupProvider.notifier).removeAlbumForBackup(album);
-        }
+        void removeSelection() =>
+            ref.read(backupProvider.notifier).removeAlbumForBackup(album);
 
 
         return Padding(
         return Padding(
           padding: const EdgeInsets.only(right: 8.0),
           padding: const EdgeInsets.only(right: 8.0),
@@ -252,47 +239,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
                   ),
                   ),
                 ),
                 ),
 
 
-                Padding(
-                  padding:
-                      const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
-                  child: Card(
-                    margin: const EdgeInsets.all(0),
-                    shape: RoundedRectangleBorder(
-                      borderRadius: BorderRadius.circular(10),
-                      side: BorderSide(
-                        color: isDarkTheme
-                            ? const Color.fromARGB(255, 0, 0, 0)
-                            : const Color.fromARGB(255, 235, 235, 235),
-                        width: 1,
-                      ),
-                    ),
-                    elevation: 0,
-                    borderOnForeground: false,
-                    child: Column(
-                      children: [
-                        ListTile(
-                          visualDensity: VisualDensity.compact,
-                          title: const Text(
-                            "backup_album_selection_page_total_assets",
-                            style: TextStyle(
-                              fontWeight: FontWeight.bold,
-                              fontSize: 14,
-                            ),
-                          ).tr(),
-                          trailing: Text(
-                            ref
-                                .watch(backupProvider)
-                                .allUniqueAssets
-                                .length
-                                .toString(),
-                            style: const TextStyle(fontWeight: FontWeight.bold),
-                          ),
-                        ),
-                      ],
-                    ),
-                  ),
-                ),
-
                 ListTile(
                 ListTile(
                   title: Text(
                   title: Text(
                     "backup_album_selection_page_albums_device".tr(
                     "backup_album_selection_page_albums_device".tr(

+ 54 - 51
mobile/lib/modules/backup/views/backup_controller_page.dart

@@ -6,6 +6,7 @@ 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/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
 import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
 import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
 import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
 import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
 import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
@@ -38,6 +39,7 @@ class BackupControllerPage extends HookConsumerWidget {
     final settingsService = ref.watch(appSettingsServiceProvider);
     final settingsService = ref.watch(appSettingsServiceProvider);
     final showBackupFix = Platform.isAndroid &&
     final showBackupFix = Platform.isAndroid &&
         settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting);
         settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting);
+    final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty;
 
 
     final appRefreshDisabled =
     final appRefreshDisabled =
         Platform.isIOS && settings?.appRefreshEnabled != true;
         Platform.isIOS && settings?.appRefreshEnabled != true;
@@ -590,8 +592,14 @@ class BackupControllerPage extends HookConsumerWidget {
             ),
             ),
           ),
           ),
           trailing: ElevatedButton(
           trailing: ElevatedButton(
-            onPressed: () {
-              context.autoPush(const BackupAlbumSelectionRoute());
+            onPressed: () async {
+              await context.autoPush(const BackupAlbumSelectionRoute());
+              // waited until returning from selection
+              await ref
+                  .read(backupProvider.notifier)
+                  .backupAlbumSelectionDone();
+              // waited until backup albums are stored in DB
+              ref.read(albumProvider.notifier).getDeviceAlbums();
             },
             },
             child: const Text(
             child: const Text(
               "backup_controller_page_select",
               "backup_controller_page_select",
@@ -689,55 +697,50 @@ class BackupControllerPage extends HookConsumerWidget {
         padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
         padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
         child: ListView(
         child: ListView(
           // crossAxisAlignment: CrossAxisAlignment.start,
           // crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Padding(
-              padding: const EdgeInsets.all(8.0),
-              child: const Text(
-                "backup_controller_page_info",
-                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
-              ).tr(),
-            ),
-            buildFolderSelectionTile(),
-            BackupInfoCard(
-              title: "backup_controller_page_total".tr(),
-              subtitle: "backup_controller_page_total_sub".tr(),
-              info: ref.watch(backupProvider).availableAlbums.isEmpty
-                  ? "..."
-                  : "${backupState.allUniqueAssets.length}",
-            ),
-            BackupInfoCard(
-              title: "backup_controller_page_backup".tr(),
-              subtitle: "backup_controller_page_backup_sub".tr(),
-              info: ref.watch(backupProvider).availableAlbums.isEmpty
-                  ? "..."
-                  : "${backupState.selectedAlbumsBackupAssetsIds.length}",
-            ),
-            BackupInfoCard(
-              title: "backup_controller_page_remainder".tr(),
-              subtitle: "backup_controller_page_remainder_sub".tr(),
-              info: ref.watch(backupProvider).availableAlbums.isEmpty
-                  ? "..."
-                  : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
-            ),
-            const Divider(),
-            buildAutoBackupController(),
-            const Divider(),
-            AnimatedSwitcher(
-              duration: const Duration(milliseconds: 500),
-              child: Platform.isIOS
-                  ? (appRefreshDisabled
-                      ? buildBackgroundAppRefreshWarning()
-                      : buildBackgroundBackupController())
-                  : buildBackgroundBackupController(),
-            ),
-            if (showBackupFix) const Divider(),
-            if (showBackupFix) buildCheckCorruptBackups(),
-            const Divider(),
-            const Divider(),
-            const CurrentUploadingAssetInfoBox(),
-            if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
-            buildBackupButton(),
-          ],
+          children: hasAnyAlbum
+              ? [
+                  buildFolderSelectionTile(),
+                  BackupInfoCard(
+                    title: "backup_controller_page_total".tr(),
+                    subtitle: "backup_controller_page_total_sub".tr(),
+                    info: ref.watch(backupProvider).availableAlbums.isEmpty
+                        ? "..."
+                        : "${backupState.allUniqueAssets.length}",
+                  ),
+                  BackupInfoCard(
+                    title: "backup_controller_page_backup".tr(),
+                    subtitle: "backup_controller_page_backup_sub".tr(),
+                    info: ref.watch(backupProvider).availableAlbums.isEmpty
+                        ? "..."
+                        : "${backupState.selectedAlbumsBackupAssetsIds.length}",
+                  ),
+                  BackupInfoCard(
+                    title: "backup_controller_page_remainder".tr(),
+                    subtitle: "backup_controller_page_remainder_sub".tr(),
+                    info: ref.watch(backupProvider).availableAlbums.isEmpty
+                        ? "..."
+                        : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
+                  ),
+                  const Divider(),
+                  buildAutoBackupController(),
+                  const Divider(),
+                  AnimatedSwitcher(
+                    duration: const Duration(milliseconds: 500),
+                    child: Platform.isIOS
+                        ? (appRefreshDisabled
+                            ? buildBackgroundAppRefreshWarning()
+                            : buildBackgroundBackupController())
+                        : buildBackgroundBackupController(),
+                  ),
+                  if (showBackupFix) const Divider(),
+                  if (showBackupFix) buildCheckCorruptBackups(),
+                  const Divider(),
+                  const Divider(),
+                  const CurrentUploadingAssetInfoBox(),
+                  if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
+                  buildBackupButton(),
+                ]
+              : [buildFolderSelectionTile()],
         ),
         ),
       ),
       ),
     );
     );

+ 6 - 17
mobile/lib/modules/onboarding/views/permission_onboarding_page.dart

@@ -2,8 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/backup/providers/backup.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/ui/immich_logo.dart';
 import 'package:immich_mobile/shared/ui/immich_logo.dart';
@@ -18,13 +16,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
     final PermissionStatus permission = ref.watch(galleryPermissionNotifier);
     final PermissionStatus permission = ref.watch(galleryPermissionNotifier);
 
 
     // Navigate to the main Tab Controller when permission is granted
     // Navigate to the main Tab Controller when permission is granted
-    void goToHome() {
-      // Resume backup (if enable) then navigate
-      ref.watch(backupProvider.notifier).resumeBackup().catchError((error) {
-        debugPrint('PermissionOnboardingPage error: $error');
-      });
-      context.autoReplace(const TabControllerRoute());
-    }
+    void goToBackup() => context.autoReplace(const BackupControllerRoute());
 
 
     // When the permission is denied, we show a request permission page
     // When the permission is denied, we show a request permission page
     buildRequestPermission() {
     buildRequestPermission() {
@@ -46,7 +38,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
               if (permission.isGranted) {
               if (permission.isGranted) {
                 // If permission is limited, we will show the limited
                 // If permission is limited, we will show the limited
                 // permission page
                 // permission page
-                goToHome();
+                goToBackup();
               }
               }
             }),
             }),
             child: const Text(
             child: const Text(
@@ -71,7 +63,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
           ).tr(),
           ).tr(),
           const SizedBox(height: 18),
           const SizedBox(height: 18),
           ElevatedButton(
           ElevatedButton(
-            onPressed: () => goToHome(),
+            onPressed: () => goToBackup(),
             child: const Text('permission_onboarding_get_started').tr(),
             child: const Text('permission_onboarding_get_started').tr(),
           ),
           ),
         ],
         ],
@@ -106,7 +98,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
           ),
           ),
           const SizedBox(height: 8.0),
           const SizedBox(height: 8.0),
           TextButton(
           TextButton(
-            onPressed: () => goToHome(),
+            onPressed: () => goToBackup(),
             child: const Text(
             child: const Text(
               'permission_onboarding_continue_anyway',
               'permission_onboarding_continue_anyway',
             ).tr(),
             ).tr(),
@@ -181,11 +173,8 @@ class PermissionOnboardingPage extends HookConsumerWidget {
                   ),
                   ),
                 ),
                 ),
                 TextButton(
                 TextButton(
-                  child: const Text('permission_onboarding_log_out').tr(),
-                  onPressed: () {
-                    ref.read(authenticationProvider.notifier).logout();
-                    context.autoReplace(const LoginRoute());
-                  },
+                  child: const Text('permission_onboarding_back').tr(),
+                  onPressed: () => context.autoPop(),
                 ),
                 ),
               ],
               ],
             ),
             ),

+ 3 - 3
mobile/lib/routing/gallery_permission_guard.dart → mobile/lib/routing/backup_permission_guard.dart

@@ -2,10 +2,10 @@ import 'package:auto_route/auto_route.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';
 
 
-class GalleryPermissionGuard extends AutoRouteGuard {
+class BackupPermissionGuard extends AutoRouteGuard {
   final GalleryPermissionNotifier _permission;
   final GalleryPermissionNotifier _permission;
 
 
-  GalleryPermissionGuard(this._permission);
+  BackupPermissionGuard(this._permission);
 
 
   @override
   @override
   void onNavigation(NavigationResolver resolver, StackRouter router) async {
   void onNavigation(NavigationResolver resolver, StackRouter router) async {
@@ -13,7 +13,7 @@ class GalleryPermissionGuard extends AutoRouteGuard {
     if (p) {
     if (p) {
       resolver.next(true);
       resolver.next(true);
     } else {
     } else {
-      router.replaceAll([const PermissionOnboardingRoute()]);
+      router.push(const PermissionOnboardingRoute());
     }
     }
   }
   }
 }
 }

+ 9 - 6
mobile/lib/routing/router.dart

@@ -44,7 +44,7 @@ import 'package:immich_mobile/modules/search/views/search_result_page.dart';
 import 'package:immich_mobile/modules/settings/views/settings_page.dart';
 import 'package:immich_mobile/modules/settings/views/settings_page.dart';
 import 'package:immich_mobile/routing/auth_guard.dart';
 import 'package:immich_mobile/routing/auth_guard.dart';
 import 'package:immich_mobile/routing/duplicate_guard.dart';
 import 'package:immich_mobile/routing/duplicate_guard.dart';
-import 'package:immich_mobile/routing/gallery_permission_guard.dart';
+import 'package:immich_mobile/routing/backup_permission_guard.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/logger_message.model.dart';
 import 'package:immich_mobile/shared/models/logger_message.model.dart';
@@ -77,7 +77,7 @@ part 'router.gr.dart';
     AutoRoute(page: ChangePasswordPage),
     AutoRoute(page: ChangePasswordPage),
     CustomRoute(
     CustomRoute(
       page: TabControllerPage,
       page: TabControllerPage,
-      guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard],
+      guards: [AuthGuard, DuplicateGuard],
       children: [
       children: [
         AutoRoute(page: HomePage, guards: [AuthGuard, DuplicateGuard]),
         AutoRoute(page: HomePage, guards: [AuthGuard, DuplicateGuard]),
         AutoRoute(page: SearchPage, guards: [AuthGuard, DuplicateGuard]),
         AutoRoute(page: SearchPage, guards: [AuthGuard, DuplicateGuard]),
@@ -88,10 +88,13 @@ part 'router.gr.dart';
     ),
     ),
     AutoRoute(
     AutoRoute(
       page: GalleryViewerPage,
       page: GalleryViewerPage,
-      guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard],
+      guards: [AuthGuard, DuplicateGuard],
     ),
     ),
     AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
     AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]),
-    AutoRoute(page: BackupControllerPage, guards: [AuthGuard, DuplicateGuard]),
+    AutoRoute(
+      page: BackupControllerPage,
+      guards: [AuthGuard, DuplicateGuard, BackupPermissionGuard],
+    ),
     AutoRoute(page: SearchResultPage, guards: [AuthGuard, DuplicateGuard]),
     AutoRoute(page: SearchResultPage, guards: [AuthGuard, DuplicateGuard]),
     AutoRoute(page: CuratedLocationPage, guards: [AuthGuard, DuplicateGuard]),
     AutoRoute(page: CuratedLocationPage, guards: [AuthGuard, DuplicateGuard]),
     AutoRoute(page: CreateAlbumPage, guards: [AuthGuard, DuplicateGuard]),
     AutoRoute(page: CreateAlbumPage, guards: [AuthGuard, DuplicateGuard]),
@@ -179,8 +182,8 @@ class AppRouter extends _$AppRouter {
   ) : super(
   ) : super(
           authGuard: AuthGuard(_apiService),
           authGuard: AuthGuard(_apiService),
           duplicateGuard: DuplicateGuard(),
           duplicateGuard: DuplicateGuard(),
-          galleryPermissionGuard:
-              GalleryPermissionGuard(galleryPermissionNotifier),
+          backupPermissionGuard:
+              BackupPermissionGuard(galleryPermissionNotifier),
         );
         );
 }
 }
 
 

+ 3 - 4
mobile/lib/routing/router.gr.dart

@@ -17,14 +17,14 @@ class _$AppRouter extends RootStackRouter {
     GlobalKey<NavigatorState>? navigatorKey,
     GlobalKey<NavigatorState>? navigatorKey,
     required this.authGuard,
     required this.authGuard,
     required this.duplicateGuard,
     required this.duplicateGuard,
-    required this.galleryPermissionGuard,
+    required this.backupPermissionGuard,
   }) : super(navigatorKey);
   }) : super(navigatorKey);
 
 
   final AuthGuard authGuard;
   final AuthGuard authGuard;
 
 
   final DuplicateGuard duplicateGuard;
   final DuplicateGuard duplicateGuard;
 
 
-  final GalleryPermissionGuard galleryPermissionGuard;
+  final BackupPermissionGuard backupPermissionGuard;
 
 
   @override
   @override
   final Map<String, PageFactory> pagesMap = {
   final Map<String, PageFactory> pagesMap = {
@@ -414,7 +414,6 @@ class _$AppRouter extends RootStackRouter {
           guards: [
           guards: [
             authGuard,
             authGuard,
             duplicateGuard,
             duplicateGuard,
-            galleryPermissionGuard,
           ],
           ],
           children: [
           children: [
             RouteConfig(
             RouteConfig(
@@ -461,7 +460,6 @@ class _$AppRouter extends RootStackRouter {
           guards: [
           guards: [
             authGuard,
             authGuard,
             duplicateGuard,
             duplicateGuard,
-            galleryPermissionGuard,
           ],
           ],
         ),
         ),
         RouteConfig(
         RouteConfig(
@@ -478,6 +476,7 @@ class _$AppRouter extends RootStackRouter {
           guards: [
           guards: [
             authGuard,
             authGuard,
             duplicateGuard,
             duplicateGuard,
+            backupPermissionGuard,
           ],
           ],
         ),
         ),
         RouteConfig(
         RouteConfig(

+ 7 - 5
mobile/lib/shared/providers/app_state.provider.dart

@@ -45,12 +45,14 @@ class AppStateNotiifer extends StateNotifier<AppStateEnum> {
     _wasPaused = false;
     _wasPaused = false;
 
 
     final isAuthenticated = _ref.read(authenticationProvider).isAuthenticated;
     final isAuthenticated = _ref.read(authenticationProvider).isAuthenticated;
-    final permission = _ref.read(galleryPermissionNotifier);
 
 
-    // Needs to be logged in and have gallery permissions
-    if (isAuthenticated && (permission.isGranted || permission.isLimited)) {
-      _ref.read(backupProvider.notifier).resumeBackup();
-      _ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
+    // Needs to be logged in
+    if (isAuthenticated) {
+      final permission = _ref.watch(galleryPermissionNotifier);
+      if (permission.isGranted || permission.isLimited) {
+        _ref.read(backupProvider.notifier).resumeBackup();
+        _ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
+      }
       _ref.read(serverInfoProvider.notifier).getServerVersion();
       _ref.read(serverInfoProvider.notifier).getServerVersion();
       switch (_ref.read(tabProvider)) {
       switch (_ref.read(tabProvider)) {
         case TabEnum.home:
         case TabEnum.home:

+ 20 - 0
mobile/lib/shared/services/sync.service.dart

@@ -90,6 +90,9 @@ class SyncService {
   Future<bool> syncNewAssetToDb(Asset newAsset) =>
   Future<bool> syncNewAssetToDb(Asset newAsset) =>
       _lock.run(() => _syncNewAssetToDb(newAsset));
       _lock.run(() => _syncNewAssetToDb(newAsset));
 
 
+  Future<bool> removeAllLocalAlbumsAndAssets() =>
+      _lock.run(_removeAllLocalAlbumsAndAssets);
+
   // private methods:
   // private methods:
 
 
   /// Syncs users from the server to the local database
   /// Syncs users from the server to the local database
@@ -756,6 +759,23 @@ class SyncService {
         await a.assetCountAsync !=
         await a.assetCountAsync !=
             (await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
             (await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount;
   }
   }
+
+  Future<bool> _removeAllLocalAlbumsAndAssets() async {
+    try {
+      final assets = await _db.assets.where().localIdIsNotNull().findAll();
+      final (toDelete, toUpdate) =
+          _handleAssetRemoval(assets, [], remote: false);
+      await _db.writeTxn(() async {
+        await _db.assets.deleteAll(toDelete);
+        await _db.assets.putAll(toUpdate);
+        await _db.albums.where().localIdIsNotNull().deleteAll();
+      });
+      return true;
+    } catch (e) {
+      _log.severe("Failed to remove all local albums and assets: $e");
+      return false;
+    }
+  }
 }
 }
 
 
 /// Returns a triple(toAdd, toUpdate, toRemove)
 /// Returns a triple(toAdd, toUpdate, toRemove)