17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@ -32,8 +32,9 @@ Loading ~4000 images/videos
 | 
				
			|||||||
## Screenshots
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<p align="left">
 | 
					<p align="left">
 | 
				
			||||||
  <img src="design/nsc1.png" width="150" title="Login With Custom URL">
 | 
					  <img src="design/login-screen.png" width="150" title="Login With Custom URL">
 | 
				
			||||||
  <img src="design/nsc2.png" width="150" title="Backup Setting Info">
 | 
					  <img src="design/backup-screen.png" width="150" title="Backup Setting Info">
 | 
				
			||||||
 | 
					  <img src="design/selective-backup-screen.png" width="150" title="Backup Setting Info">
 | 
				
			||||||
  <img src="design/home-screen.jpeg" width="150" title="Home Screen">
 | 
					  <img src="design/home-screen.jpeg" width="150" title="Home Screen">
 | 
				
			||||||
  <img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
 | 
					  <img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
 | 
				
			||||||
  <img src="design/shared-albums.png" width="150" title="Shared Albums">
 | 
					  <img src="design/shared-albums.png" width="150" title="Shared Albums">
 | 
				
			||||||
@ -50,10 +51,10 @@ This project is under heavy development, there will be continous functions, feat
 | 
				
			|||||||
# Features
 | 
					# Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Upload and view assets (videos/images).
 | 
					- Upload and view assets (videos/images).
 | 
				
			||||||
 | 
					- Auto Backup.
 | 
				
			||||||
- Download asset to local device.
 | 
					- Download asset to local device.
 | 
				
			||||||
- Multi-user supported.
 | 
					- Multi-user supported.
 | 
				
			||||||
- Quick navigation with drag scroll bar.
 | 
					- Quick navigation with drag scroll bar.
 | 
				
			||||||
- Auto Backup.
 | 
					 | 
				
			||||||
- Support HEIC/HEIF Backup.
 | 
					- Support HEIC/HEIF Backup.
 | 
				
			||||||
- Extract and display EXIF info.
 | 
					- Extract and display EXIF info.
 | 
				
			||||||
- Real-time render from multi-device upload event.
 | 
					- Real-time render from multi-device upload event.
 | 
				
			||||||
@ -65,14 +66,20 @@ This project is under heavy development, there will be continous functions, feat
 | 
				
			|||||||
- Show curated places on the search page
 | 
					- Show curated places on the search page
 | 
				
			||||||
- Show curated objects on the search page
 | 
					- Show curated objects on the search page
 | 
				
			||||||
- Shared album with users on the same server
 | 
					- Shared album with users on the same server
 | 
				
			||||||
 | 
					- Selective backup - albums can be included and excluded during the backup process.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# System Requirement
 | 
					# System Requirement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**OS**: Preferred Linux-based operating system (Ubuntu, Debian, MacOS...etc). I haven't tested with `Docker for Windows` as well as `WSL` on Windows
 | 
					**OS**: Preferred Linux-based operating system (Ubuntu, Debian, MacOS...etc). 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					I haven't tested with `Docker for Windows` as well as `WSL` on Windows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Dockec image on arm64v7 yet.*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**RAM**: At least 2GB, preffered 4GB.
 | 
					**RAM**: At least 2GB, preffered 4GB.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Cores**: At least 2 cores, preffered 4 cores.
 | 
					**Core**: At least 2 cores, preffered 4 cores.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Development and Testing out the application
 | 
					# Development and Testing out the application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								design/backup-screen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 308 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								design/login-screen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 278 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								design/nsc1.png
									
									
									
									
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 176 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								design/nsc2.png
									
									
									
									
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 303 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								design/selective-backup-screen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 570 KiB  | 
@ -2,7 +2,7 @@ version: "3.8"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
services:
 | 
					services:
 | 
				
			||||||
  immich_server:
 | 
					  immich_server:
 | 
				
			||||||
    image: immich-server-dev:1.8.0
 | 
					    image: immich-server-dev:1.9.0
 | 
				
			||||||
    build:
 | 
					    build:
 | 
				
			||||||
      context: ../server
 | 
					      context: ../server
 | 
				
			||||||
      dockerfile: Dockerfile
 | 
					      dockerfile: Dockerfile
 | 
				
			||||||
@ -24,7 +24,7 @@ services:
 | 
				
			|||||||
      - immich_network
 | 
					      - immich_network
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  immich_microservices:
 | 
					  immich_microservices:
 | 
				
			||||||
    image: immich-microservices-dev:1.8.0
 | 
					    image: immich-microservices-dev:1.9.0
 | 
				
			||||||
    build:
 | 
					    build:
 | 
				
			||||||
      context: ../microservices
 | 
					      context: ../microservices
 | 
				
			||||||
      dockerfile: Dockerfile
 | 
					      dockerfile: Dockerfile
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ version: "3.8"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
services:
 | 
					services:
 | 
				
			||||||
  immich_server:
 | 
					  immich_server:
 | 
				
			||||||
    image: immich-server-dev:1.8.0
 | 
					    image: immich-server-dev:1.9.0
 | 
				
			||||||
    build:
 | 
					    build:
 | 
				
			||||||
      context: ../server
 | 
					      context: ../server
 | 
				
			||||||
      dockerfile: Dockerfile
 | 
					      dockerfile: Dockerfile
 | 
				
			||||||
@ -22,7 +22,7 @@ services:
 | 
				
			|||||||
      - immich_network
 | 
					      - immich_network
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  immich_microservices:
 | 
					  immich_microservices:
 | 
				
			||||||
    image: immich-microservices-dev:1.8.0
 | 
					    image: immich-microservices-dev:1.9.0
 | 
				
			||||||
    build:
 | 
					    build:
 | 
				
			||||||
      context: ../microservices
 | 
					      context: ../microservices
 | 
				
			||||||
      dockerfile: Dockerfile
 | 
					      dockerfile: Dockerfile
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					* New Feature - Selection backup. User can now select a combination of albums to be included or excluded during the backup process, and only unique photos, and videos that are not overlapping between the two groups will be backup.
 | 
				
			||||||
 | 
					* Bug fix - Show correct count of backup and remainder assets.
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 570 KiB  | 
| 
		 Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 308 KiB  | 
| 
		 Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 74 KiB  | 
| 
		 After Width: | Height: | Size: 183 KiB  | 
