mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:39:03 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			348 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			348 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:dio/dio.dart';
 | |
| import 'package:flutter/foundation.dart';
 | |
| import 'package:hive_flutter/hive_flutter.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:immich_mobile/constants/hive_box.dart';
 | |
| import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
 | |
| import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
 | |
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | |
| import 'package:immich_mobile/shared/services/server_info.service.dart';
 | |
| import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 | |
| import 'package:immich_mobile/shared/models/server_info.model.dart';
 | |
| import 'package:immich_mobile/modules/backup/services/backup.service.dart';
 | |
| import 'package:photo_manager/photo_manager.dart';
 | |
| 
 | |
| class BackupNotifier extends StateNotifier<BackUpState> {
 | |
|   BackupNotifier({this.ref})
 | |
|       : super(
 | |
|           BackUpState(
 | |
|             backupProgress: BackUpProgressEnum.idle,
 | |
|             allAssetOnDatabase: const [],
 | |
|             progressInPercentage: 0,
 | |
|             cancelToken: CancelToken(),
 | |
|             serverInfo: ServerInfo(
 | |
|               diskAvailable: "0",
 | |
|               diskAvailableRaw: 0,
 | |
|               diskSize: "0",
 | |
|               diskSizeRaw: 0,
 | |
|               diskUsagePercentage: 0.0,
 | |
|               diskUse: "0",
 | |
|               diskUseRaw: 0,
 | |
|             ),
 | |
|             availableAlbums: const [],
 | |
|             selectedBackupAlbums: const {},
 | |
|             excludedBackupAlbums: const {},
 | |
|             allUniqueAssets: const {},
 | |
|             selectedAlbumsBackupAssetsIds: const {},
 | |
|           ),
 | |
|         );
 | |
| 
 | |
|   Ref? ref;
 | |
|   final BackupService _backupService = BackupService();
 | |
|   final ServerInfoService _serverInfoService = ServerInfoService();
 | |
| 
 | |
|   ///
 | |
|   /// UI INTERACTION
 | |
|   ///
 | |
|   /// Album selection
 | |
|   /// Due to the overlapping assets across multiple albums on the device
 | |
|   /// We have method to include and exclude albums
 | |
|   /// The total unique assets will be used for backing mechanism
 | |
|   ///
 | |
|   void addAlbumForBackup(AssetPathEntity album) {
 | |
|     if (state.excludedBackupAlbums.contains(album)) {
 | |
|       removeExcludedAlbumForBackup(album);
 | |
|     }
 | |
| 
 | |
|     state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
 | |
|     _updateBackupAssetCount();
 | |
|   }
 | |
| 
 | |
|   void addExcludedAlbumForBackup(AssetPathEntity album) {
 | |
|     if (state.selectedBackupAlbums.contains(album)) {
 | |
|       removeAlbumForBackup(album);
 | |
|     }
 | |
|     state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
 | |
|     _updateBackupAssetCount();
 | |
|   }
 | |
| 
 | |
|   void removeAlbumForBackup(AssetPathEntity album) {
 | |
|     Set<AssetPathEntity> currentSelectedAlbums = state.selectedBackupAlbums;
 | |
| 
 | |
|     currentSelectedAlbums.removeWhere((a) => a == album);
 | |
| 
 | |
|     state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
 | |
|     _updateBackupAssetCount();
 | |
|   }
 | |
| 
 | |
|   void removeExcludedAlbumForBackup(AssetPathEntity album) {
 | |
|     Set<AssetPathEntity> currentExcludedAlbums = state.excludedBackupAlbums;
 | |
| 
 | |
|     currentExcludedAlbums.removeWhere((a) => a == album);
 | |
| 
 | |
|     state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
 | |
|     _updateBackupAssetCount();
 | |
|   }
 | |
| 
 | |
|   ///
 | |
|   /// Get all album on the device
 | |
|   /// Get all selected and excluded album from the user's persistent storage
 | |
|   /// If this is the first time performing backup - set the default selected album to be
 | |
|   /// the one that has all assets (Recent on Android, Recents on iOS)
 | |
|   ///
 | |
|   Future<void> getBackupAlbumsInfo() async {
 | |
|     // Get all albums on the device
 | |
|     List<AvailableAlbum> availableAlbums = [];
 | |
|     List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(hasAll: true, type: RequestType.common);
 | |
| 
 | |
|     for (AssetPathEntity album in albums) {
 | |
|       AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
 | |
| 
 | |
|       var assetList = await album.getAssetListRange(start: 0, end: album.assetCount);
 | |
| 
 | |
|       if (assetList.isNotEmpty) {
 | |
|         var thumbnailAsset = assetList.first;
 | |
|         var thumbnailData = await thumbnailAsset.thumbnailDataWithSize(const ThumbnailSize(512, 512));
 | |
|         availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
 | |
|       }
 | |
| 
 | |
|       availableAlbums.add(availableAlbum);
 | |
|     }
 | |
| 
 | |
|     state = state.copyWith(availableAlbums: availableAlbums);
 | |
| 
 | |
|     // Put persistent storage info into local state of the app
 | |
|     // Get local storage on selected backup album
 | |
|     Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
 | |
|     HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
 | |
|       backupInfoKey,
 | |
|       defaultValue: HiveBackupAlbums(
 | |
|         selectedAlbumIds: [],
 | |
|         excludedAlbumsIds: [],
 | |
|       ),
 | |
|     );
 | |
| 
 | |
|     if (backupAlbumInfo == null) {
 | |
|       debugPrint("[ERROR] getting Hive backup album infomation");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // First time backup - set isAll album is the default one for backup.
 | |
|     if (backupAlbumInfo.selectedAlbumIds.isEmpty) {
 | |
|       debugPrint("First time backup setup recent album as default");
 | |
| 
 | |
|       // Get album that contains all assets
 | |
|       var list = await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
 | |
|       AssetPathEntity albumHasAllAssets = list.first;
 | |
| 
 | |
|       backupAlbumInfoBox.put(
 | |
|         backupInfoKey,
 | |
|         HiveBackupAlbums(
 | |
|           selectedAlbumIds: [albumHasAllAssets.id],
 | |
|           excludedAlbumsIds: [],
 | |
|         ),
 | |
|       );
 | |
| 
 | |
|       backupAlbumInfo = backupAlbumInfoBox.get(backupInfoKey);
 | |
|     }
 | |
| 
 | |
|     // Generate AssetPathEntity from id to add to local state
 | |
|     try {
 | |
|       for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
 | |
|         var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
 | |
|         state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
 | |
|       }
 | |
| 
 | |
|       for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
 | |
|         var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
 | |
|         state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
 | |
|       }
 | |
|     } catch (e) {
 | |
|       debugPrint("[ERROR] Failed to generate album from id $e");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   ///
 | |
|   /// From all the selected and albums assets
 | |
|   /// Find the assets that are not overlapping between the two sets
 | |
|   /// Those assets are unique and are used as the total assets
 | |
|   ///
 | |
|   void _updateBackupAssetCount() async {
 | |
|     Set<AssetEntity> assetsFromSelectedAlbums = {};
 | |
|     Set<AssetEntity> assetsFromExcludedAlbums = {};
 | |
| 
 | |
|     for (var album in state.selectedBackupAlbums) {
 | |
|       var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
 | |
|       assetsFromSelectedAlbums.addAll(assets);
 | |
|     }
 | |
| 
 | |
|     for (var album in state.excludedBackupAlbums) {
 | |
|       var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
 | |
|       assetsFromExcludedAlbums.addAll(assets);
 | |
|     }
 | |
| 
 | |
|     Set<AssetEntity> allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
 | |
|     List<String> allAssetOnDatabase = await _backupService.getDeviceBackupAsset();
 | |
| 
 | |
|     // Find asset that were backup from selected albums
 | |
|     Set<String> selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.id));
 | |