@ -19,7 +19,7 @@ platform :ios do
 | 
				
			|||||||
  desc "iOS Beta"
 | 
					  desc "iOS Beta"
 | 
				
			||||||
  lane :beta do
 | 
					  lane :beta do
 | 
				
			||||||
    increment_version_number(
 | 
					    increment_version_number(
 | 
				
			||||||
      version_number: "1.8.0"
 | 
					      version_number: "1.9.0"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    increment_build_number(
 | 
					    increment_build_number(
 | 
				
			||||||
      build_number: latest_testflight_build_number + 1,
 | 
					      build_number: latest_testflight_build_number + 1,
 | 
				
			||||||
 | 
				
			|||||||
@ -9,3 +9,7 @@ const String serverEndpointKey = 'immichBoxServerEndpoint';
 | 
				
			|||||||
// Login Info
 | 
					// Login Info
 | 
				
			||||||
const String hiveLoginInfoBox = "immichLoginInfoBox";
 | 
					const String hiveLoginInfoBox = "immichLoginInfoBox";
 | 
				
			||||||
const String savedLoginInfoKey = "immichSavedLoginInfoKey";
 | 
					const String savedLoginInfoKey = "immichSavedLoginInfoKey";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Backup Info
 | 
				
			||||||
 | 
					const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
 | 
				
			||||||
 | 
					const String backupInfoKey = "immichBackupAlbumInfoKey";
 | 
				
			||||||
 | 
				
			|||||||
@ -3,12 +3,13 @@ import 'package:flutter/services.dart';
 | 
				
			|||||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
					import 'package:hive_flutter/hive_flutter.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:immich_mobile/constants/immich_colors.dart';
 | 
					import 'package:immich_mobile/constants/immich_colors.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
 | 
					import 'package:immich_mobile/routing/tab_navigation_observer.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/app_state.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
					import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 | 
					import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 | 
				
			||||||
@ -16,9 +17,13 @@ import 'constants/hive_box.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
void main() async {
 | 
					void main() async {
 | 
				
			||||||
  await Hive.initFlutter();
 | 
					  await Hive.initFlutter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Hive.registerAdapter(HiveSavedLoginInfoAdapter());
 | 
					  Hive.registerAdapter(HiveSavedLoginInfoAdapter());
 | 
				
			||||||
 | 
					  Hive.registerAdapter(HiveBackupAlbumsAdapter());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await Hive.openBox(userInfoBox);
 | 
					  await Hive.openBox(userInfoBox);
 | 
				
			||||||
  await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
 | 
					  await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
 | 
				
			||||||
 | 
					  await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SystemChrome.setSystemUIOverlayStyle(
 | 
					  SystemChrome.setSystemUIOverlayStyle(
 | 
				
			||||||
    const SystemUiOverlayStyle(
 | 
					    const SystemUiOverlayStyle(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										35
									
								
								mobile/lib/modules/backup/models/available_album.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AvailableAlbum {
 | 
				
			||||||
 | 
					  final AssetPathEntity albumEntity;
 | 
				
			||||||
 | 
					  final Uint8List? thumbnailData;
 | 
				
			||||||
 | 
					  AvailableAlbum({
 | 
				
			||||||
 | 
					    required this.albumEntity,
 | 
				
			||||||
 | 
					    this.thumbnailData,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AvailableAlbum copyWith({
 | 
				
			||||||
 | 
					    AssetPathEntity? albumEntity,
 | 
				
			||||||
 | 
					    Uint8List? thumbnailData,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return AvailableAlbum(
 | 
				
			||||||
 | 
					      albumEntity: albumEntity ?? this.albumEntity,
 | 
				
			||||||
 | 
					      thumbnailData: thumbnailData ?? this.thumbnailData,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return other is AvailableAlbum && other.albumEntity == albumEntity && other.thumbnailData == thumbnailData;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode => albumEntity.hashCode ^ thumbnailData.hashCode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								mobile/lib/modules/backup/models/backup_state.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
 | 
					import 'package:equatable/equatable.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/server_info.model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum BackUpProgressEnum { idle, inProgress, done }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BackUpState extends Equatable {
 | 
				
			||||||
 | 
					  // enum
 | 
				
			||||||
 | 
					  final BackUpProgressEnum backupProgress;
 | 
				
			||||||
 | 
					  final List<String> allAssetOnDatabase;
 | 
				
			||||||
 | 
					  final double progressInPercentage;
 | 
				
			||||||
 | 
					  final CancelToken cancelToken;
 | 
				
			||||||
 | 
					  final ServerInfo serverInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// All available albums on the device
 | 
				
			||||||
 | 
					  final List<AvailableAlbum> availableAlbums;
 | 
				
			||||||
 | 
					  final Set<AssetPathEntity> selectedBackupAlbums;
 | 
				
			||||||
 | 
					  final Set<AssetPathEntity> excludedBackupAlbums;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Assets that are not overlapping in selected backup albums and excluded backup albums
 | 
				
			||||||
 | 
					  final Set<AssetEntity> allUniqueAssets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// All assets from the selected albums that have been backup
 | 
				
			||||||
 | 
					  final Set<String> selectedAlbumsBackupAssetsIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const BackUpState({
 | 
				
			||||||
 | 
					    required this.backupProgress,
 | 
				
			||||||
 | 
					    required this.allAssetOnDatabase,
 | 
				
			||||||
 | 
					    required this.progressInPercentage,
 | 
				
			||||||
 | 
					    required this.cancelToken,
 | 
				
			||||||
 | 
					    required this.serverInfo,
 | 
				
			||||||
 | 
					    required this.availableAlbums,
 | 
				
			||||||
 | 
					    required this.selectedBackupAlbums,
 | 
				
			||||||
 | 
					    required this.excludedBackupAlbums,
 | 
				
			||||||
 | 
					    required this.allUniqueAssets,
 | 
				
			||||||
 | 
					    required this.selectedAlbumsBackupAssetsIds,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  BackUpState copyWith({
 | 
				
			||||||
 | 
					    BackUpProgressEnum? backupProgress,
 | 
				
			||||||
 | 
					    List<String>? allAssetOnDatabase,
 | 
				
			||||||
 | 
					    double? progressInPercentage,
 | 
				
			||||||
 | 
					    CancelToken? cancelToken,
 | 
				
			||||||
 | 
					    ServerInfo? serverInfo,
 | 
				
			||||||
 | 
					    List<AvailableAlbum>? availableAlbums,
 | 
				
			||||||
 | 
					    Set<AssetPathEntity>? selectedBackupAlbums,
 | 
				
			||||||
 | 
					    Set<AssetPathEntity>? excludedBackupAlbums,
 | 
				
			||||||
 | 
					    Set<AssetEntity>? allUniqueAssets,
 | 
				
			||||||
 | 
					    Set<String>? selectedAlbumsBackupAssetsIds,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return BackUpState(
 | 
				
			||||||
 | 
					      backupProgress: backupProgress ?? this.backupProgress,
 | 
				
			||||||
 | 
					      allAssetOnDatabase: allAssetOnDatabase ?? this.allAssetOnDatabase,
 | 
				
			||||||
 | 
					      progressInPercentage: progressInPercentage ?? this.progressInPercentage,
 | 
				
			||||||
 | 
					      cancelToken: cancelToken ?? this.cancelToken,
 | 
				
			||||||
 | 
					      serverInfo: serverInfo ?? this.serverInfo,
 | 
				
			||||||
 | 
					      availableAlbums: availableAlbums ?? this.availableAlbums,
 | 
				
			||||||
 | 
					      selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
 | 
				
			||||||
 | 
					      excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
 | 
				
			||||||
 | 
					      allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
 | 
				
			||||||
 | 
					      selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'BackUpState(backupProgress: $backupProgress, allAssetOnDatabase: $allAssetOnDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<Object> get props {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      backupProgress,
 | 
				
			||||||
 | 
					      allAssetOnDatabase,
 | 
				
			||||||
 | 
					      progressInPercentage,
 | 
				
			||||||
 | 
					      cancelToken,
 | 
				
			||||||
 | 
					      serverInfo,
 | 
				
			||||||
 | 
					      availableAlbums,
 | 
				
			||||||
 | 
					      selectedBackupAlbums,
 | 
				
			||||||
 | 
					      excludedBackupAlbums,
 | 
				
			||||||
 | 
					      allUniqueAssets,
 | 
				
			||||||
 | 
					      selectedAlbumsBackupAssetsIds,
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:hive/hive.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'hive_backup_albums.model.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@HiveType(typeId: 1)
 | 
				
			||||||
 | 
					class HiveBackupAlbums {
 | 
				
			||||||
 | 
					  @HiveField(0)
 | 
				
			||||||
 | 
					  List<String> selectedAlbumIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @HiveField(1)
 | 
				
			||||||
 | 
					  List<String> excludedAlbumsIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  HiveBackupAlbums({
 | 
				
			||||||
 | 
					    required this.selectedAlbumIds,
 | 
				
			||||||
 | 
					    required this.excludedAlbumsIds,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  HiveBackupAlbums copyWith({
 | 
				
			||||||
 | 
					    List<String>? selectedAlbumIds,
 | 
				
			||||||
 | 
					    List<String>? excludedAlbumsIds,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return HiveBackupAlbums(
 | 
				
			||||||
 | 
					      selectedAlbumIds: selectedAlbumIds ?? this.selectedAlbumIds,
 | 
				
			||||||
 | 
					      excludedAlbumsIds: excludedAlbumsIds ?? this.excludedAlbumsIds,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toMap() {
 | 
				
			||||||
 | 
					    final result = <String, dynamic>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result.addAll({'selectedAlbumIds': selectedAlbumIds});
 | 
				
			||||||
 | 
					    result.addAll({'excludedAlbumsIds': excludedAlbumsIds});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory HiveBackupAlbums.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
 | 
					    return HiveBackupAlbums(
 | 
				
			||||||
 | 
					      selectedAlbumIds: List<String>.from(map['selectedAlbumIds']),
 | 
				
			||||||
 | 
					      excludedAlbumsIds: List<String>.from(map['excludedAlbumsIds']),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory HiveBackupAlbums.fromJson(String source) => HiveBackupAlbums.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					    final listEquals = const DeepCollectionEquality().equals;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return other is HiveBackupAlbums &&
 | 
				
			||||||
 | 
					        listEquals(other.selectedAlbumIds, selectedAlbumIds) &&
 | 
				
			||||||
 | 
					        listEquals(other.excludedAlbumsIds, excludedAlbumsIds);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode => selectedAlbumIds.hashCode ^ excludedAlbumsIds.hashCode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'hive_backup_albums.model.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					// TypeAdapterGenerator
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> {
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  final int typeId = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  HiveBackupAlbums read(BinaryReader reader) {
 | 
				
			||||||
 | 
					    final numOfFields = reader.readByte();
 | 
				
			||||||
 | 
					    final fields = <int, dynamic>{
 | 
				
			||||||
 | 
					      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    return HiveBackupAlbums(
 | 
				
			||||||
 | 
					      selectedAlbumIds: (fields[0] as List).cast<String>(),
 | 
				
			||||||
 | 
					      excludedAlbumsIds: (fields[1] as List).cast<String>(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void write(BinaryWriter writer, HiveBackupAlbums obj) {
 | 
				
			||||||
 | 
					    writer
 | 
				
			||||||
 | 
					      ..writeByte(2)
 | 
				
			||||||
 | 
					      ..writeByte(0)
 | 
				
			||||||
 | 
					      ..write(obj.selectedAlbumIds)
 | 
				
			||||||
 | 
					      ..writeByte(1)
 | 
				
			||||||
 | 
					      ..write(obj.excludedAlbumsIds);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode => typeId.hashCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) =>
 | 
				
			||||||
 | 
					      identical(this, other) ||
 | 
				
			||||||
 | 
					      other is HiveBackupAlbumsAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										347
									
								
								mobile/lib/modules/backup/providers/backup.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,347 @@
 | 
				
			|||||||
 | 
					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);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -26,7 +26,7 @@ class BackupService {
 | 
				
			|||||||
    return result.cast<String>();
 | 
					    return result.cast<String>();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  backupAsset(List<AssetEntity> assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb,
 | 
					  backupAsset(Set<AssetEntity> assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb,
 | 
				
			||||||
      Function(int, int) uploadProgress) async {
 | 
					      Function(int, int) uploadProgress) async {
 | 
				
			||||||
    var dio = Dio();
 | 
					    var dio = Dio();
 | 
				
			||||||
    dio.interceptors.add(AuthenticatedRequestInterceptor());
 | 
					    dio.interceptors.add(AuthenticatedRequestInterceptor());
 | 
				
			||||||
							
								
								
									
										185
									
								
								mobile/lib/modules/backup/ui/album_info_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,185 @@
 | 
				
			|||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:fluttertoast/fluttertoast.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  final Uint8List? imageData;
 | 
				
			||||||
 | 
					  final AssetPathEntity albumInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
 | 
				
			||||||
 | 
					    final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ColorFilter selectedFilter = ColorFilter.mode(Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
 | 
				
			||||||
 | 
					    ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
 | 
				
			||||||
 | 
					    ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _buildSelectedTextBox() {
 | 
				
			||||||
 | 
					      if (isSelected) {
 | 
				
			||||||
 | 
					        return Chip(
 | 
				
			||||||
 | 
					          visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
 | 
					          label: const Text(
 | 
				
			||||||
 | 
					            "INCLUDED",
 | 
				
			||||||
 | 
					            style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          backgroundColor: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else if (isExcluded) {
 | 
				
			||||||
 | 
					        return Chip(
 | 
				
			||||||
 | 
					          visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
 | 
					          label: const Text(
 | 
				
			||||||
 | 
					            "EXCLUDED",
 | 
				
			||||||
 | 
					            style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          backgroundColor: Colors.red[300],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return Container();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _buildImageFilter() {
 | 
				
			||||||
 | 
					      if (isSelected) {
 | 
				
			||||||
 | 
					        return selectedFilter;
 | 
				
			||||||
 | 
					      } else if (isExcluded) {
 | 
				
			||||||
 | 
					        return excludedFilter;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return unselectedFilter;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return GestureDetector(
 | 
				
			||||||
 | 
					      onTap: () {
 | 
				
			||||||
 | 
					        HapticFeedback.selectionClick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isSelected) {
 | 
				
			||||||
 | 
					          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
 | 
				
			||||||
 | 
					            ImmichToast.show(
 | 
				
			||||||
 | 
					              context: context,
 | 
				
			||||||
 | 
					              msg: "Cannot remove the only album",
 | 
				
			||||||
 | 
					              toastType: ToastType.error,
 | 
				
			||||||
 | 
					              gravity: ToastGravity.BOTTOM,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onDoubleTap: () {
 | 
				
			||||||
 | 
					        HapticFeedback.selectionClick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isExcluded) {
 | 
				
			||||||
 | 
					          ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(albumInfo);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
 | 
				
			||||||
 | 
					              ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo)) {
 | 
				
			||||||
 | 
					            ImmichToast.show(
 | 
				
			||||||
 | 
					              context: context,
 | 
				
			||||||
 | 
					              msg: "Cannot exclude the only album",
 | 
				
			||||||
 | 
					              toastType: ToastType.error,
 | 
				
			||||||
 | 
					              gravity: ToastGravity.BOTTOM,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ref.watch(backupProvider.notifier).addExcludedAlbumForBackup(albumInfo);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      child: Card(
 | 
				
			||||||
 | 
					        margin: const EdgeInsets.all(1),
 | 
				
			||||||
 | 
					        shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					          borderRadius: BorderRadius.circular(12), // if you need this
 | 
				
			||||||
 | 
					          side: const BorderSide(
 | 
				
			||||||
 | 
					            color: Color(0xFFC9C9C9),
 | 
				
			||||||
 | 
					            width: 1,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        elevation: 0,
 | 
				
			||||||
 | 
					        borderOnForeground: false,
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            Stack(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Container(
 | 
				
			||||||
 | 
					                  width: 200,
 | 
				
			||||||
 | 
					                  height: 200,
 | 
				
			||||||
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                    borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
 | 
				
			||||||
 | 
					                    image: DecorationImage(
 | 
				
			||||||
 | 
					                      colorFilter: _buildImageFilter(),
 | 
				
			||||||
 | 
					                      image: imageData != null
 | 
				
			||||||
 | 
					                          ? MemoryImage(imageData!)
 | 
				
			||||||
 | 
					                          : const AssetImage('assets/immich-logo-no-outline.png') as ImageProvider,
 | 
				
			||||||
 | 
					                      fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  child: null,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                Positioned(bottom: 10, left: 25, child: _buildSelectedTextBox())
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.only(top: 8.0),
 | 
				
			||||||
 | 
					              child: Row(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  SizedBox(
 | 
				
			||||||
 | 
					                    width: 140,
 | 
				
			||||||
 | 
					                    child: Padding(
 | 
				
			||||||
 | 
					                      padding: const EdgeInsets.only(left: 25.0),
 | 
				
			||||||
 | 
					                      child: Column(
 | 
				
			||||||
 | 
					                        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                        children: [
 | 
				
			||||||
 | 
					                          Text(
 | 
				
			||||||
 | 
					                            albumInfo.name,
 | 
				
			||||||
 | 
					                            style: TextStyle(
 | 
				
			||||||
 | 
					                                fontSize: 14, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          Padding(
 | 
				
			||||||
 | 
					                            padding: const EdgeInsets.only(top: 2.0),
 | 
				
			||||||
 | 
					                            child: Text(
 | 
				
			||||||
 | 
					                              albumInfo.assetCount.toString() + (albumInfo.isAll ? " (ALL)" : ""),
 | 
				
			||||||
 | 
					                              style: TextStyle(fontSize: 12, color: Colors.grey[600]),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          )
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  IconButton(
 | 
				
			||||||
 | 
					                    onPressed: () {
 | 
				
			||||||
 | 
					                      AutoRouter.of(context).push(AlbumPreviewRoute(album: albumInfo));
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    icon: Icon(
 | 
				
			||||||
 | 
					                      Icons.image_outlined,
 | 
				
			||||||
 | 
					                      color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					                      size: 24,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    splashRadius: 25,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								mobile/lib/modules/backup/ui/backup_info_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BackupInfoCard extends StatelessWidget {
 | 
				
			||||||
 | 
					  final String title;
 | 
				
			||||||
 | 
					  final String subtitle;
 | 
				
			||||||
 | 
					  final String info;
 | 
				
			||||||
 | 
					  const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Card(
 | 
				
			||||||
 | 
					      shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					        borderRadius: BorderRadius.circular(5), // if you need this
 | 
				
			||||||
 | 
					        side: const BorderSide(
 | 
				
			||||||
 | 
					          color: Colors.black12,
 | 
				
			||||||
 | 
					          width: 1,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      elevation: 0,
 | 
				
			||||||
 | 
					      borderOnForeground: false,
 | 
				
			||||||
 | 
					      child: ListTile(
 | 
				
			||||||
 | 
					        minVerticalPadding: 15,
 | 
				
			||||||
 | 
					        isThreeLine: true,
 | 
				
			||||||
 | 
					        title: Text(
 | 
				
			||||||
 | 
					          title,
 | 
				
			||||||
 | 
					          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        subtitle: Padding(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.only(top: 8.0),
 | 
				
			||||||
 | 
					          child: Text(
 | 
				
			||||||
 | 
					            subtitle,
 | 
				
			||||||
 | 
					            style: const TextStyle(color: Color(0xFF808080), fontSize: 12),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        trailing: Column(
 | 
				
			||||||
 | 
					          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            Text(
 | 
				
			||||||
 | 
					              info,
 | 
				
			||||||
 | 
					              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            const Text("assets"),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										84
									
								
								mobile/lib/modules/backup/views/album_preview_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AlbumPreviewPage extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  final AssetPathEntity album;
 | 
				
			||||||
 | 
					  const AlbumPreviewPage({Key? key, required this.album}) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final assets = useState<List<AssetEntity>>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _getAssetsInAlbum() async {
 | 
				
			||||||
 | 
					      assets.value = await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      _getAssetsInAlbum();
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        elevation: 0,
 | 
				
			||||||
 | 
					        title: Column(
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            Text(
 | 
				
			||||||
 | 
					              "${album.name} (${album.assetCount})",
 | 
				
			||||||
 | 
					              style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.only(top: 4.0),
 | 
				
			||||||
 | 
					              child: Text(
 | 
				
			||||||
 | 
					                "ID ${album.id}",
 | 
				
			||||||
 | 
					                style: TextStyle(fontSize: 10, color: Colors.grey[600], fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        leading: IconButton(
 | 
				
			||||||
 | 
					          onPressed: () => AutoRouter.of(context).pop(),
 | 
				
			||||||
 | 
					          icon: const Icon(Icons.arrow_back_ios_new_rounded),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: GridView.builder(
 | 
				
			||||||
 | 
					        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
 | 
				
			||||||
 | 
					          crossAxisCount: 5,
 | 
				
			||||||
 | 
					          crossAxisSpacing: 2,
 | 
				
			||||||
 | 
					          mainAxisSpacing: 2,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        itemCount: assets.value.length,
 | 
				
			||||||
 | 
					        itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					          Future<Uint8List?> thumbData =
 | 
				
			||||||
 | 
					              assets.value[index].thumbnailDataWithSize(const ThumbnailSize(200, 200), quality: 50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return FutureBuilder<Uint8List?>(
 | 
				
			||||||
 | 
					            future: thumbData,
 | 
				
			||||||
 | 
					            builder: ((context, snapshot) {
 | 
				
			||||||
 | 
					              if (snapshot.hasData && snapshot.data != null) {
 | 
				
			||||||
 | 
					                return Image.memory(
 | 
				
			||||||
 | 
					                  snapshot.data!,
 | 
				
			||||||
 | 
					                  width: 100,
 | 
				
			||||||
 | 
					                  height: 100,
 | 
				
			||||||
 | 
					                  fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return const SizedBox(
 | 
				
			||||||
 | 
					                width: 100,
 | 
				
			||||||
 | 
					                height: 100,
 | 
				
			||||||
 | 
					                child: ImmichLoadingIndicator(),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										244
									
								
								mobile/lib/modules/backup/views/backup_album_selection_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,244 @@
 | 
				
			|||||||
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:fluttertoast/fluttertoast.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					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/shared/ui/immich_loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  const BackupAlbumSelectionPage({Key? key}) : super(key: key);
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final availableAlbums = ref.watch(backupProvider).availableAlbums;
 | 
				
			||||||
 | 
					    final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
 | 
				
			||||||
 | 
					    final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      ref.read(backupProvider.notifier).getBackupAlbumsInfo();
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _buildAlbumSelectionList() {
 | 
				
			||||||
 | 
					      if (availableAlbums.isEmpty) {
 | 
				
			||||||
 | 
					        return const Center(
 | 
				
			||||||
 | 
					          child: ImmichLoadingIndicator(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SizedBox(
 | 
				
			||||||
 | 
					        height: 265,
 | 
				
			||||||
 | 
					        child: ListView.builder(
 | 
				
			||||||
 | 
					          scrollDirection: Axis.horizontal,
 | 
				
			||||||
 | 
					          itemCount: availableAlbums.length,
 | 
				
			||||||
 | 
					          physics: const BouncingScrollPhysics(),
 | 
				
			||||||
 | 
					          itemBuilder: ((context, index) {
 | 
				
			||||||
 | 
					            var thumbnailData = availableAlbums[index].thumbnailData;
 | 
				
			||||||
 | 
					            return Padding(
 | 
				
			||||||
 | 
					              padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0),
 | 
				
			||||||
 | 
					              child: AlbumInfoCard(imageData: thumbnailData, albumInfo: availableAlbums[index].albumEntity),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _buildSelectedAlbumNameChip() {
 | 
				
			||||||
 | 
					      return selectedBackupAlbums.map((album) {
 | 
				
			||||||
 | 
					        void removeSelection() {
 | 
				
			||||||
 | 
					          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
 | 
				
			||||||
 | 
					            ImmichToast.show(
 | 
				
			||||||
 | 
					              context: context,
 | 
				
			||||||
 | 
					              msg: "Cannot remove the only album",
 | 
				
			||||||
 | 
					              toastType: ToastType.error,
 | 
				
			||||||
 | 
					              gravity: ToastGravity.BOTTOM,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ref.watch(backupProvider.notifier).removeAlbumForBackup(album);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Padding(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.only(right: 8.0),
 | 
				
			||||||
 | 
					          child: GestureDetector(
 | 
				
			||||||
 | 
					            onTap: removeSelection,
 | 
				
			||||||
 | 
					            child: Chip(
 | 
				
			||||||
 | 
					              visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
 | 
					              label: Text(
 | 
				
			||||||
 | 
					                album.name,
 | 
				
			||||||
 | 
					                style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              backgroundColor: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					              deleteIconColor: Colors.white,
 | 
				
			||||||
 | 
					              deleteIcon: const Icon(
 | 
				
			||||||
 | 
					                Icons.cancel_rounded,
 | 
				
			||||||
 | 
					                size: 15,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onDeleted: removeSelection,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }).toSet();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _buildExcludedAlbumNameChip() {
 | 
				
			||||||
 | 
					      return excludedBackupAlbums.map((album) {
 | 
				
			||||||
 | 
					        void removeSelection() {
 | 
				
			||||||
 | 
					          ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return GestureDetector(
 | 
				
			||||||
 | 
					          onTap: removeSelection,
 | 
				
			||||||
 | 
					          child: Padding(
 | 
				
			||||||
 | 
					            padding: const EdgeInsets.only(right: 8.0),
 | 
				
			||||||
 | 
					            child: Chip(
 | 
				
			||||||
 | 
					              visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
 | 
					              label: Text(
 | 
				
			||||||
 | 
					                album.name,
 | 
				
			||||||
 | 
					                style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              backgroundColor: Colors.red[300],
 | 
				
			||||||
 | 
					              deleteIconColor: Colors.white,
 | 
				
			||||||
 | 
					              deleteIcon: const Icon(
 | 
				
			||||||
 | 
					                Icons.cancel_rounded,
 | 
				
			||||||
 | 
					                size: 15,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onDeleted: removeSelection,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }).toSet();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: IconButton(
 | 
				
			||||||
 | 
					          onPressed: () => AutoRouter.of(context).pop(),
 | 
				
			||||||
 | 
					          icon: const Icon(Icons.arrow_back_ios_rounded),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        title: const Text(
 | 
				
			||||||
 | 
					          "Select Albums",
 | 
				
			||||||
 | 
					          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        elevation: 0,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: ListView(
 | 
				
			||||||
 | 
					        physics: const ClampingScrollPhysics(),
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          const Padding(
 | 
				
			||||||
 | 
					            padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
 | 
				
			||||||
 | 
					            child: Text(
 | 
				
			||||||
 | 
					              "Selection Info",
 | 
				
			||||||
 | 
					              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          // Selected Album Chips
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          Padding(
 | 
				
			||||||
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
				
			||||||
 | 
					            child: Wrap(
 | 
				
			||||||
 | 
					              children: [..._buildSelectedAlbumNameChip(), ..._buildExcludedAlbumNameChip()],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          Padding(
 | 
				
			||||||
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
 | 
				
			||||||
 | 
					            child: Card(
 | 
				
			||||||
 | 
					              margin: const EdgeInsets.all(0),
 | 
				
			||||||
 | 
					              shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                borderRadius: BorderRadius.circular(5), // if you need this
 | 
				
			||||||
 | 
					                side: const BorderSide(
 | 
				
			||||||
 | 
					                  color: Color.fromARGB(255, 235, 235, 235),
 | 
				
			||||||
 | 
					                  width: 1,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              elevation: 0,
 | 
				
			||||||
 | 
					              borderOnForeground: false,
 | 
				
			||||||
 | 
					              child: Column(
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  ListTile(
 | 
				
			||||||
 | 
					                    visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					                    title: Text(
 | 
				
			||||||
 | 
					                      "Total unique assets",
 | 
				
			||||||
 | 
					                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    trailing: Text(
 | 
				
			||||||
 | 
					                      ref.watch(backupProvider).allUniqueAssets.length.toString(),
 | 
				
			||||||
 | 
					                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ListTile(
 | 
				
			||||||
 | 
					            title: Text(
 | 
				
			||||||
 | 
					              "Albums on device (${availableAlbums.length.toString()})",
 | 
				
			||||||
 | 
					              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            subtitle: Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.symmetric(vertical: 8.0),
 | 
				
			||||||
 | 
					              child: Text(
 | 
				
			||||||
 | 
					                "Tap to include, double tap to exclude",
 | 
				
			||||||
 | 
					                style: TextStyle(
 | 
				
			||||||
 | 
					                  fontSize: 12,
 | 
				
			||||||
 | 
					                  color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					                  fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            trailing: IconButton(
 | 
				
			||||||
 | 
					              splashRadius: 16,
 | 
				
			||||||
 | 
					              icon: Icon(
 | 
				
			||||||
 | 
					                Icons.info,
 | 
				
			||||||
 | 
					                size: 20,
 | 
				
			||||||
 | 
					                color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onPressed: () {
 | 
				
			||||||
 | 
					                // show the dialog
 | 
				
			||||||
 | 
					                showDialog(
 | 
				
			||||||
 | 
					                  context: context,
 | 
				
			||||||
 | 
					                  builder: (BuildContext context) {
 | 
				
			||||||
 | 
					                    return AlertDialog(
 | 
				
			||||||
 | 
					                      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
 | 
				
			||||||
 | 
					                      elevation: 5,
 | 
				
			||||||
 | 
					                      title: Text(
 | 
				
			||||||
 | 
					                        'Selection Info',
 | 
				
			||||||
 | 
					                        style: TextStyle(
 | 
				
			||||||
 | 
					                          fontSize: 16,
 | 
				
			||||||
 | 
					                          fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                          color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      content: SingleChildScrollView(
 | 
				
			||||||
 | 
					                        child: ListBody(
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            Text(
 | 
				
			||||||
 | 
					                              'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
 | 
				
			||||||
 | 
					                              style: TextStyle(fontSize: 14, color: Colors.grey[700]),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          Padding(
 | 
				
			||||||
 | 
					            padding: const EdgeInsets.only(bottom: 16.0),
 | 
				
			||||||
 | 
					            child: _buildAlbumSelectionList(),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,10 +3,12 @@ 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/modules/login/models/authentication_state.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
 | 
					import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
					import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
					import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
 | 
				
			||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
 | 
					import 'package:percent_indicator/linear_percent_indicator.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BackupControllerPage extends HookConsumerWidget {
 | 
					class BackupControllerPage extends HookConsumerWidget {
 | 
				
			||||||
@ -14,13 +16,13 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    BackUpState _backupState = ref.watch(backupProvider);
 | 
					    BackUpState backupState = ref.watch(backupProvider);
 | 
				
			||||||
    AuthenticationState _authenticationState = ref.watch(authenticationProvider);
 | 
					    AuthenticationState _authenticationState = ref.watch(authenticationProvider);
 | 
				
			||||||
 | 
					    bool shouldBackup =
 | 
				
			||||||
    bool shouldBackup = _backupState.totalAssetCount - _backupState.assetOnDatabase == 0 ? false : true;
 | 
					        backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 ? false : true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      if (_backupState.backupProgress != BackUpProgressEnum.inProgress) {
 | 
					      if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
 | 
				
			||||||
        ref.read(backupProvider.notifier).getBackupInfo();
 | 
					        ref.read(backupProvider.notifier).getBackupInfo();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -46,13 +48,13 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
              LinearPercentIndicator(
 | 
					              LinearPercentIndicator(
 | 
				
			||||||
                padding: const EdgeInsets.only(top: 8.0),
 | 
					                padding: const EdgeInsets.only(top: 8.0),
 | 
				
			||||||
                lineHeight: 5.0,
 | 
					                lineHeight: 5.0,
 | 
				
			||||||
                percent: _backupState.serverInfo.diskUsagePercentage / 100.0,
 | 
					                percent: backupState.serverInfo.diskUsagePercentage / 100.0,
 | 
				
			||||||
                backgroundColor: Colors.grey,
 | 
					                backgroundColor: Colors.grey,
 | 
				
			||||||
                progressColor: Theme.of(context).primaryColor,
 | 
					                progressColor: Theme.of(context).primaryColor,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              Padding(
 | 
					              Padding(
 | 
				
			||||||
                padding: const EdgeInsets.only(top: 12.0),
 | 
					                padding: const EdgeInsets.only(top: 12.0),
 | 
				
			||||||
                child: Text('${_backupState.serverInfo.diskUse} of ${_backupState.serverInfo.diskSize} used'),
 | 
					                child: Text('${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@ -104,18 +106,120 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget _buildSelectedAlbumName() {
 | 
				
			||||||
 | 
					      var text = "Selected: ";
 | 
				
			||||||
 | 
					      var albums = ref.watch(backupProvider).selectedBackupAlbums;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (albums.isNotEmpty) {
 | 
				
			||||||
 | 
					        for (var album in albums) {
 | 
				
			||||||
 | 
					          if (album.name == "Recent" || album.name == "Recents") {
 | 
				
			||||||
 | 
					            text += "${album.name} (All), ";
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            text += "${album.name}, ";
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Padding(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.only(top: 8.0),
 | 
				
			||||||
 | 
					          child: Text(
 | 
				
			||||||
 | 
					            text.trim().substring(0, text.length - 2),
 | 
				
			||||||
 | 
					            style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return Padding(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.only(top: 8.0),
 | 
				
			||||||
 | 
					          child: Text(
 | 
				
			||||||
 | 
					            "None selected",
 | 
				
			||||||
 | 
					            style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget _buildExcludedAlbumName() {
 | 
				
			||||||
 | 
					      var text = "Excluded: ";
 | 
				
			||||||
 | 
					      var albums = ref.watch(backupProvider).excludedBackupAlbums;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (albums.isNotEmpty) {
 | 
				
			||||||
 | 
					        for (var album in albums) {
 | 
				
			||||||
 | 
					          text += "${album.name}, ";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Padding(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.only(top: 8.0),
 | 
				
			||||||
 | 
					          child: Text(
 | 
				
			||||||
 | 
					            text.trim().substring(0, text.length - 2),
 | 
				
			||||||
 | 
					            style: TextStyle(color: Colors.red[300], fontSize: 12, fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return Container();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _buildFolderSelectionTile() {
 | 
				
			||||||
 | 
					      return Card(
 | 
				
			||||||
 | 
					        shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					          borderRadius: BorderRadius.circular(5), // if you need this
 | 
				
			||||||
 | 
					          side: const BorderSide(
 | 
				
			||||||
 | 
					            color: Colors.black12,
 | 
				
			||||||
 | 
					            width: 1,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        elevation: 0,
 | 
				
			||||||
 | 
					        borderOnForeground: false,
 | 
				
			||||||
 | 
					        child: ListTile(
 | 
				
			||||||
 | 
					          minVerticalPadding: 15,
 | 
				
			||||||
 | 
					          title: const Text("Backup Albums", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
 | 
				
			||||||
 | 
					          subtitle: Padding(
 | 
				
			||||||
 | 
					            padding: const EdgeInsets.only(top: 8.0),
 | 
				
			||||||
 | 
					            child: Column(
 | 
				
			||||||
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                const Text(
 | 
				
			||||||
 | 
					                  "Albums to be backup",
 | 
				
			||||||
 | 
					                  style: TextStyle(color: Color(0xFF808080), fontSize: 12),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                _buildSelectedAlbumName(),
 | 
				
			||||||
 | 
					                _buildExcludedAlbumName()
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          trailing: OutlinedButton(
 | 
				
			||||||
 | 
					            onPressed: () {
 | 
				
			||||||
 | 
					              AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            child: const Padding(
 | 
				
			||||||
 | 
					              padding: EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                vertical: 16.0,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              child: Text(
 | 
				
			||||||
 | 
					                "Select",
 | 
				
			||||||
 | 
					                style: TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        elevation: 0,
 | 
				
			||||||
        title: const Text(
 | 
					        title: const Text(
 | 
				
			||||||
          "Backup",
 | 
					          "Backup",
 | 
				
			||||||
          style: TextStyle(fontWeight: FontWeight.bold),
 | 
					          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        leading: IconButton(
 | 
					        leading: IconButton(
 | 
				
			||||||
            onPressed: () {
 | 
					            onPressed: () {
 | 
				
			||||||
              ref.watch(websocketProvider.notifier).listenUploadEvent();
 | 
					              ref.watch(websocketProvider.notifier).listenUploadEvent();
 | 
				
			||||||
              AutoRouter.of(context).pop(true);
 | 
					              AutoRouter.of(context).pop(true);
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            icon: const Icon(Icons.arrow_back_ios_rounded)),
 | 
					            splashRadius: 24,
 | 
				
			||||||
 | 
					            icon: const Icon(
 | 
				
			||||||
 | 
					              Icons.arrow_back_ios_rounded,
 | 
				
			||||||
 | 
					            )),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: Padding(
 | 
					      body: Padding(
 | 
				
			||||||
        padding: const EdgeInsets.all(16.0),
 | 
					        padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
@ -129,20 +233,21 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
					                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            _buildFolderSelectionTile(),
 | 
				
			||||||
            BackupInfoCard(
 | 
					            BackupInfoCard(
 | 
				
			||||||
              title: "Total",
 | 
					              title: "Total",
 | 
				
			||||||
              subtitle: "All images and videos on the device",
 | 
					              subtitle: "All unique photos and videos from selected albums",
 | 
				
			||||||
              info: "${_backupState.totalAssetCount}",
 | 
					              info: "${backupState.allUniqueAssets.length}",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            BackupInfoCard(
 | 
					            BackupInfoCard(
 | 
				
			||||||
              title: "Backup",
 | 
					              title: "Backup",
 | 
				
			||||||
              subtitle: "Images and videos of the device that are backup on server",
 | 
					              subtitle: "Photos and videos from selected albums that are backup",
 | 
				
			||||||
              info: "${_backupState.assetOnDatabase}",
 | 
					              info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            BackupInfoCard(
 | 
					            BackupInfoCard(
 | 
				
			||||||
              title: "Remainder",
 | 
					              title: "Remainder",
 | 
				
			||||||
              subtitle: "Images and videos that has not been backing up",
 | 
					              subtitle: "Photos and videos that has not been backing up from selected albums",
 | 
				
			||||||
              info: "${_backupState.totalAssetCount - _backupState.assetOnDatabase}",
 | 
					              info: "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            const Divider(),
 | 
					            const Divider(),
 | 
				
			||||||
            _buildBackupController(),
 | 
					            _buildBackupController(),
 | 
				
			||||||
@ -152,14 +257,14 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
            Padding(
 | 
					            Padding(
 | 
				
			||||||
              padding: const EdgeInsets.all(8.0),
 | 
					              padding: const EdgeInsets.all(8.0),
 | 
				
			||||||
              child: Text(
 | 
					              child: Text(
 | 
				
			||||||
                  "Asset that were being backup: ${_backupState.backingUpAssetCount} [${_backupState.progressInPercentage.toStringAsFixed(0)}%]"),
 | 
					                  "Asset that were being backup: ${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length} [${backupState.progressInPercentage.toStringAsFixed(0)}%]"),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            Padding(
 | 
					            Padding(
 | 
				
			||||||
              padding: const EdgeInsets.only(left: 8.0),
 | 
					              padding: const EdgeInsets.only(left: 8.0),
 | 
				
			||||||
              child: Row(children: [
 | 
					              child: Row(children: [
 | 
				
			||||||
                const Text("Backup Progress:"),
 | 
					                const Text("Backup Progress:"),
 | 
				
			||||||
                const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
 | 
					                const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
 | 
				
			||||||
                _backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
					                backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
				
			||||||
                    ? const CircularProgressIndicator.adaptive()
 | 
					                    ? const CircularProgressIndicator.adaptive()
 | 
				
			||||||
                    : const Text("Done"),
 | 
					                    : const Text("Done"),
 | 
				
			||||||
              ]),
 | 
					              ]),
 | 
				
			||||||
@ -167,7 +272,7 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
            Padding(
 | 
					            Padding(
 | 
				
			||||||
              padding: const EdgeInsets.all(8.0),
 | 
					              padding: const EdgeInsets.all(8.0),
 | 
				
			||||||
              child: Container(
 | 
					              child: Container(
 | 
				
			||||||
                child: _backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
					                child: backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
				
			||||||
                    ? ElevatedButton(
 | 
					                    ? ElevatedButton(
 | 
				
			||||||
                        style: ElevatedButton.styleFrom(primary: Colors.red[300]),
 | 
					                        style: ElevatedButton.styleFrom(primary: Colors.red[300]),
 | 
				
			||||||
                        onPressed: () {
 | 
					                        onPressed: () {
 | 
				
			||||||
@ -191,50 +296,3 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
class BackupInfoCard extends StatelessWidget {
 | 
					 | 
				
			||||||
  final String title;
 | 
					 | 
				
			||||||
  final String subtitle;
 | 
					 | 
				
			||||||
  final String info;
 | 
					 | 
				
			||||||
  const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    return Card(
 | 
					 | 
				
			||||||
      shape: RoundedRectangleBorder(
 | 
					 | 
				
			||||||
        borderRadius: BorderRadius.circular(5), // if you need this
 | 
					 | 
				
			||||||
        side: const BorderSide(
 | 
					 | 
				
			||||||
          color: Colors.black12,
 | 
					 | 
				
			||||||
          width: 1,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      elevation: 0,
 | 
					 | 
				
			||||||
      borderOnForeground: false,
 | 
					 | 
				
			||||||
      child: ListTile(
 | 
					 | 
				
			||||||
        minVerticalPadding: 15,
 | 
					 | 
				
			||||||
        isThreeLine: true,
 | 
					 | 
				
			||||||
        title: Text(
 | 
					 | 
				
			||||||
          title,
 | 
					 | 
				
			||||||
          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        subtitle: Padding(
 | 
					 | 
				
			||||||
          padding: const EdgeInsets.only(top: 8.0),
 | 
					 | 
				
			||||||
          child: Text(
 | 
					 | 
				
			||||||
            subtitle,
 | 
					 | 
				
			||||||
            style: const TextStyle(color: Color(0xFF808080), fontSize: 12),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        trailing: Column(
 | 
					 | 
				
			||||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
					 | 
				
			||||||
          children: [
 | 
					 | 
				
			||||||
            Text(
 | 
					 | 
				
			||||||
              info,
 | 
					 | 
				
			||||||
              style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            const Text("assets"),
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -5,9 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
					import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:immich_mobile/routing/router.dart';
 | 
					import 'package:immich_mobile/routing/router.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
 | 
					import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 | 
					import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
					import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImmichSliverAppBar extends ConsumerWidget {
 | 
					class ImmichSliverAppBar extends ConsumerWidget {
 | 
				
			||||||
@ -130,7 +130,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
				
			|||||||
                ? Positioned(
 | 
					                ? Positioned(
 | 
				
			||||||
                    bottom: 5,
 | 
					                    bottom: 5,
 | 
				
			||||||
                    child: Text(
 | 
					                    child: Text(
 | 
				
			||||||
                      _backupState.backingUpAssetCount.toString(),
 | 
					                      (_backupState.allUniqueAssets.length - _backupState.selectedAlbumsBackupAssetsIds.length)
 | 
				
			||||||
 | 
					                          .toString(),
 | 
				
			||||||
                      style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
 | 
					                      style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  )
 | 
					                  )
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
					import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 | 
					import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
					import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 | 
				
			||||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
					import 'package:package_info_plus/package_info_plus.dart';
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import 'package:immich_mobile/constants/hive_box.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/models/login_response.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/login_response.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/services/backup.service.dart';
 | 
					import 'package:immich_mobile/modules/backup/services/backup.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
 | 
					import 'package:immich_mobile/shared/services/device_info.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/services/network.service.dart';
 | 
					import 'package:immich_mobile/shared/services/network.service.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
 | 
					import 'package:immich_mobile/shared/models/device_info.model.dart';
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import 'package:immich_mobile/constants/hive_box.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
					import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
					import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
					import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
					import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LoginForm extends HookConsumerWidget {
 | 
					class LoginForm extends HookConsumerWidget {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
 | 
					import 'package:immich_mobile/modules/login/views/login_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/home/views/home_page.dart';
 | 
					import 'package:immich_mobile/modules/home/views/home_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
 | 
					import 'package:immich_mobile/modules/search/views/search_page.dart';
 | 
				
			||||||
@ -14,10 +16,11 @@ import 'package:immich_mobile/modules/sharing/views/select_user_for_sharing_page
 | 
				
			|||||||
import 'package:immich_mobile/modules/sharing/views/sharing_page.dart';
 | 
					import 'package:immich_mobile/modules/sharing/views/sharing_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/routing/auth_guard.dart';
 | 
					import 'package:immich_mobile/routing/auth_guard.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
					import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/views/backup_controller_page.dart';
 | 
					import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
 | 
					import 'package:immich_mobile/shared/views/tab_controller_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
 | 
					import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
 | 
				
			||||||
 | 
					import 'package:photo_manager/photo_manager.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'router.gr.dart';
 | 
					part 'router.gr.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -55,6 +58,8 @@ part 'router.gr.dart';
 | 
				
			|||||||
      guards: [AuthGuard],
 | 
					      guards: [AuthGuard],
 | 
				
			||||||
      transitionsBuilder: TransitionsBuilders.slideBottom,
 | 
					      transitionsBuilder: TransitionsBuilders.slideBottom,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    AutoRoute(page: BackupAlbumSelectionPage, guards: [AuthGuard]),
 | 
				
			||||||
 | 
					    AutoRoute(page: AlbumPreviewPage, guards: [AuthGuard]),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
class AppRouter extends _$AppRouter {
 | 
					class AppRouter extends _$AppRouter {
 | 
				
			||||||
 | 
				
			|||||||
@ -93,6 +93,16 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
          opaque: true,
 | 
					          opaque: true,
 | 
				
			||||||
          barrierDismissible: false);
 | 
					          barrierDismissible: false);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    BackupAlbumSelectionRoute.name: (routeData) {
 | 
				
			||||||
 | 
					      return MaterialPageX<dynamic>(
 | 
				
			||||||
 | 
					          routeData: routeData, child: const BackupAlbumSelectionPage());
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    AlbumPreviewRoute.name: (routeData) {
 | 
				
			||||||
 | 
					      final args = routeData.argsAs<AlbumPreviewRouteArgs>();
 | 
				
			||||||
 | 
					      return MaterialPageX<dynamic>(
 | 
				
			||||||
 | 
					          routeData: routeData,
 | 
				
			||||||
 | 
					          child: AlbumPreviewPage(key: args.key, album: args.album));
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    HomeRoute.name: (routeData) {
 | 
					    HomeRoute.name: (routeData) {
 | 
				
			||||||
      return MaterialPageX<dynamic>(
 | 
					      return MaterialPageX<dynamic>(
 | 
				
			||||||
          routeData: routeData, child: const HomePage());
 | 
					          routeData: routeData, child: const HomePage());
 | 
				
			||||||
@ -149,7 +159,11 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
            path: '/album-viewer-page', guards: [authGuard]),
 | 
					            path: '/album-viewer-page', guards: [authGuard]),
 | 
				
			||||||
        RouteConfig(SelectAdditionalUserForSharingRoute.name,
 | 
					        RouteConfig(SelectAdditionalUserForSharingRoute.name,
 | 
				
			||||||
            path: '/select-additional-user-for-sharing-page',
 | 
					            path: '/select-additional-user-for-sharing-page',
 | 
				
			||||||
            guards: [authGuard])
 | 
					            guards: [authGuard]),
 | 
				
			||||||
 | 
					        RouteConfig(BackupAlbumSelectionRoute.name,
 | 
				
			||||||
 | 
					            path: '/backup-album-selection-page', guards: [authGuard]),
 | 
				
			||||||
 | 
					        RouteConfig(AlbumPreviewRoute.name,
 | 
				
			||||||
 | 
					            path: '/album-preview-page', guards: [authGuard])
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -358,6 +372,40 @@ class SelectAdditionalUserForSharingRouteArgs {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// generated route for
 | 
				
			||||||
 | 
					/// [BackupAlbumSelectionPage]
 | 
				
			||||||
 | 
					class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
 | 
				
			||||||
 | 
					  const BackupAlbumSelectionRoute()
 | 
				
			||||||
 | 
					      : super(BackupAlbumSelectionRoute.name,
 | 
				
			||||||
 | 
					            path: '/backup-album-selection-page');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const String name = 'BackupAlbumSelectionRoute';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// generated route for
 | 
				
			||||||
 | 
					/// [AlbumPreviewPage]
 | 
				
			||||||
 | 
					class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
 | 
				
			||||||
 | 
					  AlbumPreviewRoute({Key? key, required AssetPathEntity album})
 | 
				
			||||||
 | 
					      : super(AlbumPreviewRoute.name,
 | 
				
			||||||
 | 
					            path: '/album-preview-page',
 | 
				
			||||||
 | 
					            args: AlbumPreviewRouteArgs(key: key, album: album));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const String name = 'AlbumPreviewRoute';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AlbumPreviewRouteArgs {
 | 
				
			||||||
 | 
					  const AlbumPreviewRouteArgs({this.key, required this.album});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Key? key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final AssetPathEntity album;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'AlbumPreviewRouteArgs{key: $key, album: $album}';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// generated route for
 | 
					/// generated route for
 | 
				
			||||||
/// [HomePage]
 | 
					/// [HomePage]
 | 
				
			||||||
class HomeRoute extends PageRouteInfo<void> {
 | 
					class HomeRoute extends PageRouteInfo<void> {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,77 +0,0 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum BackUpProgressEnum { idle, inProgress, done }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BackUpState {
 | 
					 | 
				
			||||||
  final BackUpProgressEnum backupProgress;
 | 
					 | 
				
			||||||
  final int totalAssetCount;
 | 
					 | 
				
			||||||
  final int assetOnDatabase;
 | 
					 | 
				
			||||||
  final int backingUpAssetCount;
 | 
					 | 
				
			||||||
  final double progressInPercentage;
 | 
					 | 
				
			||||||
  final CancelToken cancelToken;
 | 
					 | 
				
			||||||
  final ServerInfo serverInfo;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  BackUpState({
 | 
					 | 
				
			||||||
    required this.backupProgress,
 | 
					 | 
				
			||||||
    required this.totalAssetCount,
 | 
					 | 
				
			||||||
    required this.assetOnDatabase,
 | 
					 | 
				
			||||||
    required this.backingUpAssetCount,
 | 
					 | 
				
			||||||
    required this.progressInPercentage,
 | 
					 | 
				
			||||||
    required this.cancelToken,
 | 
					 | 
				
			||||||
    required this.serverInfo,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  BackUpState copyWith({
 | 
					 | 
				
			||||||
    BackUpProgressEnum? backupProgress,
 | 
					 | 
				
			||||||
    int? totalAssetCount,
 | 
					 | 
				
			||||||
    int? assetOnDatabase,
 | 
					 | 
				
			||||||
    int? backingUpAssetCount,
 | 
					 | 
				
			||||||
    double? progressInPercentage,
 | 
					 | 
				
			||||||
    CancelToken? cancelToken,
 | 
					 | 
				
			||||||
    ServerInfo? serverInfo,
 | 
					 | 
				
			||||||
  }) {
 | 
					 | 
				
			||||||
    return BackUpState(
 | 
					 | 
				
			||||||
      backupProgress: backupProgress ?? this.backupProgress,
 | 
					 | 
				
			||||||
      totalAssetCount: totalAssetCount ?? this.totalAssetCount,
 | 
					 | 
				
			||||||
      assetOnDatabase: assetOnDatabase ?? this.assetOnDatabase,
 | 
					 | 
				
			||||||
      backingUpAssetCount: backingUpAssetCount ?? this.backingUpAssetCount,
 | 
					 | 
				
			||||||
      progressInPercentage: progressInPercentage ?? this.progressInPercentage,
 | 
					 | 
				
			||||||
      cancelToken: cancelToken ?? this.cancelToken,
 | 
					 | 
				
			||||||
      serverInfo: serverInfo ?? this.serverInfo,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  String toString() {
 | 
					 | 
				
			||||||
    return 'BackUpState(backupProgress: $backupProgress, totalAssetCount: $totalAssetCount, assetOnDatabase: $assetOnDatabase, backingUpAssetCount: $backingUpAssetCount, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo)';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return other is BackUpState &&
 | 
					 | 
				
			||||||
        other.backupProgress == backupProgress &&
 | 
					 | 
				
			||||||
        other.totalAssetCount == totalAssetCount &&
 | 
					 | 
				
			||||||
        other.assetOnDatabase == assetOnDatabase &&
 | 
					 | 
				
			||||||
        other.backingUpAssetCount == backingUpAssetCount &&
 | 
					 | 
				
			||||||
        other.progressInPercentage == progressInPercentage &&
 | 
					 | 
				
			||||||
        other.cancelToken == cancelToken &&
 | 
					 | 
				
			||||||
        other.serverInfo == serverInfo;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  int get hashCode {
 | 
					 | 
				
			||||||
    return backupProgress.hashCode ^
 | 
					 | 
				
			||||||
        totalAssetCount.hashCode ^
 | 
					 | 
				
			||||||
        assetOnDatabase.hashCode ^
 | 
					 | 
				
			||||||
        backingUpAssetCount.hashCode ^
 | 
					 | 
				
			||||||
        progressInPercentage.hashCode ^
 | 
					 | 
				
			||||||
        cancelToken.hashCode ^
 | 
					 | 
				
			||||||
        serverInfo.hashCode;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,194 +0,0 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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/login/providers/authentication.provider.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
 | 
					 | 
				
			||||||
import 'package:immich_mobile/shared/services/backup.service.dart';
 | 
					 | 
				
			||||||
import 'package:photo_manager/photo_manager.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BackupNotifier extends StateNotifier<BackUpState> {
 | 
					 | 
				
			||||||
  BackupNotifier({this.ref})
 | 
					 | 
				
			||||||
      : super(
 | 
					 | 
				
			||||||
          BackUpState(
 | 
					 | 
				
			||||||
            backupProgress: BackUpProgressEnum.idle,
 | 
					 | 
				
			||||||
            backingUpAssetCount: 0,
 | 
					 | 
				
			||||||
            assetOnDatabase: 0,
 | 
					 | 
				
			||||||
            totalAssetCount: 0,
 | 
					 | 
				
			||||||
            progressInPercentage: 0,
 | 
					 | 
				
			||||||
            cancelToken: CancelToken(),
 | 
					 | 
				
			||||||
            serverInfo: ServerInfo(
 | 
					 | 
				
			||||||
              diskAvailable: "0",
 | 
					 | 
				
			||||||
              diskAvailableRaw: 0,
 | 
					 | 
				
			||||||
              diskSize: "0",
 | 
					 | 
				
			||||||
              diskSizeRaw: 0,
 | 
					 | 
				
			||||||
              diskUsagePercentage: 0.0,
 | 
					 | 
				
			||||||
              diskUse: "0",
 | 
					 | 
				
			||||||
              diskUseRaw: 0,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Ref? ref;
 | 
					 | 
				
			||||||
  final BackupService _backupService = BackupService();
 | 
					 | 
				
			||||||
  final ServerInfoService _serverInfoService = ServerInfoService();
 | 
					 | 
				
			||||||
  final StreamController _onAssetBackupStreamCtrl =
 | 
					 | 
				
			||||||
      StreamController.broadcast();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void getBackupInfo() async {
 | 
					 | 
				
			||||||
    _updateServerInfo();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    List<AssetPathEntity> list = await PhotoManager.getAssetPathList(
 | 
					 | 
				
			||||||
        onlyAll: true, type: RequestType.common);
 | 
					 | 
				
			||||||
    List<String> didBackupAsset = await _backupService.getDeviceBackupAsset();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (list.isEmpty) {
 | 
					 | 
				
			||||||
      debugPrint("No Asset On Device");
 | 
					 | 
				
			||||||
      state = state.copyWith(
 | 
					 | 
				
			||||||
          backupProgress: BackUpProgressEnum.idle,
 | 
					 | 
				
			||||||
          totalAssetCount: 0,
 | 
					 | 
				
			||||||
          assetOnDatabase: didBackupAsset.length);
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    int totalAsset = list[0].assetCount;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    state = state.copyWith(
 | 
					 | 
				
			||||||
        totalAssetCount: totalAsset, assetOnDatabase: didBackupAsset.length);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void startBackupProcess() async {
 | 
					 | 
				
			||||||
    _updateServerInfo();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var authResult = await PhotoManager.requestPermissionExtend();
 | 
					 | 
				
			||||||
    if (authResult.isAuth) {
 | 
					 | 
				
			||||||
      await PhotoManager.clearFileCache();
 | 
					 | 
				
			||||||
      // await PhotoManager.presentLimited();
 | 
					 | 
				
			||||||
      // Gather assets info
 | 
					 | 
				
			||||||
      List<AssetPathEntity> list = await PhotoManager.getAssetPathList(
 | 
					 | 
				
			||||||
          hasAll: true, onlyAll: true, type: RequestType.common);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Get device assets info from database
 | 
					 | 
				
			||||||
      // Compare and find different assets that has not been backing up
 | 
					 | 
				
			||||||
      // Backup those assets
 | 
					 | 
				
			||||||
      List<String> backupAsset = await _backupService.getDeviceBackupAsset();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (list.isEmpty) {
 | 
					 | 
				
			||||||
        debugPrint("No Asset On Device - Abort Backup Process");
 | 
					 | 
				
			||||||
        state = state.copyWith(
 | 
					 | 
				
			||||||
            backupProgress: BackUpProgressEnum.idle,
 | 
					 | 
				
			||||||
            totalAssetCount: 0,
 | 
					 | 
				
			||||||
            assetOnDatabase: backupAsset.length);
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      int totalAsset = list[0].assetCount;
 | 
					 | 
				
			||||||
      List<AssetEntity> currentAssets =
 | 
					 | 
				
			||||||
          await list[0].getAssetListRange(start: 0, end: totalAsset);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      state = state.copyWith(
 | 
					 | 
				
			||||||
          totalAssetCount: totalAsset, assetOnDatabase: backupAsset.length);
 | 
					 | 
				
			||||||
      // Remove item that has already been backed up
 | 
					 | 
				
			||||||
      for (var backupAssetId in backupAsset) {
 | 
					 | 
				
			||||||
        currentAssets.removeWhere((e) => e.id == backupAssetId);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (currentAssets.isEmpty) {
 | 
					 | 
				
			||||||
        state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      state = state.copyWith(backingUpAssetCount: currentAssets.length);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Perform Backup
 | 
					 | 
				
			||||||
      state = state.copyWith(cancelToken: CancelToken());
 | 
					 | 
				
			||||||
      _backupService.backupAsset(currentAssets, 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(
 | 
					 | 
				
			||||||
        backingUpAssetCount: state.backingUpAssetCount - 1,
 | 
					 | 
				
			||||||
        assetOnDatabase: state.assetOnDatabase + 1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (state.backingUpAssetCount == 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);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -239,6 +239,13 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.0.4"
 | 
					    version: "4.0.4"
 | 
				
			||||||
 | 
					  equatable:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: equatable
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.0.3"
 | 
				
			||||||
  exif:
 | 
					  exif:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ name: immich_mobile
 | 
				
			|||||||
description: Immich - selfhosted backup media file on mobile phone
 | 
					description: Immich - selfhosted backup media file on mobile phone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
publish_to: "none"
 | 
					publish_to: "none"
 | 
				
			||||||
version: 1.8.0+12
 | 
					version: 1.9.0+13
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ">=2.15.1 <3.0.0"
 | 
					  sdk: ">=2.15.1 <3.0.0"
 | 
				
			||||||
@ -37,6 +37,7 @@ dependencies:
 | 
				
			|||||||
  package_info_plus: ^1.4.0
 | 
					  package_info_plus: ^1.4.0
 | 
				
			||||||
  flutter_spinkit: ^5.1.0
 | 
					  flutter_spinkit: ^5.1.0
 | 
				
			||||||
  flutter_swipe_detector: ^2.0.0
 | 
					  flutter_swipe_detector: ^2.0.0
 | 
				
			||||||
 | 
					  equatable: ^2.0.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const serverVersion = {
 | 
					export const serverVersion = {
 | 
				
			||||||
  major: 1,
 | 
					  major: 1,
 | 
				
			||||||
  minor: 8,
 | 
					  minor: 9,
 | 
				
			||||||
  patch: 0,
 | 
					  patch: 0,
 | 
				
			||||||
  build: 12,
 | 
					  build: 13,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||