|     selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
 | |
| 
 | |
|     if (allUniqueAssets.isEmpty) {
 | |
|       debugPrint("No Asset On Device");
 | |
|       state = state.copyWith(
 | |
|         backupProgress: BackUpProgressEnum.idle,
 | |
|         allAssetOnDatabase: allAssetOnDatabase,
 | |
|         allUniqueAssets: {},
 | |
|         selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
 | |
|       );
 | |
|       return;
 | |
|     } else {
 | |
|       state = state.copyWith(
 | |
|         allAssetOnDatabase: allAssetOnDatabase,
 | |
|         allUniqueAssets: allUniqueAssets,
 | |
|         selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Save to persistent storage
 | |
|     _updatePersistentAlbumsSelection();
 | |
|   }
 | |
| 
 | |
|   ///
 | |
|   /// Get all necessary information for calculating the available albums,
 | |
|   /// which albums are selected or excluded
 | |
|   /// and then update the UI according to those information
 | |
|   ///
 | |
|   void getBackupInfo() async {
 | |
|     await getBackupAlbumsInfo();
 | |
|     _updateServerInfo();
 | |
|     _updateBackupAssetCount();
 | |
|   }
 | |
| 
 | |
|   ///
 | |
|   /// Save user selection of selected albums and excluded albums to
 | |
|   /// Hive database
 | |
|   ///
 | |
|   void _updatePersistentAlbumsSelection() {
 | |
|     Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
 | |
|     backupAlbumInfoBox.put(
 | |
|       backupInfoKey,
 | |
|       HiveBackupAlbums(
 | |
|         selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(),
 | |
|         excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   ///
 | |
|   /// Invoke backup process
 | |
|   ///
 | |
|   void startBackupProcess() async {
 | |
|     _updateServerInfo();
 | |
|     _updateBackupAssetCount();
 | |
| 
 | |
|     state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
 | |
| 
 | |
|     var authResult = await PhotoManager.requestPermissionExtend();
 | |
|     if (authResult.isAuth) {
 | |
|       await PhotoManager.clearFileCache();
 | |
| 
 | |
|       if (state.allUniqueAssets.isEmpty) {
 | |
|         debugPrint("No Asset On Device - Abort Backup Process");
 | |
|         state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets;
 | |
| 
 | |
|       // Remove item that has already been backed up
 | |
|       for (var assetId in state.allAssetOnDatabase) {
 | |
|         assetsWillBeBackup.removeWhere((e) => e.id == assetId);
 | |
|       }
 | |
| 
 | |
|       if (assetsWillBeBackup.isEmpty) {
 | |
|         state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
 | |
|       }
 | |
| 
 | |
|       // Perform Backup
 | |
|       state = state.copyWith(cancelToken: CancelToken());
 | |
|       _backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
 | |
|     } else {
 | |
|       PhotoManager.openSetting();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void cancelBackup() {
 | |
|     state.cancelToken.cancel('Cancel Backup');
 | |
|     state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
 | |
|   }
 | |
| 
 | |
|   void _onAssetUploaded(String deviceAssetId, String deviceId) {
 | |
|     state = state.copyWith(
 | |
|         selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, deviceAssetId},
 | |
|         allAssetOnDatabase: [...state.allAssetOnDatabase, deviceAssetId]);
 | |
| 
 | |
|     if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) {
 | |
|       state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
 | |
|     }
 | |
| 
 | |
|     _updateServerInfo();
 | |
|   }
 | |
| 
 | |
|   void _onUploadProgress(int sent, int total) {
 | |
|     state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
 | |
|   }
 | |
| 
 | |
|   void _updateServerInfo() async {
 | |
|     var serverInfo = await _serverInfoService.getServerInfo();
 | |
| 
 | |
|     // Update server info
 | |
|     state = state.copyWith(
 | |
|       serverInfo: ServerInfo(
 | |
|         diskSize: serverInfo.diskSize,
 | |
|         diskUse: serverInfo.diskUse,
 | |
|         diskAvailable: serverInfo.diskAvailable,
 | |
|         diskSizeRaw: serverInfo.diskSizeRaw,
 | |
|         diskUseRaw: serverInfo.diskUseRaw,
 | |
|         diskAvailableRaw: serverInfo.diskAvailableRaw,
 | |
|         diskUsagePercentage: serverInfo.diskUsagePercentage,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void resumeBackup() {
 | |
|     var authState = ref?.read(authenticationProvider);
 | |
| 
 | |
|     // Check if user is login
 | |
|     var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
 | |
| 
 | |
|     // User has been logged out return
 | |
|     if (authState != null) {
 | |
|       if (accessKey == null || !authState.isAuthenticated) {
 | |
|         debugPrint("[resumeBackup] not authenticated - abort");
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Check if this device is enable backup by the user
 | |
|       if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
 | |
|         // check if backup is alreayd in process - then return
 | |
|         if (state.backupProgress == BackUpProgressEnum.inProgress) {
 | |
|           debugPrint("[resumeBackup] Backup is already in progress - abort");
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         // Run backup
 | |
|         debugPrint("[resumeBackup] Start back up");
 | |
|         startBackupProcess();
 | |
|       }
 | |
| 
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
 | |
|   return BackupNotifier(ref: ref);
 | |
| });
 |