mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	Fix backup not resuming after closed and reopen (#266)
* Fixed app not resuming backup after closing and reopening the app * Fixed cosmetic effect of backup button doesn't change state right away after pressing start backup * Fixed grammar * Fixed deep copy problem that cause incorrect asset count when backing up * Format code
This commit is contained in:
		
							parent
							
								
									d02b97e1c1
								
							
						
					
					
						commit
						40a8115101
					
				@ -28,22 +28,26 @@ class ImageViewerPageState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
 | 
					  factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
    return ImageViewerPageState(
 | 
					    return ImageViewerPageState(
 | 
				
			||||||
      downloadAssetStatus: DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
 | 
					      downloadAssetStatus:
 | 
				
			||||||
 | 
					          DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
 | 
					  factory ImageViewerPageState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ImageViewerPageState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus;
 | 
					    return other is ImageViewerPageState &&
 | 
				
			||||||
 | 
					        other.downloadAssetStatus == downloadAssetStatus;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,9 @@ import 'package:immich_mobile/shared/ui/immich_toast.dart';
 | 
				
			|||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
 | 
					class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
 | 
				
			||||||
  final ImageViewerService _imageViewerService = ImageViewerService();
 | 
					  final ImageViewerService _imageViewerService = ImageViewerService();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle));
 | 
					  ImageViewerStateNotifier()
 | 
				
			||||||
 | 
					      : super(ImageViewerPageState(
 | 
				
			||||||
 | 
					            downloadAssetStatus: DownloadAssetStatus.idle));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void downloadAsset(ImmichAsset asset, BuildContext context) async {
 | 
					  void downloadAsset(ImmichAsset asset, BuildContext context) async {
 | 
				
			||||||
    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
 | 
					    state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
 | 
				
			||||||
@ -40,4 +42,5 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final imageViewerStateProvider =
 | 
					final imageViewerStateProvider =
 | 
				
			||||||
    StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier()));
 | 
					    StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
 | 
				
			||||||
 | 
					        ((ref) => ImageViewerStateNotifier()));
 | 
				
			||||||
 | 
				
			|||||||
@ -15,12 +15,14 @@ class ImageViewerService {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      String fileName = p.basename(asset.originalPath);
 | 
					      String fileName = p.basename(asset.originalPath);
 | 
				
			||||||
      var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
 | 
					      var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
 | 
				
			||||||
      Uri filePath =
 | 
					      Uri filePath = Uri.parse(
 | 
				
			||||||
          Uri.parse("$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
 | 
					          "$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var res = await http.get(
 | 
					      var res = await http.get(
 | 
				
			||||||
        filePath,
 | 
					        filePath,
 | 
				
			||||||
        headers: {"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"},
 | 
					        headers: {
 | 
				
			||||||
 | 
					          "Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      final AssetEntity? entity;
 | 
					      final AssetEntity? entity;
 | 
				
			||||||
 | 
				
			|||||||
@ -21,13 +21,16 @@ class AvailableAlbum {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is AvailableAlbum && other.albumEntity == albumEntity && other.thumbnailData == thumbnailData;
 | 
					    return other is AvailableAlbum &&
 | 
				
			||||||
 | 
					        other.albumEntity == albumEntity &&
 | 
				
			||||||
 | 
					        other.thumbnailData == thumbnailData;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ enum BackUpProgressEnum { idle, inProgress, done }
 | 
				
			|||||||
class BackUpState extends Equatable {
 | 
					class BackUpState extends Equatable {
 | 
				
			||||||
  // enum
 | 
					  // enum
 | 
				
			||||||
  final BackUpProgressEnum backupProgress;
 | 
					  final BackUpProgressEnum backupProgress;
 | 
				
			||||||
  final List<String> allAssetOnDatabase;
 | 
					  final List<String> allAssetsInDatabase;
 | 
				
			||||||
  final double progressInPercentage;
 | 
					  final double progressInPercentage;
 | 
				
			||||||
  final CancellationToken cancelToken;
 | 
					  final CancellationToken cancelToken;
 | 
				
			||||||
  final ServerInfo serverInfo;
 | 
					  final ServerInfo serverInfo;
 | 
				
			||||||
@ -28,7 +28,7 @@ class BackUpState extends Equatable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const BackUpState({
 | 
					  const BackUpState({
 | 
				
			||||||
    required this.backupProgress,
 | 
					    required this.backupProgress,
 | 
				
			||||||
    required this.allAssetOnDatabase,
 | 
					    required this.allAssetsInDatabase,
 | 
				
			||||||
    required this.progressInPercentage,
 | 
					    required this.progressInPercentage,
 | 
				
			||||||
    required this.cancelToken,
 | 
					    required this.cancelToken,
 | 
				
			||||||
    required this.serverInfo,
 | 
					    required this.serverInfo,
 | 
				
			||||||
@ -41,7 +41,7 @@ class BackUpState extends Equatable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  BackUpState copyWith({
 | 
					  BackUpState copyWith({
 | 
				
			||||||
    BackUpProgressEnum? backupProgress,
 | 
					    BackUpProgressEnum? backupProgress,
 | 
				
			||||||
    List<String>? allAssetOnDatabase,
 | 
					    List<String>? allAssetsInDatabase,
 | 
				
			||||||
    double? progressInPercentage,
 | 
					    double? progressInPercentage,
 | 
				
			||||||
    CancellationToken? cancelToken,
 | 
					    CancellationToken? cancelToken,
 | 
				
			||||||
    ServerInfo? serverInfo,
 | 
					    ServerInfo? serverInfo,
 | 
				
			||||||
@ -53,7 +53,7 @@ class BackUpState extends Equatable {
 | 
				
			|||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return BackUpState(
 | 
					    return BackUpState(
 | 
				
			||||||
      backupProgress: backupProgress ?? this.backupProgress,
 | 
					      backupProgress: backupProgress ?? this.backupProgress,
 | 
				
			||||||
      allAssetOnDatabase: allAssetOnDatabase ?? this.allAssetOnDatabase,
 | 
					      allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase,
 | 
				
			||||||
      progressInPercentage: progressInPercentage ?? this.progressInPercentage,
 | 
					      progressInPercentage: progressInPercentage ?? this.progressInPercentage,
 | 
				
			||||||
      cancelToken: cancelToken ?? this.cancelToken,
 | 
					      cancelToken: cancelToken ?? this.cancelToken,
 | 
				
			||||||
      serverInfo: serverInfo ?? this.serverInfo,
 | 
					      serverInfo: serverInfo ?? this.serverInfo,
 | 
				
			||||||
@ -61,20 +61,21 @@ class BackUpState extends Equatable {
 | 
				
			|||||||
      selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
 | 
					      selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
 | 
				
			||||||
      excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
 | 
					      excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
 | 
				
			||||||
      allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
 | 
					      allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
 | 
				
			||||||
      selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
 | 
					      selectedAlbumsBackupAssetsIds:
 | 
				
			||||||
 | 
					          selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
    return 'BackUpState(backupProgress: $backupProgress, allAssetOnDatabase: $allAssetOnDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
 | 
					    return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  List<Object> get props {
 | 
					  List<Object> get props {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
      backupProgress,
 | 
					      backupProgress,
 | 
				
			||||||
      allAssetOnDatabase,
 | 
					      allAssetsInDatabase,
 | 
				
			||||||
      progressInPercentage,
 | 
					      progressInPercentage,
 | 
				
			||||||
      cancelToken,
 | 
					      cancelToken,
 | 
				
			||||||
      serverInfo,
 | 
					      serverInfo,
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,8 @@ class HiveBackupAlbums {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  HiveBackupAlbums copyWith({
 | 
					  HiveBackupAlbums copyWith({
 | 
				
			||||||
    List<String>? selectedAlbumIds,
 | 
					    List<String>? selectedAlbumIds,
 | 
				
			||||||
@ -49,7 +50,8 @@ class HiveBackupAlbums {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory HiveBackupAlbums.fromJson(String source) => HiveBackupAlbums.fromMap(json.decode(source));
 | 
					  factory HiveBackupAlbums.fromJson(String source) =>
 | 
				
			||||||
 | 
					      HiveBackupAlbums.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
      : super(
 | 
					      : super(
 | 
				
			||||||
          BackUpState(
 | 
					          BackUpState(
 | 
				
			||||||
            backupProgress: BackUpProgressEnum.idle,
 | 
					            backupProgress: BackUpProgressEnum.idle,
 | 
				
			||||||
            allAssetOnDatabase: const [],
 | 
					            allAssetsInDatabase: const [],
 | 
				
			||||||
            progressInPercentage: 0,
 | 
					            progressInPercentage: 0,
 | 
				
			||||||
            cancelToken: CancellationToken(),
 | 
					            cancelToken: CancellationToken(),
 | 
				
			||||||
            serverInfo: ServerInfo(
 | 
					            serverInfo: ServerInfo(
 | 
				
			||||||
@ -36,7 +36,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
            allUniqueAssets: const {},
 | 
					            allUniqueAssets: const {},
 | 
				
			||||||
            selectedAlbumsBackupAssetsIds: const {},
 | 
					            selectedAlbumsBackupAssetsIds: const {},
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        );
 | 
					        ) {
 | 
				
			||||||
 | 
					    getBackupInfo();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final BackupService _backupService;
 | 
					  final BackupService _backupService;
 | 
				
			||||||
  final ServerInfoService _serverInfoService;
 | 
					  final ServerInfoService _serverInfoService;
 | 
				
			||||||
@ -93,7 +95,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
  /// If this is the first time performing backup - set the default selected album to be
 | 
					  /// 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)
 | 
					  /// the one that has all assets (Recent on Android, Recents on iOS)
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  Future<void> getBackupAlbumsInfo() async {
 | 
					  Future<void> _getBackupAlbumsInfo() async {
 | 
				
			||||||
    // Get all albums on the device
 | 
					    // Get all albums on the device
 | 
				
			||||||
    List<AvailableAlbum> availableAlbums = [];
 | 
					    List<AvailableAlbum> availableAlbums = [];
 | 
				
			||||||
    List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
 | 
					    List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
 | 
				
			||||||
@ -177,7 +179,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
  /// Find the assets that are not overlapping between the two sets
 | 
					  /// Find the assets that are not overlapping between the two sets
 | 
				
			||||||
  /// Those assets are unique and are used as the total assets
 | 
					  /// Those assets are unique and are used as the total assets
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  void _updateBackupAssetCount() async {
 | 
					  Future<void> _updateBackupAssetCount() async {
 | 
				
			||||||
    Set<AssetEntity> assetsFromSelectedAlbums = {};
 | 
					    Set<AssetEntity> assetsFromSelectedAlbums = {};
 | 
				
			||||||
    Set<AssetEntity> assetsFromExcludedAlbums = {};
 | 
					    Set<AssetEntity> assetsFromExcludedAlbums = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -195,27 +197,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Set<AssetEntity> allUniqueAssets =
 | 
					    Set<AssetEntity> allUniqueAssets =
 | 
				
			||||||
        assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
 | 
					        assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
 | 
				
			||||||
    List<String> allAssetOnDatabase =
 | 
					    List<String> allAssetsInDatabase =
 | 
				
			||||||
        await _backupService.getDeviceBackupAsset();
 | 
					        await _backupService.getDeviceBackupAsset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Find asset that were backup from selected albums
 | 
					    // Find asset that were backup from selected albums
 | 
				
			||||||
    Set<String> selectedAlbumsBackupAssets =
 | 
					    Set<String> selectedAlbumsBackupAssets =
 | 
				
			||||||
        Set.from(allUniqueAssets.map((e) => e.id));
 | 
					        Set.from(allUniqueAssets.map((e) => e.id));
 | 
				
			||||||
    selectedAlbumsBackupAssets
 | 
					    selectedAlbumsBackupAssets
 | 
				
			||||||
        .removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
 | 
					        .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (allUniqueAssets.isEmpty) {
 | 
					    if (allUniqueAssets.isEmpty) {
 | 
				
			||||||
      debugPrint("No Asset On Device");
 | 
					      debugPrint("No Asset On Device");
 | 
				
			||||||
      state = state.copyWith(
 | 
					      state = state.copyWith(
 | 
				
			||||||
        backupProgress: BackUpProgressEnum.idle,
 | 
					        backupProgress: BackUpProgressEnum.idle,
 | 
				
			||||||
        allAssetOnDatabase: allAssetOnDatabase,
 | 
					        allAssetsInDatabase: allAssetsInDatabase,
 | 
				
			||||||
        allUniqueAssets: {},
 | 
					        allUniqueAssets: {},
 | 
				
			||||||
        selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
 | 
					        selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      state = state.copyWith(
 | 
					      state = state.copyWith(
 | 
				
			||||||
        allAssetOnDatabase: allAssetOnDatabase,
 | 
					        allAssetsInDatabase: allAssetsInDatabase,
 | 
				
			||||||
        allUniqueAssets: allUniqueAssets,
 | 
					        allUniqueAssets: allUniqueAssets,
 | 
				
			||||||
        selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
 | 
					        selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@ -223,6 +225,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Save to persistent storage
 | 
					    // Save to persistent storage
 | 
				
			||||||
    _updatePersistentAlbumsSelection();
 | 
					    _updatePersistentAlbumsSelection();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
@ -230,10 +234,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
  /// which albums are selected or excluded
 | 
					  /// which albums are selected or excluded
 | 
				
			||||||
  /// and then update the UI according to those information
 | 
					  /// and then update the UI according to those information
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  void getBackupInfo() async {
 | 
					  Future<void> getBackupInfo() async {
 | 
				
			||||||
    await getBackupAlbumsInfo();
 | 
					    await _getBackupAlbumsInfo();
 | 
				
			||||||
    _updateServerInfo();
 | 
					    await _updateServerInfo();
 | 
				
			||||||
    _updateBackupAssetCount();
 | 
					    await _updateBackupAssetCount();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
@ -256,11 +260,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
  /// Invoke backup process
 | 
					  /// Invoke backup process
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  void startBackupProcess() async {
 | 
					  void startBackupProcess() async {
 | 
				
			||||||
    _updateServerInfo();
 | 
					 | 
				
			||||||
    _updateBackupAssetCount();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
 | 
					    state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await getBackupInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var authResult = await PhotoManager.requestPermissionExtend();
 | 
					    var authResult = await PhotoManager.requestPermissionExtend();
 | 
				
			||||||
    if (authResult.isAuth) {
 | 
					    if (authResult.isAuth) {
 | 
				
			||||||
      await PhotoManager.clearFileCache();
 | 
					      await PhotoManager.clearFileCache();
 | 
				
			||||||
@ -271,10 +274,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets;
 | 
					      Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Remove item that has already been backed up
 | 
					      // Remove item that has already been backed up
 | 
				
			||||||
      for (var assetId in state.allAssetOnDatabase) {
 | 
					      for (var assetId in state.allAssetsInDatabase) {
 | 
				
			||||||
        assetsWillBeBackup.removeWhere((e) => e.id == assetId);
 | 
					        assetsWillBeBackup.removeWhere((e) => e.id == assetId);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -301,8 +304,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
    state = state.copyWith(selectedAlbumsBackupAssetsIds: {
 | 
					    state = state.copyWith(selectedAlbumsBackupAssetsIds: {
 | 
				
			||||||
      ...state.selectedAlbumsBackupAssetsIds,
 | 
					      ...state.selectedAlbumsBackupAssetsIds,
 | 
				
			||||||
      deviceAssetId
 | 
					      deviceAssetId
 | 
				
			||||||
    }, allAssetOnDatabase: [
 | 
					    }, allAssetsInDatabase: [
 | 
				
			||||||
      ...state.allAssetOnDatabase,
 | 
					      ...state.allAssetsInDatabase,
 | 
				
			||||||
      deviceAssetId
 | 
					      deviceAssetId
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -321,7 +324,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
				
			|||||||
        progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
 | 
					        progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _updateServerInfo() async {
 | 
					  Future<void> _updateServerInfo() async {
 | 
				
			||||||
    var serverInfo = await _serverInfoService.getServerInfo();
 | 
					    var serverInfo = await _serverInfoService.getServerInfo();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Update server info
 | 
					    // Update server info
 | 
				
			||||||
 | 
				
			|||||||
@ -14,16 +14,22 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
  final Uint8List? imageData;
 | 
					  final Uint8List? imageData;
 | 
				
			||||||
  final AssetPathEntity albumInfo;
 | 
					  final AssetPathEntity albumInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo}) : super(key: key);
 | 
					  const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
 | 
					    final bool isSelected =
 | 
				
			||||||
    final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
 | 
					        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 selectedFilter = ColorFilter.mode(
 | 
				
			||||||
    ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
 | 
					        Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
 | 
				
			||||||
    ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color);
 | 
					    ColorFilter excludedFilter =
 | 
				
			||||||
 | 
					        ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
 | 
				
			||||||
 | 
					    ColorFilter unselectedFilter =
 | 
				
			||||||
 | 
					        const ColorFilter.mode(Colors.black, BlendMode.color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _buildSelectedTextBox() {
 | 
					    _buildSelectedTextBox() {
 | 
				
			||||||
      if (isSelected) {
 | 
					      if (isSelected) {
 | 
				
			||||||
@ -32,7 +38,8 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
					          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
          label: const Text(
 | 
					          label: const Text(
 | 
				
			||||||
            "INCLUDED",
 | 
					            "INCLUDED",
 | 
				
			||||||
            style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					                fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          backgroundColor: Theme.of(context).primaryColor,
 | 
					          backgroundColor: Theme.of(context).primaryColor,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
@ -42,7 +49,8 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
					          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
          label: const Text(
 | 
					          label: const Text(
 | 
				
			||||||
            "EXCLUDED",
 | 
					            "EXCLUDED",
 | 
				
			||||||
            style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					                fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          backgroundColor: Colors.red[300],
 | 
					          backgroundColor: Colors.red[300],
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
@ -85,10 +93,15 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
        HapticFeedback.selectionClick();
 | 
					        HapticFeedback.selectionClick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isExcluded) {
 | 
					        if (isExcluded) {
 | 
				
			||||||
          ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(albumInfo);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(backupProvider.notifier)
 | 
				
			||||||
 | 
					              .removeExcludedAlbumForBackup(albumInfo);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
 | 
					          if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
 | 
				
			||||||
              ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo)) {
 | 
					              ref
 | 
				
			||||||
 | 
					                  .watch(backupProvider)
 | 
				
			||||||
 | 
					                  .selectedBackupAlbums
 | 
				
			||||||
 | 
					                  .contains(albumInfo)) {
 | 
				
			||||||
            ImmichToast.show(
 | 
					            ImmichToast.show(
 | 
				
			||||||
              context: context,
 | 
					              context: context,
 | 
				
			||||||
              msg: "Cannot exclude the only album",
 | 
					              msg: "Cannot exclude the only album",
 | 
				
			||||||
@ -98,7 +111,9 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          ref.watch(backupProvider.notifier).addExcludedAlbumForBackup(albumInfo);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(backupProvider.notifier)
 | 
				
			||||||
 | 
					              .addExcludedAlbumForBackup(albumInfo);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      child: Card(
 | 
					      child: Card(
 | 
				
			||||||
@ -121,12 +136,16 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
                  width: 200,
 | 
					                  width: 200,
 | 
				
			||||||
                  height: 200,
 | 
					                  height: 200,
 | 
				
			||||||
                  decoration: BoxDecoration(
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
                    borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
 | 
					                    borderRadius: const BorderRadius.only(
 | 
				
			||||||
 | 
					                        topLeft: Radius.circular(12),
 | 
				
			||||||
 | 
					                        topRight: Radius.circular(12)),
 | 
				
			||||||
                    image: DecorationImage(
 | 
					                    image: DecorationImage(
 | 
				
			||||||
                      colorFilter: _buildImageFilter(),
 | 
					                      colorFilter: _buildImageFilter(),
 | 
				
			||||||
                      image: imageData != null
 | 
					                      image: imageData != null
 | 
				
			||||||
                          ? MemoryImage(imageData!)
 | 
					                          ? MemoryImage(imageData!)
 | 
				
			||||||
                          : const AssetImage('assets/immich-logo-no-outline.png') as ImageProvider,
 | 
					                          : const AssetImage(
 | 
				
			||||||
 | 
					                                  'assets/immich-logo-no-outline.png')
 | 
				
			||||||
 | 
					                              as ImageProvider,
 | 
				
			||||||
                      fit: BoxFit.cover,
 | 
					                      fit: BoxFit.cover,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
@ -150,13 +169,17 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
                          Text(
 | 
					                          Text(
 | 
				
			||||||
                            albumInfo.name,
 | 
					                            albumInfo.name,
 | 
				
			||||||
                            style: TextStyle(
 | 
					                            style: TextStyle(
 | 
				
			||||||
                                fontSize: 14, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
 | 
					                                fontSize: 14,
 | 
				
			||||||
 | 
					                                color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					                                fontWeight: FontWeight.bold),
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                          Padding(
 | 
					                          Padding(
 | 
				
			||||||
                            padding: const EdgeInsets.only(top: 2.0),
 | 
					                            padding: const EdgeInsets.only(top: 2.0),
 | 
				
			||||||
                            child: Text(
 | 
					                            child: Text(
 | 
				
			||||||
                              albumInfo.assetCount.toString() + (albumInfo.isAll ? " (ALL)" : ""),
 | 
					                              albumInfo.assetCount.toString() +
 | 
				
			||||||
                              style: TextStyle(fontSize: 12, color: Colors.grey[600]),
 | 
					                                  (albumInfo.isAll ? " (ALL)" : ""),
 | 
				
			||||||
 | 
					                              style: TextStyle(
 | 
				
			||||||
 | 
					                                  fontSize: 12, color: Colors.grey[600]),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          )
 | 
					                          )
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
@ -165,7 +188,8 @@ class AlbumInfoCard extends HookConsumerWidget {
 | 
				
			|||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  IconButton(
 | 
					                  IconButton(
 | 
				
			||||||
                    onPressed: () {
 | 
					                    onPressed: () {
 | 
				
			||||||
                      AutoRouter.of(context).push(AlbumPreviewRoute(album: albumInfo));
 | 
					                      AutoRouter.of(context)
 | 
				
			||||||
 | 
					                          .push(AlbumPreviewRoute(album: albumInfo));
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    icon: Icon(
 | 
					                    icon: Icon(
 | 
				
			||||||
                      Icons.image_outlined,
 | 
					                      Icons.image_outlined,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,12 @@ class BackupInfoCard extends StatelessWidget {
 | 
				
			|||||||
  final String title;
 | 
					  final String title;
 | 
				
			||||||
  final String subtitle;
 | 
					  final String subtitle;
 | 
				
			||||||
  final String info;
 | 
					  final String info;
 | 
				
			||||||
  const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
 | 
					  const BackupInfoCard(
 | 
				
			||||||
 | 
					      {Key? key,
 | 
				
			||||||
 | 
					      required this.title,
 | 
				
			||||||
 | 
					      required this.subtitle,
 | 
				
			||||||
 | 
					      required this.info})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,8 @@ class AlbumPreviewPage extends HookConsumerWidget {
 | 
				
			|||||||
    final assets = useState<List<AssetEntity>>([]);
 | 
					    final assets = useState<List<AssetEntity>>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _getAssetsInAlbum() async {
 | 
					    _getAssetsInAlbum() async {
 | 
				
			||||||
      assets.value = await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
					      assets.value =
 | 
				
			||||||
 | 
					          await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
@ -37,7 +38,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
 | 
				
			|||||||
              padding: const EdgeInsets.only(top: 4.0),
 | 
					              padding: const EdgeInsets.only(top: 4.0),
 | 
				
			||||||
              child: Text(
 | 
					              child: Text(
 | 
				
			||||||
                "ID ${album.id}",
 | 
					                "ID ${album.id}",
 | 
				
			||||||
                style: TextStyle(fontSize: 10, color: Colors.grey[600], fontWeight: FontWeight.bold),
 | 
					                style: TextStyle(
 | 
				
			||||||
 | 
					                    fontSize: 10,
 | 
				
			||||||
 | 
					                    color: Colors.grey[600],
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
@ -55,8 +59,9 @@ class AlbumPreviewPage extends HookConsumerWidget {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
        itemCount: assets.value.length,
 | 
					        itemCount: assets.value.length,
 | 
				
			||||||
        itemBuilder: (context, index) {
 | 
					        itemBuilder: (context, index) {
 | 
				
			||||||
          Future<Uint8List?> thumbData =
 | 
					          Future<Uint8List?> thumbData = assets.value[index]
 | 
				
			||||||
              assets.value[index].thumbnailDataWithSize(const ThumbnailSize(200, 200), quality: 50);
 | 
					              .thumbnailDataWithSize(const ThumbnailSize(200, 200),
 | 
				
			||||||
 | 
					                  quality: 50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return FutureBuilder<Uint8List?>(
 | 
					          return FutureBuilder<Uint8List?>(
 | 
				
			||||||
            future: thumbData,
 | 
					            future: thumbData,
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
    final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
 | 
					    final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      ref.read(backupProvider.notifier).getBackupAlbumsInfo();
 | 
					      ref.read(backupProvider.notifier).getBackupInfo();
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }, []);
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,8 +37,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
          itemBuilder: ((context, index) {
 | 
					          itemBuilder: ((context, index) {
 | 
				
			||||||
            var thumbnailData = availableAlbums[index].thumbnailData;
 | 
					            var thumbnailData = availableAlbums[index].thumbnailData;
 | 
				
			||||||
            return Padding(
 | 
					            return Padding(
 | 
				
			||||||
              padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0),
 | 
					              padding: index == 0
 | 
				
			||||||
              child: AlbumInfoCard(imageData: thumbnailData, albumInfo: availableAlbums[index].albumEntity),
 | 
					                  ? const EdgeInsets.only(left: 16.00)
 | 
				
			||||||
 | 
					                  : const EdgeInsets.all(0),
 | 
				
			||||||
 | 
					              child: AlbumInfoCard(
 | 
				
			||||||
 | 
					                  imageData: thumbnailData,
 | 
				
			||||||
 | 
					                  albumInfo: availableAlbums[index].albumEntity),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@ -67,10 +71,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
            onTap: removeSelection,
 | 
					            onTap: removeSelection,
 | 
				
			||||||
            child: Chip(
 | 
					            child: Chip(
 | 
				
			||||||
              visualDensity: VisualDensity.compact,
 | 
					              visualDensity: VisualDensity.compact,
 | 
				
			||||||
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
					              shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
              label: Text(
 | 
					              label: Text(
 | 
				
			||||||
                album.name,
 | 
					                album.name,
 | 
				
			||||||
                style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
					                style: const TextStyle(
 | 
				
			||||||
 | 
					                    fontSize: 10,
 | 
				
			||||||
 | 
					                    color: Colors.white,
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              backgroundColor: Theme.of(context).primaryColor,
 | 
					              backgroundColor: Theme.of(context).primaryColor,
 | 
				
			||||||
              deleteIconColor: Colors.white,
 | 
					              deleteIconColor: Colors.white,
 | 
				
			||||||
@ -88,7 +96,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
    _buildExcludedAlbumNameChip() {
 | 
					    _buildExcludedAlbumNameChip() {
 | 
				
			||||||
      return excludedBackupAlbums.map((album) {
 | 
					      return excludedBackupAlbums.map((album) {
 | 
				
			||||||
        void removeSelection() {
 | 
					        void removeSelection() {
 | 
				
			||||||
          ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(backupProvider.notifier)
 | 
				
			||||||
 | 
					              .removeExcludedAlbumForBackup(album);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return GestureDetector(
 | 
					        return GestureDetector(
 | 
				
			||||||
@ -97,10 +107,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
            padding: const EdgeInsets.only(right: 8.0),
 | 
					            padding: const EdgeInsets.only(right: 8.0),
 | 
				
			||||||
            child: Chip(
 | 
					            child: Chip(
 | 
				
			||||||
              visualDensity: VisualDensity.compact,
 | 
					              visualDensity: VisualDensity.compact,
 | 
				
			||||||
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
					              shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
              label: Text(
 | 
					              label: Text(
 | 
				
			||||||
                album.name,
 | 
					                album.name,
 | 
				
			||||||
                style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
 | 
					                style: const TextStyle(
 | 
				
			||||||
 | 
					                    fontSize: 10,
 | 
				
			||||||
 | 
					                    color: Colors.white,
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              backgroundColor: Colors.red[300],
 | 
					              backgroundColor: Colors.red[300],
 | 
				
			||||||
              deleteIconColor: Colors.white,
 | 
					              deleteIconColor: Colors.white,
 | 
				
			||||||
@ -142,7 +156,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
          Padding(
 | 
					          Padding(
 | 
				
			||||||
            padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
				
			||||||
            child: Wrap(
 | 
					            child: Wrap(
 | 
				
			||||||
              children: [..._buildSelectedAlbumNameChip(), ..._buildExcludedAlbumNameChip()],
 | 
					              children: [
 | 
				
			||||||
 | 
					                ..._buildSelectedAlbumNameChip(),
 | 
				
			||||||
 | 
					                ..._buildExcludedAlbumNameChip()
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -165,10 +182,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
                    visualDensity: VisualDensity.compact,
 | 
					                    visualDensity: VisualDensity.compact,
 | 
				
			||||||
                    title: Text(
 | 
					                    title: Text(
 | 
				
			||||||
                      "Total unique assets",
 | 
					                      "Total unique assets",
 | 
				
			||||||
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
 | 
					                      style: TextStyle(
 | 
				
			||||||
 | 
					                          fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                          fontSize: 14,
 | 
				
			||||||
 | 
					                          color: Colors.grey[700]),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    trailing: Text(
 | 
					                    trailing: Text(
 | 
				
			||||||
                      ref.watch(backupProvider).allUniqueAssets.length.toString(),
 | 
					                      ref
 | 
				
			||||||
 | 
					                          .watch(backupProvider)
 | 
				
			||||||
 | 
					                          .allUniqueAssets
 | 
				
			||||||
 | 
					                          .length
 | 
				
			||||||
 | 
					                          .toString(),
 | 
				
			||||||
                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
					                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
@ -206,7 +230,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
                  context: context,
 | 
					                  context: context,
 | 
				
			||||||
                  builder: (BuildContext context) {
 | 
					                  builder: (BuildContext context) {
 | 
				
			||||||
                    return AlertDialog(
 | 
					                    return AlertDialog(
 | 
				
			||||||
                      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
 | 
					                      shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                          borderRadius: BorderRadius.circular(12)),
 | 
				
			||||||
                      elevation: 5,
 | 
					                      elevation: 5,
 | 
				
			||||||
                      title: Text(
 | 
					                      title: Text(
 | 
				
			||||||
                        'Selection Info',
 | 
					                        'Selection Info',
 | 
				
			||||||
@ -221,7 +246,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
 | 
				
			|||||||
                          children: [
 | 
					                          children: [
 | 
				
			||||||
                            Text(
 | 
					                            Text(
 | 
				
			||||||
                              'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
 | 
					                              '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]),
 | 
					                              style: TextStyle(
 | 
				
			||||||
 | 
					                                  fontSize: 14, color: Colors.grey[700]),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          ],
 | 
					                          ],
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,7 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
 | 
					      if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
 | 
				
			||||||
        ref.read(backupProvider.notifier).getBackupInfo();
 | 
					        ref.watch(backupProvider.notifier).getBackupInfo();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ref
 | 
					      ref
 | 
				
			||||||
@ -112,13 +112,15 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onPressed: () {
 | 
					                  onPressed: () {
 | 
				
			||||||
                    isAutoBackup
 | 
					                    if (isAutoBackup) {
 | 
				
			||||||
                        ? ref
 | 
					                      ref
 | 
				
			||||||
                            .watch(authenticationProvider.notifier)
 | 
					                          .read(authenticationProvider.notifier)
 | 
				
			||||||
                            .setAutoBackup(false)
 | 
					                          .setAutoBackup(false);
 | 
				
			||||||
                        : ref
 | 
					                    } else {
 | 
				
			||||||
                            .watch(authenticationProvider.notifier)
 | 
					                      ref
 | 
				
			||||||
 | 
					                          .read(authenticationProvider.notifier)
 | 
				
			||||||
                          .setAutoBackup(true);
 | 
					                          .setAutoBackup(true);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                  child: Text("Turn $backupBtnText Backup",
 | 
					                  child: Text("Turn $backupBtnText Backup",
 | 
				
			||||||
                      style: const TextStyle(fontWeight: FontWeight.bold)),
 | 
					                      style: const TextStyle(fontWeight: FontWeight.bold)),
 | 
				
			||||||
@ -212,7 +214,7 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                const Text(
 | 
					                const Text(
 | 
				
			||||||
                  "Albums to be backup",
 | 
					                  "Albums to be backed up",
 | 
				
			||||||
                  style: TextStyle(color: Color(0xFF808080), fontSize: 12),
 | 
					                  style: TextStyle(color: Color(0xFF808080), fontSize: 12),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                _buildSelectedAlbumName(),
 | 
					                _buildSelectedAlbumName(),
 | 
				
			||||||
@ -282,14 +284,12 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
            BackupInfoCard(
 | 
					            BackupInfoCard(
 | 
				
			||||||
              title: "Backup",
 | 
					              title: "Backup",
 | 
				
			||||||
              subtitle:
 | 
					              subtitle: "Backed up photos and videos",
 | 
				
			||||||
                  "Photos and videos from selected albums that are backup",
 | 
					 | 
				
			||||||
              info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
					              info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            BackupInfoCard(
 | 
					            BackupInfoCard(
 | 
				
			||||||
              title: "Remainder",
 | 
					              title: "Remainder",
 | 
				
			||||||
              subtitle:
 | 
					              subtitle: "Remaining photos and albums to back up from selection",
 | 
				
			||||||
                  "Photos and videos that has not been backing up from selected albums",
 | 
					 | 
				
			||||||
              info:
 | 
					              info:
 | 
				
			||||||
                  "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
					                  "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
				
			|||||||
@ -35,7 +35,8 @@ class DeleteAssetResponse {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory DeleteAssetResponse.fromJson(String source) => DeleteAssetResponse.fromMap(json.decode(source));
 | 
					  factory DeleteAssetResponse.fromJson(String source) =>
 | 
				
			||||||
 | 
					      DeleteAssetResponse.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
 | 
					  String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
 | 
				
			||||||
@ -44,7 +45,9 @@ class DeleteAssetResponse {
 | 
				
			|||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is DeleteAssetResponse && other.id == id && other.status == status;
 | 
					    return other is DeleteAssetResponse &&
 | 
				
			||||||
 | 
					        other.id == id &&
 | 
				
			||||||
 | 
					        other.status == status;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -31,13 +31,15 @@ class ImmichAssetGroupByDate {
 | 
				
			|||||||
  factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) {
 | 
					  factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
    return ImmichAssetGroupByDate(
 | 
					    return ImmichAssetGroupByDate(
 | 
				
			||||||
      date: map['date'] ?? '',
 | 
					      date: map['date'] ?? '',
 | 
				
			||||||
      assets: List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					      assets: List<ImmichAsset>.from(
 | 
				
			||||||
 | 
					          map['assets']?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ImmichAssetGroupByDate.fromJson(String source) => ImmichAssetGroupByDate.fromMap(json.decode(source));
 | 
					  factory ImmichAssetGroupByDate.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ImmichAssetGroupByDate.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
 | 
					  String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
 | 
				
			||||||
@ -46,7 +48,9 @@ class ImmichAssetGroupByDate {
 | 
				
			|||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is ImmichAssetGroupByDate && other.date == date && listEquals(other.assets, assets);
 | 
					    return other is ImmichAssetGroupByDate &&
 | 
				
			||||||
 | 
					        other.date == date &&
 | 
				
			||||||
 | 
					        listEquals(other.assets, assets);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -86,17 +90,20 @@ class GetAllAssetResponse {
 | 
				
			|||||||
  factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) {
 | 
					  factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
    return GetAllAssetResponse(
 | 
					    return GetAllAssetResponse(
 | 
				
			||||||
      count: map['count']?.toInt() ?? 0,
 | 
					      count: map['count']?.toInt() ?? 0,
 | 
				
			||||||
      data: List<ImmichAssetGroupByDate>.from(map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
 | 
					      data: List<ImmichAssetGroupByDate>.from(
 | 
				
			||||||
 | 
					          map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
 | 
				
			||||||
      nextPageKey: map['nextPageKey'] ?? '',
 | 
					      nextPageKey: map['nextPageKey'] ?? '',
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory GetAllAssetResponse.fromJson(String source) => GetAllAssetResponse.fromMap(json.decode(source));
 | 
					  factory GetAllAssetResponse.fromJson(String source) =>
 | 
				
			||||||
 | 
					      GetAllAssetResponse.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
				
			|||||||
@ -37,14 +37,16 @@ class HomePageState {
 | 
				
			|||||||
  factory HomePageState.fromMap(Map<String, dynamic> map) {
 | 
					  factory HomePageState.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
    return HomePageState(
 | 
					    return HomePageState(
 | 
				
			||||||
      isMultiSelectEnable: map['isMultiSelectEnable'] ?? false,
 | 
					      isMultiSelectEnable: map['isMultiSelectEnable'] ?? false,
 | 
				
			||||||
      selectedItems: Set<ImmichAsset>.from(map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					      selectedItems: Set<ImmichAsset>.from(
 | 
				
			||||||
 | 
					          map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
      selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
 | 
					      selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory HomePageState.fromJson(String source) => HomePageState.fromMap(json.decode(source));
 | 
					  factory HomePageState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      HomePageState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() =>
 | 
					  String toString() =>
 | 
				
			||||||
@ -62,5 +64,8 @@ class HomePageState {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode => isMultiSelectEnable.hashCode ^ selectedItems.hashCode ^ selectedDateGroup.hashCode;
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					      isMultiSelectEnable.hashCode ^
 | 
				
			||||||
 | 
					      selectedItems.hashCode ^
 | 
				
			||||||
 | 
					      selectedDateGroup.hashCode;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,8 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addSelectedDateGroup(String dateGroupTitle) {
 | 
					  void addSelectedDateGroup(String dateGroupTitle) {
 | 
				
			||||||
    state = state.copyWith(selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
 | 
					    state = state.copyWith(
 | 
				
			||||||
 | 
					        selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void removeSelectedDateGroup(String dateGroupTitle) {
 | 
					  void removeSelectedDateGroup(String dateGroupTitle) {
 | 
				
			||||||
@ -25,11 +26,13 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void enableMultiSelect(Set<ImmichAsset> selectedItems) {
 | 
					  void enableMultiSelect(Set<ImmichAsset> selectedItems) {
 | 
				
			||||||
    state = state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
 | 
					    state =
 | 
				
			||||||
 | 
					        state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void disableMultiSelect() {
 | 
					  void disableMultiSelect() {
 | 
				
			||||||
    state = state.copyWith(isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
 | 
					    state = state.copyWith(
 | 
				
			||||||
 | 
					        isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addSingleSelectedItem(ImmichAsset asset) {
 | 
					  void addSingleSelectedItem(ImmichAsset asset) {
 | 
				
			||||||
@ -60,4 +63,5 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final homePageStateProvider =
 | 
					final homePageStateProvider =
 | 
				
			||||||
    StateNotifierProvider<HomePageStateNotifier, HomePageState>(((ref) => HomePageStateNotifier()));
 | 
					    StateNotifierProvider<HomePageStateNotifier, HomePageState>(
 | 
				
			||||||
 | 
					        ((ref) => HomePageStateNotifier()));
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,8 @@ class ControlBottomAppBar extends StatelessWidget {
 | 
				
			|||||||
        width: MediaQuery.of(context).size.width,
 | 
					        width: MediaQuery.of(context).size.width,
 | 
				
			||||||
        height: MediaQuery.of(context).size.height * 0.15,
 | 
					        height: MediaQuery.of(context).size.height * 0.15,
 | 
				
			||||||
        decoration: BoxDecoration(
 | 
					        decoration: BoxDecoration(
 | 
				
			||||||
          borderRadius: const BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),
 | 
					          borderRadius: const BorderRadius.only(
 | 
				
			||||||
 | 
					              topLeft: Radius.circular(15), topRight: Radius.circular(15)),
 | 
				
			||||||
          color: Colors.grey[300]?.withOpacity(0.98),
 | 
					          color: Colors.grey[300]?.withOpacity(0.98),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
@ -46,7 +47,11 @@ class ControlBottomAppBar extends StatelessWidget {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ControlBoxButton extends StatelessWidget {
 | 
					class ControlBoxButton extends StatelessWidget {
 | 
				
			||||||
  const ControlBoxButton({Key? key, required this.label, required this.iconData, required this.onPressed})
 | 
					  const ControlBoxButton(
 | 
				
			||||||
 | 
					      {Key? key,
 | 
				
			||||||
 | 
					      required this.label,
 | 
				
			||||||
 | 
					      required this.iconData,
 | 
				
			||||||
 | 
					      required this.onPressed})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final String label;
 | 
					  final String label;
 | 
				
			||||||
 | 
				
			|||||||
@ -18,9 +18,12 @@ class DailyTitleText extends ConsumerWidget {
 | 
				
			|||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var currentYear = DateTime.now().year;
 | 
					    var currentYear = DateTime.now().year;
 | 
				
			||||||
    var groupYear = DateTime.parse(isoDate).year;
 | 
					    var groupYear = DateTime.parse(isoDate).year;
 | 
				
			||||||
    var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
 | 
					    var formatDateTemplate =
 | 
				
			||||||
    var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
 | 
					        currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
 | 
				
			||||||
    var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
					    var dateText =
 | 
				
			||||||
 | 
					        DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
 | 
				
			||||||
 | 
					    var isMultiSelectEnable =
 | 
				
			||||||
 | 
					        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
				
			||||||
    var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
 | 
					    var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
 | 
				
			||||||
    var selectedItems = ref.watch(homePageStateProvider).selectedItems;
 | 
					    var selectedItems = ref.watch(homePageStateProvider).selectedItems;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,23 +38,42 @@ class DailyTitleText extends ConsumerWidget {
 | 
				
			|||||||
          selectedDateGroup.contains(dateText) &&
 | 
					          selectedDateGroup.contains(dateText) &&
 | 
				
			||||||
          selectedItems.length != assetGroup.length) {
 | 
					          selectedItems.length != assetGroup.length) {
 | 
				
			||||||
        // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
 | 
					        // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
 | 
					        ref
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
      } else if (isMultiSelectEnable && selectedDateGroup.contains(dateText) && selectedDateGroup.length > 1) {
 | 
					            .removeSelectedDateGroup(dateText);
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
 | 
					        ref
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .removeMultipleSelectedItem(assetGroup);
 | 
				
			||||||
 | 
					      } else if (isMultiSelectEnable &&
 | 
				
			||||||
 | 
					          selectedDateGroup.contains(dateText) &&
 | 
				
			||||||
 | 
					          selectedDateGroup.length > 1) {
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .removeSelectedDateGroup(dateText);
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .removeMultipleSelectedItem(assetGroup);
 | 
				
			||||||
      } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
 | 
					      } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
 | 
					        ref
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup);
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .addSelectedDateGroup(dateText);
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .addMultipleSelectedItems(assetGroup);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet());
 | 
					        ref
 | 
				
			||||||
        ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .enableMultiSelect(assetGroup.toSet());
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					            .addSelectedDateGroup(dateText);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SliverToBoxAdapter(
 | 
					    return SliverToBoxAdapter(
 | 
				
			||||||
      child: Padding(
 | 
					      child: Padding(
 | 
				
			||||||
        padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
 | 
					        padding: const EdgeInsets.only(
 | 
				
			||||||
 | 
					            top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
 | 
				
			||||||
        child: Row(
 | 
					        child: Row(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            Text(
 | 
					            Text(
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,8 @@ class DeleteDialog extends ConsumerWidget {
 | 
				
			|||||||
      backgroundColor: Colors.grey[200],
 | 
					      backgroundColor: Colors.grey[200],
 | 
				
			||||||
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
 | 
					      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
 | 
				
			||||||
      title: const Text("Delete Permanently"),
 | 
					      title: const Text("Delete Permanently"),
 | 
				
			||||||
      content: const Text("These items will be permanently deleted from Immich and from your device"),
 | 
					      content: const Text(
 | 
				
			||||||
 | 
					          "These items will be permanently deleted from Immich and from your device"),
 | 
				
			||||||
      actions: [
 | 
					      actions: [
 | 
				
			||||||
        TextButton(
 | 
					        TextButton(
 | 
				
			||||||
          onPressed: () {
 | 
					          onPressed: () {
 | 
				
			||||||
@ -27,7 +28,9 @@ class DeleteDialog extends ConsumerWidget {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
        TextButton(
 | 
					        TextButton(
 | 
				
			||||||
          onPressed: () {
 | 
					          onPressed: () {
 | 
				
			||||||
            ref.watch(assetProvider.notifier).deleteAssets(homePageState.selectedItems);
 | 
					            ref
 | 
				
			||||||
 | 
					                .watch(assetProvider.notifier)
 | 
				
			||||||
 | 
					                .deleteAssets(homePageState.selectedItems);
 | 
				
			||||||
            ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
					            ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Navigator.of(context).pop();
 | 
					            Navigator.of(context).pop();
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,10 @@ class ImageGrid extends ConsumerWidget {
 | 
				
			|||||||
                        child: Row(
 | 
					                        child: Row(
 | 
				
			||||||
                          children: [
 | 
					                          children: [
 | 
				
			||||||
                            Text(
 | 
					                            Text(
 | 
				
			||||||
                              assetGroup[index].duration.toString().substring(0, 7),
 | 
					                              assetGroup[index]
 | 
				
			||||||
 | 
					                                  .duration
 | 
				
			||||||
 | 
					                                  .toString()
 | 
				
			||||||
 | 
					                                  .substring(0, 7),
 | 
				
			||||||
                              style: const TextStyle(
 | 
					                              style: const TextStyle(
 | 
				
			||||||
                                color: Colors.white,
 | 
					                                color: Colors.white,
 | 
				
			||||||
                                fontSize: 10,
 | 
					                                fontSize: 10,
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,8 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
 | 
					        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
 | 
					    var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
 | 
				
			||||||
    var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
					    var isMultiSelectEnable =
 | 
				
			||||||
 | 
					        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
				
			||||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
					    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Widget _buildSelectionIcon(ImmichAsset asset) {
 | 
					    Widget _buildSelectionIcon(ImmichAsset asset) {
 | 
				
			||||||
@ -45,12 +46,20 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
    return GestureDetector(
 | 
					    return GestureDetector(
 | 
				
			||||||
      onTap: () {
 | 
					      onTap: () {
 | 
				
			||||||
        debugPrint("View ${asset.id}");
 | 
					        debugPrint("View ${asset.id}");
 | 
				
			||||||
        if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) {
 | 
					        if (isMultiSelectEnable &&
 | 
				
			||||||
 | 
					            selectedAsset.contains(asset) &&
 | 
				
			||||||
 | 
					            selectedAsset.length == 1) {
 | 
				
			||||||
          ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
					          ref.watch(homePageStateProvider.notifier).disableMultiSelect();
 | 
				
			||||||
        } else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) {
 | 
					        } else if (isMultiSelectEnable &&
 | 
				
			||||||
          ref.watch(homePageStateProvider.notifier).removeSingleSelectedItem(asset);
 | 
					            selectedAsset.contains(asset) &&
 | 
				
			||||||
 | 
					            selectedAsset.length > 1) {
 | 
				
			||||||
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					              .removeSingleSelectedItem(asset);
 | 
				
			||||||
        } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
 | 
					        } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
 | 
				
			||||||
          ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(homePageStateProvider.notifier)
 | 
				
			||||||
 | 
					              .addSingleSelectedItem(asset);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          if (asset.type == 'IMAGE') {
 | 
					          if (asset.type == 'IMAGE') {
 | 
				
			||||||
            AutoRouter.of(context).push(
 | 
					            AutoRouter.of(context).push(
 | 
				
			||||||
@ -65,7 +74,8 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            AutoRouter.of(context).push(
 | 
					            AutoRouter.of(context).push(
 | 
				
			||||||
              VideoViewerRoute(
 | 
					              VideoViewerRoute(
 | 
				
			||||||
                  videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
 | 
					                  videoUrl:
 | 
				
			||||||
 | 
					                      '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
 | 
				
			||||||
                  asset: asset),
 | 
					                  asset: asset),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -83,7 +93,8 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
            Container(
 | 
					            Container(
 | 
				
			||||||
              decoration: BoxDecoration(
 | 
					              decoration: BoxDecoration(
 | 
				
			||||||
                border: isMultiSelectEnable && selectedAsset.contains(asset)
 | 
					                border: isMultiSelectEnable && selectedAsset.contains(asset)
 | 
				
			||||||
                    ? Border.all(color: Theme.of(context).primaryColorLight, width: 10)
 | 
					                    ? Border.all(
 | 
				
			||||||
 | 
					                        color: Theme.of(context).primaryColorLight, width: 10)
 | 
				
			||||||
                    : const Border(),
 | 
					                    : const Border(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              child: CachedNetworkImage(
 | 
					              child: CachedNetworkImage(
 | 
				
			||||||
@ -93,11 +104,15 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
                memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
 | 
					                memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
 | 
				
			||||||
                fit: BoxFit.cover,
 | 
					                fit: BoxFit.cover,
 | 
				
			||||||
                imageUrl: thumbnailRequestUrl,
 | 
					                imageUrl: thumbnailRequestUrl,
 | 
				
			||||||
                httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					                httpHeaders: {
 | 
				
			||||||
 | 
					                  "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
                fadeInDuration: const Duration(milliseconds: 250),
 | 
					                fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
                progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
 | 
					                progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
				
			||||||
 | 
					                    Transform.scale(
 | 
				
			||||||
                  scale: 0.2,
 | 
					                  scale: 0.2,
 | 
				
			||||||
                  child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
					                  child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					                      value: downloadProgress.progress),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                errorWidget: (context, url, error) {
 | 
					                errorWidget: (context, url, error) {
 | 
				
			||||||
                  return Icon(
 | 
					                  return Icon(
 | 
				
			||||||
@ -122,7 +137,9 @@ class ThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
              right: 10,
 | 
					              right: 10,
 | 
				
			||||||
              bottom: 5,
 | 
					              bottom: 5,
 | 
				
			||||||
              child: Icon(
 | 
					              child: Icon(
 | 
				
			||||||
                (deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
 | 
					                (deviceId != asset.deviceId)
 | 
				
			||||||
 | 
					                    ? Icons.cloud_done_outlined
 | 
				
			||||||
 | 
					                    : Icons.photo_library_rounded,
 | 
				
			||||||
                color: Colors.white,
 | 
					                color: Colors.white,
 | 
				
			||||||
                size: 18,
 | 
					                size: 18,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
				
			|||||||
@ -98,7 +98,8 @@ class AuthenticationState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory AuthenticationState.fromJson(String source) => AuthenticationState.fromMap(json.decode(source));
 | 
					  factory AuthenticationState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      AuthenticationState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
				
			|||||||
@ -16,5 +16,9 @@ class HiveSavedLoginInfo {
 | 
				
			|||||||
  @HiveField(3)
 | 
					  @HiveField(3)
 | 
				
			||||||
  bool isSaveLogin;
 | 
					  bool isSaveLogin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  HiveSavedLoginInfo({required this.email, required this.password, required this.serverUrl, required this.isSaveLogin});
 | 
					  HiveSavedLoginInfo(
 | 
				
			||||||
 | 
					      {required this.email,
 | 
				
			||||||
 | 
					      required this.password,
 | 
				
			||||||
 | 
					      required this.serverUrl,
 | 
				
			||||||
 | 
					      required this.isSaveLogin});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -73,7 +73,8 @@ class LogInReponse {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
 | 
					  factory LogInReponse.fromJson(String source) =>
 | 
				
			||||||
 | 
					      LogInReponse.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -15,13 +15,17 @@ class LoginForm extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final usernameController = useTextEditingController.fromValue(TextEditingValue.empty);
 | 
					    final usernameController =
 | 
				
			||||||
    final passwordController = useTextEditingController.fromValue(TextEditingValue.empty);
 | 
					        useTextEditingController.fromValue(TextEditingValue.empty);
 | 
				
			||||||
    final serverEndpointController = useTextEditingController(text: 'http://your-server-ip:2283');
 | 
					    final passwordController =
 | 
				
			||||||
 | 
					        useTextEditingController.fromValue(TextEditingValue.empty);
 | 
				
			||||||
 | 
					    final serverEndpointController =
 | 
				
			||||||
 | 
					        useTextEditingController(text: 'http://your-server-ip:2283');
 | 
				
			||||||
    final isSaveLoginInfo = useState<bool>(false);
 | 
					    final isSaveLoginInfo = useState<bool>(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
 | 
					      var loginInfo =
 | 
				
			||||||
 | 
					          Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (loginInfo != null) {
 | 
					      if (loginInfo != null) {
 | 
				
			||||||
        usernameController.text = loginInfo.email;
 | 
					        usernameController.text = loginInfo.email;
 | 
				
			||||||
@ -64,11 +68,15 @@ class LoginForm extends HookConsumerWidget {
 | 
				
			|||||||
                contentPadding: const EdgeInsets.symmetric(horizontal: 8),
 | 
					                contentPadding: const EdgeInsets.symmetric(horizontal: 8),
 | 
				
			||||||
                dense: true,
 | 
					                dense: true,
 | 
				
			||||||
                side: const BorderSide(color: Colors.grey, width: 1.5),
 | 
					                side: const BorderSide(color: Colors.grey, width: 1.5),
 | 
				
			||||||
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
 | 
					                shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                    borderRadius: BorderRadius.circular(5)),
 | 
				
			||||||
                enableFeedback: true,
 | 
					                enableFeedback: true,
 | 
				
			||||||
                title: const Text(
 | 
					                title: const Text(
 | 
				
			||||||
                  "Stay logged in",
 | 
					                  "Stay logged in",
 | 
				
			||||||
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
 | 
					                  style: TextStyle(
 | 
				
			||||||
 | 
					                      fontSize: 16,
 | 
				
			||||||
 | 
					                      fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                      color: Colors.grey),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                value: isSaveLoginInfo.value,
 | 
					                value: isSaveLoginInfo.value,
 | 
				
			||||||
                onChanged: (switchValue) {
 | 
					                onChanged: (switchValue) {
 | 
				
			||||||
@ -94,11 +102,13 @@ class LoginForm extends HookConsumerWidget {
 | 
				
			|||||||
class ServerEndpointInput extends StatelessWidget {
 | 
					class ServerEndpointInput extends StatelessWidget {
 | 
				
			||||||
  final TextEditingController controller;
 | 
					  final TextEditingController controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ServerEndpointInput({Key? key, required this.controller}) : super(key: key);
 | 
					  const ServerEndpointInput({Key? key, required this.controller})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? _validateInput(String? url) {
 | 
					  String? _validateInput(String? url) {
 | 
				
			||||||
    if (url == null) return null;
 | 
					    if (url == null) return null;
 | 
				
			||||||
    if (!url.startsWith(RegExp(r'https?://'))) return 'Please specify http:// or https://';
 | 
					    if (!url.startsWith(RegExp(r'https?://')))
 | 
				
			||||||
 | 
					      return 'Please specify http:// or https://';
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -107,7 +117,9 @@ class ServerEndpointInput extends StatelessWidget {
 | 
				
			|||||||
    return TextFormField(
 | 
					    return TextFormField(
 | 
				
			||||||
      controller: controller,
 | 
					      controller: controller,
 | 
				
			||||||
      decoration: const InputDecoration(
 | 
					      decoration: const InputDecoration(
 | 
				
			||||||
          labelText: 'Server Endpoint URL', border: OutlineInputBorder(), hintText: 'http://your-server-ip:port'),
 | 
					          labelText: 'Server Endpoint URL',
 | 
				
			||||||
 | 
					          border: OutlineInputBorder(),
 | 
				
			||||||
 | 
					          hintText: 'http://your-server-ip:port'),
 | 
				
			||||||
      validator: _validateInput,
 | 
					      validator: _validateInput,
 | 
				
			||||||
      autovalidateMode: AutovalidateMode.always,
 | 
					      autovalidateMode: AutovalidateMode.always,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -131,8 +143,10 @@ class EmailInput extends StatelessWidget {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return TextFormField(
 | 
					    return TextFormField(
 | 
				
			||||||
      controller: controller,
 | 
					      controller: controller,
 | 
				
			||||||
      decoration:
 | 
					      decoration: const InputDecoration(
 | 
				
			||||||
          const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
 | 
					          labelText: 'Email',
 | 
				
			||||||
 | 
					          border: OutlineInputBorder(),
 | 
				
			||||||
 | 
					          hintText: 'youremail@email.com'),
 | 
				
			||||||
      validator: _validateInput,
 | 
					      validator: _validateInput,
 | 
				
			||||||
      autovalidateMode: AutovalidateMode.always,
 | 
					      autovalidateMode: AutovalidateMode.always,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -149,7 +163,10 @@ class PasswordInput extends StatelessWidget {
 | 
				
			|||||||
    return TextFormField(
 | 
					    return TextFormField(
 | 
				
			||||||
      obscureText: true,
 | 
					      obscureText: true,
 | 
				
			||||||
      controller: controller,
 | 
					      controller: controller,
 | 
				
			||||||
      decoration: const InputDecoration(labelText: 'Password', border: OutlineInputBorder(), hintText: 'password'),
 | 
					      decoration: const InputDecoration(
 | 
				
			||||||
 | 
					          labelText: 'Password',
 | 
				
			||||||
 | 
					          border: OutlineInputBorder(),
 | 
				
			||||||
 | 
					          hintText: 'password'),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -184,7 +201,8 @@ class LoginButton extends ConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          var isAuthenticated = await ref
 | 
					          var isAuthenticated = await ref
 | 
				
			||||||
              .read(authenticationProvider.notifier)
 | 
					              .read(authenticationProvider.notifier)
 | 
				
			||||||
              .login(emailController.text, passwordController.text, serverEndpointController.text, isSavedLoginInfo);
 | 
					              .login(emailController.text, passwordController.text,
 | 
				
			||||||
 | 
					                  serverEndpointController.text, isSavedLoginInfo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (isAuthenticated) {
 | 
					          if (isAuthenticated) {
 | 
				
			||||||
            // Resume backup (if enable) then navigate
 | 
					            // Resume backup (if enable) then navigate
 | 
				
			||||||
@ -193,7 +211,8 @@ class LoginButton extends ConsumerWidget {
 | 
				
			|||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            ImmichToast.show(
 | 
					            ImmichToast.show(
 | 
				
			||||||
              context: context,
 | 
					              context: context,
 | 
				
			||||||
              msg: "Error logging you in, check server url, email and password!",
 | 
					              msg:
 | 
				
			||||||
 | 
					                  "Error logging you in, check server url, email and password!",
 | 
				
			||||||
              toastType: ToastType.error,
 | 
					              toastType: ToastType.error,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
				
			|||||||
@ -53,7 +53,8 @@ class CuratedLocation {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory CuratedLocation.fromJson(String source) => CuratedLocation.fromMap(json.decode(source));
 | 
					  factory CuratedLocation.fromJson(String source) =>
 | 
				
			||||||
 | 
					      CuratedLocation.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
@ -74,6 +75,10 @@ class CuratedLocation {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode {
 | 
					  int get hashCode {
 | 
				
			||||||
    return id.hashCode ^ city.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
 | 
					    return id.hashCode ^
 | 
				
			||||||
 | 
					        city.hashCode ^
 | 
				
			||||||
 | 
					        resizePath.hashCode ^
 | 
				
			||||||
 | 
					        deviceAssetId.hashCode ^
 | 
				
			||||||
 | 
					        deviceId.hashCode;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,8 @@ class CuratedObject {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory CuratedObject.fromJson(String source) => CuratedObject.fromMap(json.decode(source));
 | 
					  factory CuratedObject.fromJson(String source) =>
 | 
				
			||||||
 | 
					      CuratedObject.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
@ -75,6 +76,10 @@ class CuratedObject {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode {
 | 
					  int get hashCode {
 | 
				
			||||||
    return id.hashCode ^ object.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
 | 
					    return id.hashCode ^
 | 
				
			||||||
 | 
					        object.hashCode ^
 | 
				
			||||||
 | 
					        resizePath.hashCode ^
 | 
				
			||||||
 | 
					        deviceAssetId.hashCode ^
 | 
				
			||||||
 | 
					        deviceId.hashCode;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,8 @@ class SearchPageState {
 | 
				
			|||||||
      searchTerm: searchTerm ?? this.searchTerm,
 | 
					      searchTerm: searchTerm ?? this.searchTerm,
 | 
				
			||||||
      isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
 | 
					      isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
 | 
				
			||||||
      searchSuggestion: searchSuggestion ?? this.searchSuggestion,
 | 
					      searchSuggestion: searchSuggestion ?? this.searchSuggestion,
 | 
				
			||||||
      userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
 | 
					      userSuggestedSearchTerms:
 | 
				
			||||||
 | 
					          userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -43,13 +44,15 @@ class SearchPageState {
 | 
				
			|||||||
      searchTerm: map['searchTerm'] ?? '',
 | 
					      searchTerm: map['searchTerm'] ?? '',
 | 
				
			||||||
      isSearchEnabled: map['isSearchEnabled'] ?? false,
 | 
					      isSearchEnabled: map['isSearchEnabled'] ?? false,
 | 
				
			||||||
      searchSuggestion: List<String>.from(map['searchSuggestion']),
 | 
					      searchSuggestion: List<String>.from(map['searchSuggestion']),
 | 
				
			||||||
      userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']),
 | 
					      userSuggestedSearchTerms:
 | 
				
			||||||
 | 
					          List<String>.from(map['userSuggestedSearchTerms']),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source));
 | 
					  factory SearchPageState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      SearchPageState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -44,13 +44,15 @@ class SearchResultPageState {
 | 
				
			|||||||
      isLoading: map['isLoading'] ?? false,
 | 
					      isLoading: map['isLoading'] ?? false,
 | 
				
			||||||
      isSuccess: map['isSuccess'] ?? false,
 | 
					      isSuccess: map['isSuccess'] ?? false,
 | 
				
			||||||
      isError: map['isError'] ?? false,
 | 
					      isError: map['isError'] ?? false,
 | 
				
			||||||
      searchResult: List<ImmichAsset>.from(map['searchResult']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					      searchResult: List<ImmichAsset>.from(
 | 
				
			||||||
 | 
					          map['searchResult']?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory SearchResultPageState.fromJson(String source) => SearchResultPageState.fromMap(json.decode(source));
 | 
					  factory SearchResultPageState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      SearchResultPageState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
@ -71,6 +73,9 @@ class SearchResultPageState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode {
 | 
					  int get hashCode {
 | 
				
			||||||
    return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
 | 
					    return isLoading.hashCode ^
 | 
				
			||||||
 | 
					        isSuccess.hashCode ^
 | 
				
			||||||
 | 
					        isError.hashCode ^
 | 
				
			||||||
 | 
					        searchResult.hashCode;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
					import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
 | 
					class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
 | 
				
			||||||
  SearchBar({Key? key, required this.searchFocusNode, required this.onSubmitted}) : super(key: key);
 | 
					  SearchBar(
 | 
				
			||||||
 | 
					      {Key? key, required this.searchFocusNode, required this.onSubmitted})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final FocusNode searchFocusNode;
 | 
					  final FocusNode searchFocusNode;
 | 
				
			||||||
  final Function(String) onSubmitted;
 | 
					  final Function(String) onSubmitted;
 | 
				
			||||||
 | 
				
			|||||||
@ -3,16 +3,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
					import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SearchSuggestionList extends ConsumerWidget {
 | 
					class SearchSuggestionList extends ConsumerWidget {
 | 
				
			||||||
  const SearchSuggestionList({Key? key, required this.onSubmitted}) : super(key: key);
 | 
					  const SearchSuggestionList({Key? key, required this.onSubmitted})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Function(String) onSubmitted;
 | 
					  final Function(String) onSubmitted;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
 | 
					    final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
 | 
				
			||||||
    final searchSuggestion = ref.watch(searchPageStateProvider).searchSuggestion;
 | 
					    final searchSuggestion =
 | 
				
			||||||
 | 
					        ref.watch(searchPageStateProvider).searchSuggestion;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Container(
 | 
					    return Container(
 | 
				
			||||||
      color: searchTerm.isEmpty ? Colors.black.withOpacity(0.5) : Theme.of(context).scaffoldBackgroundColor,
 | 
					      color: searchTerm.isEmpty
 | 
				
			||||||
 | 
					          ? Colors.black.withOpacity(0.5)
 | 
				
			||||||
 | 
					          : Theme.of(context).scaffoldBackgroundColor,
 | 
				
			||||||
      child: CustomScrollView(
 | 
					      child: CustomScrollView(
 | 
				
			||||||
        slivers: [
 | 
					        slivers: [
 | 
				
			||||||
          SliverFillRemaining(
 | 
					          SliverFillRemaining(
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,11 @@ import 'package:immich_mobile/constants/hive_box.dart';
 | 
				
			|||||||
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
 | 
					import 'package:immich_mobile/utils/capitalize_first_letter.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ThumbnailWithInfo extends StatelessWidget {
 | 
					class ThumbnailWithInfo extends StatelessWidget {
 | 
				
			||||||
  const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
 | 
					  const ThumbnailWithInfo(
 | 
				
			||||||
 | 
					      {Key? key,
 | 
				
			||||||
 | 
					      required this.textInfo,
 | 
				
			||||||
 | 
					      required this.imageUrl,
 | 
				
			||||||
 | 
					      required this.onTap})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final String textInfo;
 | 
					  final String textInfo;
 | 
				
			||||||
@ -39,7 +43,9 @@ class ThumbnailWithInfo extends StatelessWidget {
 | 
				
			|||||||
                    height: 250,
 | 
					                    height: 250,
 | 
				
			||||||
                    fit: BoxFit.cover,
 | 
					                    fit: BoxFit.cover,
 | 
				
			||||||
                    imageUrl: imageUrl,
 | 
					                    imageUrl: imageUrl,
 | 
				
			||||||
                    httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					                    httpHeaders: {
 | 
				
			||||||
 | 
					                      "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
				
			|||||||
@ -36,16 +36,20 @@ class AlbumViewerPageState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source));
 | 
					  factory AlbumViewerPageState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      AlbumViewerPageState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is AlbumViewerPageState && other.isEditAlbum == isEditAlbum && other.editTitleText == editTitleText;
 | 
					    return other is AlbumViewerPageState &&
 | 
				
			||||||
 | 
					        other.isEditAlbum == isEditAlbum &&
 | 
				
			||||||
 | 
					        other.editTitleText == editTitleText;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,8 @@ class AssetSelectionPageResult {
 | 
				
			|||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return AssetSelectionPageResult(
 | 
					    return AssetSelectionPageResult(
 | 
				
			||||||
      selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
 | 
					      selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
 | 
				
			||||||
      selectedAdditionalAsset: selectedAdditionalAsset ?? this.selectedAdditionalAsset,
 | 
					      selectedAdditionalAsset:
 | 
				
			||||||
 | 
					          selectedAdditionalAsset ?? this.selectedAdditionalAsset,
 | 
				
			||||||
      isAlbumExist: isAlbumExist ?? this.isAlbumExist,
 | 
					      isAlbumExist: isAlbumExist ?? this.isAlbumExist,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -30,8 +31,12 @@ class AssetSelectionPageResult {
 | 
				
			|||||||
  Map<String, dynamic> toMap() {
 | 
					  Map<String, dynamic> toMap() {
 | 
				
			||||||
    final result = <String, dynamic>{};
 | 
					    final result = <String, dynamic>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result.addAll({'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
 | 
					    result.addAll(
 | 
				
			||||||
    result.addAll({'selectedAdditionalAsset': selectedAdditionalAsset.map((x) => x.toMap()).toList()});
 | 
					        {'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
 | 
				
			||||||
 | 
					    result.addAll({
 | 
				
			||||||
 | 
					      'selectedAdditionalAsset':
 | 
				
			||||||
 | 
					          selectedAdditionalAsset.map((x) => x.toMap()).toList()
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    result.addAll({'isAlbumExist': isAlbumExist});
 | 
					    result.addAll({'isAlbumExist': isAlbumExist});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
@ -39,16 +44,18 @@ class AssetSelectionPageResult {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
 | 
					  factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
    return AssetSelectionPageResult(
 | 
					    return AssetSelectionPageResult(
 | 
				
			||||||
      selectedNewAsset: Set<ImmichAsset>.from(map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					      selectedNewAsset: Set<ImmichAsset>.from(
 | 
				
			||||||
      selectedAdditionalAsset:
 | 
					          map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
          Set<ImmichAsset>.from(map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					      selectedAdditionalAsset: Set<ImmichAsset>.from(
 | 
				
			||||||
 | 
					          map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
      isAlbumExist: map['isAlbumExist'] ?? false,
 | 
					      isAlbumExist: map['isAlbumExist'] ?? false,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory AssetSelectionPageResult.fromJson(String source) => AssetSelectionPageResult.fromMap(json.decode(source));
 | 
					  factory AssetSelectionPageResult.fromJson(String source) =>
 | 
				
			||||||
 | 
					      AssetSelectionPageResult.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() =>
 | 
					  String toString() =>
 | 
				
			||||||
@ -66,5 +73,8 @@ class AssetSelectionPageResult {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode => selectedNewAsset.hashCode ^ selectedAdditionalAsset.hashCode ^ isAlbumExist.hashCode;
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					      selectedNewAsset.hashCode ^
 | 
				
			||||||
 | 
					      selectedAdditionalAsset.hashCode ^
 | 
				
			||||||
 | 
					      isAlbumExist.hashCode;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -32,9 +32,12 @@ class AssetSelectionState {
 | 
				
			|||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return AssetSelectionState(
 | 
					    return AssetSelectionState(
 | 
				
			||||||
      selectedMonths: selectedMonths ?? this.selectedMonths,
 | 
					      selectedMonths: selectedMonths ?? this.selectedMonths,
 | 
				
			||||||
      selectedNewAssetsForAlbum: selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
 | 
					      selectedNewAssetsForAlbum:
 | 
				
			||||||
      selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ?? this.selectedAdditionalAssetsForAlbum,
 | 
					          selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
 | 
				
			||||||
      selectedAssetsInAlbumViewer: selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
 | 
					      selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ??
 | 
				
			||||||
 | 
					          this.selectedAdditionalAssetsForAlbum,
 | 
				
			||||||
 | 
					      selectedAssetsInAlbumViewer:
 | 
				
			||||||
 | 
					          selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
 | 
				
			||||||
      isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
 | 
					      isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
 | 
				
			||||||
      isAlbumExist: isAlbumExist ?? this.isAlbumExist,
 | 
					      isAlbumExist: isAlbumExist ?? this.isAlbumExist,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -44,10 +47,18 @@ class AssetSelectionState {
 | 
				
			|||||||
    final result = <String, dynamic>{};
 | 
					    final result = <String, dynamic>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result.addAll({'selectedMonths': selectedMonths.toList()});
 | 
					    result.addAll({'selectedMonths': selectedMonths.toList()});
 | 
				
			||||||
    result.addAll({'selectedNewAssetsForAlbum': selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()});
 | 
					    result.addAll({
 | 
				
			||||||
    result
 | 
					      'selectedNewAssetsForAlbum':
 | 
				
			||||||
        .addAll({'selectedAdditionalAssetsForAlbum': selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()});
 | 
					          selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()
 | 
				
			||||||
    result.addAll({'selectedAssetsInAlbumViewer': selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()});
 | 
					    });
 | 
				
			||||||
 | 
					    result.addAll({
 | 
				
			||||||
 | 
					      'selectedAdditionalAssetsForAlbum':
 | 
				
			||||||
 | 
					          selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    result.addAll({
 | 
				
			||||||
 | 
					      'selectedAssetsInAlbumViewer':
 | 
				
			||||||
 | 
					          selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    result.addAll({'isMultiselectEnable': isMultiselectEnable});
 | 
					    result.addAll({'isMultiselectEnable': isMultiselectEnable});
 | 
				
			||||||
    result.addAll({'isAlbumExist': isAlbumExist});
 | 
					    result.addAll({'isAlbumExist': isAlbumExist});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,12 +68,14 @@ class AssetSelectionState {
 | 
				
			|||||||
  factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
 | 
					  factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
 | 
				
			||||||
    return AssetSelectionState(
 | 
					    return AssetSelectionState(
 | 
				
			||||||
      selectedMonths: Set<String>.from(map['selectedMonths']),
 | 
					      selectedMonths: Set<String>.from(map['selectedMonths']),
 | 
				
			||||||
      selectedNewAssetsForAlbum:
 | 
					      selectedNewAssetsForAlbum: Set<ImmichAsset>.from(
 | 
				
			||||||
          Set<ImmichAsset>.from(map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					          map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
      selectedAdditionalAssetsForAlbum:
 | 
					      selectedAdditionalAssetsForAlbum: Set<ImmichAsset>.from(
 | 
				
			||||||
          Set<ImmichAsset>.from(map['selectedAdditionalAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					          map['selectedAdditionalAssetsForAlbum']
 | 
				
			||||||
      selectedAssetsInAlbumViewer:
 | 
					              ?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
          Set<ImmichAsset>.from(map['selectedAssetsInAlbumViewer']?.map((x) => ImmichAsset.fromMap(x))),
 | 
					      selectedAssetsInAlbumViewer: Set<ImmichAsset>.from(
 | 
				
			||||||
 | 
					          map['selectedAssetsInAlbumViewer']
 | 
				
			||||||
 | 
					              ?.map((x) => ImmichAsset.fromMap(x))),
 | 
				
			||||||
      isMultiselectEnable: map['isMultiselectEnable'] ?? false,
 | 
					      isMultiselectEnable: map['isMultiselectEnable'] ?? false,
 | 
				
			||||||
      isAlbumExist: map['isAlbumExist'] ?? false,
 | 
					      isAlbumExist: map['isAlbumExist'] ?? false,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -70,7 +83,8 @@ class AssetSelectionState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory AssetSelectionState.fromJson(String source) => AssetSelectionState.fromMap(json.decode(source));
 | 
					  factory AssetSelectionState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      AssetSelectionState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
@ -85,8 +99,10 @@ class AssetSelectionState {
 | 
				
			|||||||
    return other is AssetSelectionState &&
 | 
					    return other is AssetSelectionState &&
 | 
				
			||||||
        setEquals(other.selectedMonths, selectedMonths) &&
 | 
					        setEquals(other.selectedMonths, selectedMonths) &&
 | 
				
			||||||
        setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
 | 
					        setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
 | 
				
			||||||
        setEquals(other.selectedAdditionalAssetsForAlbum, selectedAdditionalAssetsForAlbum) &&
 | 
					        setEquals(other.selectedAdditionalAssetsForAlbum,
 | 
				
			||||||
        setEquals(other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
 | 
					            selectedAdditionalAssetsForAlbum) &&
 | 
				
			||||||
 | 
					        setEquals(
 | 
				
			||||||
 | 
					            other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
 | 
				
			||||||
        other.isMultiselectEnable == isMultiselectEnable &&
 | 
					        other.isMultiselectEnable == isMultiselectEnable &&
 | 
				
			||||||
        other.isAlbumExist == isAlbumExist;
 | 
					        other.isAlbumExist == isAlbumExist;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,8 @@ class SharedAlbum {
 | 
				
			|||||||
      ownerId: ownerId ?? this.ownerId,
 | 
					      ownerId: ownerId ?? this.ownerId,
 | 
				
			||||||
      albumName: albumName ?? this.albumName,
 | 
					      albumName: albumName ?? this.albumName,
 | 
				
			||||||
      createdAt: createdAt ?? this.createdAt,
 | 
					      createdAt: createdAt ?? this.createdAt,
 | 
				
			||||||
      albumThumbnailAssetId: albumThumbnailAssetId ?? this.albumThumbnailAssetId,
 | 
					      albumThumbnailAssetId:
 | 
				
			||||||
 | 
					          albumThumbnailAssetId ?? this.albumThumbnailAssetId,
 | 
				
			||||||
      sharedUsers: sharedUsers ?? this.sharedUsers,
 | 
					      sharedUsers: sharedUsers ?? this.sharedUsers,
 | 
				
			||||||
      assets: assets ?? this.assets,
 | 
					      assets: assets ?? this.assets,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -69,16 +70,19 @@ class SharedAlbum {
 | 
				
			|||||||
      albumName: map['albumName'] ?? '',
 | 
					      albumName: map['albumName'] ?? '',
 | 
				
			||||||
      createdAt: map['createdAt'] ?? '',
 | 
					      createdAt: map['createdAt'] ?? '',
 | 
				
			||||||
      albumThumbnailAssetId: map['albumThumbnailAssetId'],
 | 
					      albumThumbnailAssetId: map['albumThumbnailAssetId'],
 | 
				
			||||||
      sharedUsers: List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
 | 
					      sharedUsers:
 | 
				
			||||||
 | 
					          List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
 | 
				
			||||||
      assets: map['assets'] != null
 | 
					      assets: map['assets'] != null
 | 
				
			||||||
          ? List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x)))
 | 
					          ? List<ImmichAsset>.from(
 | 
				
			||||||
 | 
					              map['assets']?.map((x) => ImmichAsset.fromMap(x)))
 | 
				
			||||||
          : null,
 | 
					          : null,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory SharedAlbum.fromJson(String source) => SharedAlbum.fromMap(json.decode(source));
 | 
					  factory SharedAlbum.fromJson(String source) =>
 | 
				
			||||||
 | 
					      SharedAlbum.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -12,4 +12,5 @@ class AlbumTitleNotifier extends StateNotifier<String> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>((ref) => AlbumTitleNotifier());
 | 
					final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
 | 
				
			||||||
 | 
					    (ref) => AlbumTitleNotifier());
 | 
				
			||||||
 | 
				
			|||||||
@ -18,35 +18,48 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
    state = state.copyWith(isAlbumExist: isAlbumExist);
 | 
					    state = state.copyWith(isAlbumExist: isAlbumExist);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void removeAssetsInMonth(String removedMonth, List<ImmichAsset> assetsInMonth) {
 | 
					  void removeAssetsInMonth(
 | 
				
			||||||
 | 
					      String removedMonth, List<ImmichAsset> assetsInMonth) {
 | 
				
			||||||
    Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
 | 
					    Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
 | 
				
			||||||
    Set<String> currentMonthList = state.selectedMonths;
 | 
					    Set<String> currentMonthList = state.selectedMonths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    currentMonthList.removeWhere((selectedMonth) => selectedMonth == removedMonth);
 | 
					    currentMonthList
 | 
				
			||||||
 | 
					        .removeWhere((selectedMonth) => selectedMonth == removedMonth);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (ImmichAsset asset in assetsInMonth) {
 | 
					    for (ImmichAsset asset in assetsInMonth) {
 | 
				
			||||||
      currentAssetList.removeWhere((e) => e.id == asset.id);
 | 
					      currentAssetList.removeWhere((e) => e.id == asset.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    state = state.copyWith(selectedNewAssetsForAlbum: currentAssetList, selectedMonths: currentMonthList);
 | 
					    state = state.copyWith(
 | 
				
			||||||
 | 
					        selectedNewAssetsForAlbum: currentAssetList,
 | 
				
			||||||
 | 
					        selectedMonths: currentMonthList);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addAdditionalAssets(List<ImmichAsset> assets) {
 | 
					  void addAdditionalAssets(List<ImmichAsset> assets) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedAdditionalAssetsForAlbum: {...state.selectedAdditionalAssetsForAlbum, ...assets},
 | 
					      selectedAdditionalAssetsForAlbum: {
 | 
				
			||||||
 | 
					        ...state.selectedAdditionalAssetsForAlbum,
 | 
				
			||||||
 | 
					        ...assets
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
 | 
					  void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedMonths: {...state.selectedMonths, month},
 | 
					      selectedMonths: {...state.selectedMonths, month},
 | 
				
			||||||
      selectedNewAssetsForAlbum: {...state.selectedNewAssetsForAlbum, ...assetsInMonth},
 | 
					      selectedNewAssetsForAlbum: {
 | 
				
			||||||
 | 
					        ...state.selectedNewAssetsForAlbum,
 | 
				
			||||||
 | 
					        ...assetsInMonth
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void addNewAssets(List<ImmichAsset> assets) {
 | 
					  void addNewAssets(List<ImmichAsset> assets) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedNewAssetsForAlbum: {...state.selectedNewAssetsForAlbum, ...assets},
 | 
					      selectedNewAssetsForAlbum: {
 | 
				
			||||||
 | 
					        ...state.selectedNewAssetsForAlbum,
 | 
				
			||||||
 | 
					        ...assets
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -93,7 +106,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
 | 
					  void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
 | 
				
			||||||
    state = state.copyWith(
 | 
					    state = state.copyWith(
 | 
				
			||||||
      selectedAssetsInAlbumViewer: {...state.selectedAssetsInAlbumViewer, ...assets},
 | 
					      selectedAssetsInAlbumViewer: {
 | 
				
			||||||
 | 
					        ...state.selectedAssetsInAlbumViewer,
 | 
				
			||||||
 | 
					        ...assets
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -108,6 +124,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final assetSelectionProvider = StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
 | 
					final assetSelectionProvider =
 | 
				
			||||||
 | 
					    StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
 | 
				
			||||||
  return AssetSelectionNotifier();
 | 
					  return AssetSelectionNotifier();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,11 @@ class AlbumActionOutlinedButton extends StatelessWidget {
 | 
				
			|||||||
  final String labelText;
 | 
					  final String labelText;
 | 
				
			||||||
  final IconData iconData;
 | 
					  final IconData iconData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const AlbumActionOutlinedButton({Key? key, this.onPressed, required this.labelText, required this.iconData})
 | 
					  const AlbumActionOutlinedButton(
 | 
				
			||||||
 | 
					      {Key? key,
 | 
				
			||||||
 | 
					      this.onPressed,
 | 
				
			||||||
 | 
					      required this.labelText,
 | 
				
			||||||
 | 
					      required this.iconData})
 | 
				
			||||||
      : super(key: key);
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -26,7 +30,8 @@ class AlbumActionOutlinedButton extends StatelessWidget {
 | 
				
			|||||||
        icon: Icon(iconData, size: 15),
 | 
					        icon: Icon(iconData, size: 15),
 | 
				
			||||||
        label: Text(
 | 
					        label: Text(
 | 
				
			||||||
          labelText,
 | 
					          labelText,
 | 
				
			||||||
          style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
 | 
					          style: const TextStyle(
 | 
				
			||||||
 | 
					              fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        onPressed: onPressed,
 | 
					        onPressed: onPressed,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,8 @@ class AlbumTitleTextField extends ConsumerWidget {
 | 
				
			|||||||
        ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
 | 
					        ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      focusNode: albumTitleTextFieldFocusNode,
 | 
					      focusNode: albumTitleTextFieldFocusNode,
 | 
				
			||||||
      style: TextStyle(fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
 | 
					      style: TextStyle(
 | 
				
			||||||
 | 
					          fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
 | 
				
			||||||
      controller: albumTitleController,
 | 
					      controller: albumTitleController,
 | 
				
			||||||
      onTap: () {
 | 
					      onTap: () {
 | 
				
			||||||
        isAlbumTitleTextFieldFocus.value = true;
 | 
					        isAlbumTitleTextFieldFocus.value = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -7,11 +7,14 @@ import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.da
 | 
				
			|||||||
class AlbumViewerEditableTitle extends HookConsumerWidget {
 | 
					class AlbumViewerEditableTitle extends HookConsumerWidget {
 | 
				
			||||||
  final SharedAlbum albumInfo;
 | 
					  final SharedAlbum albumInfo;
 | 
				
			||||||
  final FocusNode titleFocusNode;
 | 
					  final FocusNode titleFocusNode;
 | 
				
			||||||
  const AlbumViewerEditableTitle({Key? key, required this.albumInfo, required this.titleFocusNode}) : super(key: key);
 | 
					  const AlbumViewerEditableTitle(
 | 
				
			||||||
 | 
					      {Key? key, required this.albumInfo, required this.titleFocusNode})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final titleTextEditController = useTextEditingController(text: albumInfo.albumName);
 | 
					    final titleTextEditController =
 | 
				
			||||||
 | 
					        useTextEditingController(text: albumInfo.albumName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void onFocusModeChange() {
 | 
					    void onFocusModeChange() {
 | 
				
			||||||
      if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
 | 
					      if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
 | 
				
			||||||
@ -40,7 +43,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
 | 
				
			|||||||
      onTap: () {
 | 
					      onTap: () {
 | 
				
			||||||
        FocusScope.of(context).requestFocus(titleFocusNode);
 | 
					        FocusScope.of(context).requestFocus(titleFocusNode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ref.watch(albumViewerProvider.notifier).setEditTitleText(albumInfo.albumName);
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(albumViewerProvider.notifier)
 | 
				
			||||||
 | 
					            .setEditTitleText(albumInfo.albumName);
 | 
				
			||||||
        ref.watch(albumViewerProvider.notifier).enableEditAlbum();
 | 
					        ref.watch(albumViewerProvider.notifier).enableEditAlbum();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (titleTextEditController.text == 'Untitled') {
 | 
					        if (titleTextEditController.text == 'Untitled') {
 | 
				
			||||||
 | 
				
			|||||||
@ -22,8 +22,10 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
    var thumbnailRequestUrl =
 | 
					    var thumbnailRequestUrl =
 | 
				
			||||||
        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
 | 
					        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
 | 
				
			||||||
    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
					    var deviceId = ref.watch(authenticationProvider).deviceId;
 | 
				
			||||||
    final selectedAssetsInAlbumViewer = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
 | 
					    final selectedAssetsInAlbumViewer =
 | 
				
			||||||
    final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
 | 
					        ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
 | 
				
			||||||
 | 
					    final isMultiSelectionEnable =
 | 
				
			||||||
 | 
					        ref.watch(assetSelectionProvider).isMultiselectEnable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _viewAsset() {
 | 
					    _viewAsset() {
 | 
				
			||||||
      if (asset.type == 'IMAGE') {
 | 
					      if (asset.type == 'IMAGE') {
 | 
				
			||||||
@ -39,7 +41,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        AutoRouter.of(context).push(
 | 
					        AutoRouter.of(context).push(
 | 
				
			||||||
          VideoViewerRoute(
 | 
					          VideoViewerRoute(
 | 
				
			||||||
              videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
 | 
					              videoUrl:
 | 
				
			||||||
 | 
					                  '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
 | 
				
			||||||
              asset: asset),
 | 
					              asset: asset),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -58,7 +61,9 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    _enableMultiSelection() {
 | 
					    _enableMultiSelection() {
 | 
				
			||||||
      ref.watch(assetSelectionProvider.notifier).enableMultiselection();
 | 
					      ref.watch(assetSelectionProvider.notifier).enableMultiselection();
 | 
				
			||||||
      ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
 | 
					      ref
 | 
				
			||||||
 | 
					          .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					          .addAssetsInAlbumViewer([asset]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _disableMultiSelection() {
 | 
					    _disableMultiSelection() {
 | 
				
			||||||
@ -96,7 +101,9 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
        right: 10,
 | 
					        right: 10,
 | 
				
			||||||
        bottom: 5,
 | 
					        bottom: 5,
 | 
				
			||||||
        child: Icon(
 | 
					        child: Icon(
 | 
				
			||||||
          (deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
 | 
					          (deviceId != asset.deviceId)
 | 
				
			||||||
 | 
					              ? Icons.cloud_done_outlined
 | 
				
			||||||
 | 
					              : Icons.photo_library_rounded,
 | 
				
			||||||
          color: Colors.white,
 | 
					          color: Colors.white,
 | 
				
			||||||
          size: 18,
 | 
					          size: 18,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@ -136,7 +143,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
          imageUrl: thumbnailRequestUrl,
 | 
					          imageUrl: thumbnailRequestUrl,
 | 
				
			||||||
          httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					          httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
				
			||||||
          fadeInDuration: const Duration(milliseconds: 250),
 | 
					          fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
          progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
 | 
					          progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
				
			||||||
 | 
					              Transform.scale(
 | 
				
			||||||
            scale: 0.2,
 | 
					            scale: 0.2,
 | 
				
			||||||
            child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
					            child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@ -152,13 +160,17 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    _handleSelectionGesture() {
 | 
					    _handleSelectionGesture() {
 | 
				
			||||||
      if (selectedAssetsInAlbumViewer.contains(asset)) {
 | 
					      if (selectedAssetsInAlbumViewer.contains(asset)) {
 | 
				
			||||||
        ref.watch(assetSelectionProvider.notifier).removeAssetsInAlbumViewer([asset]);
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					            .removeAssetsInAlbumViewer([asset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (selectedAssetsInAlbumViewer.isEmpty) {
 | 
					        if (selectedAssetsInAlbumViewer.isEmpty) {
 | 
				
			||||||
          _disableMultiSelection();
 | 
					          _disableMultiSelection();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
 | 
					        ref
 | 
				
			||||||
 | 
					            .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					            .addAssetsInAlbumViewer([asset]);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,8 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class AssetGridByMonth extends HookConsumerWidget {
 | 
					class AssetGridByMonth extends HookConsumerWidget {
 | 
				
			||||||
  final List<ImmichAsset> assetGroup;
 | 
					  final List<ImmichAsset> assetGroup;
 | 
				
			||||||
  const AssetGridByMonth({Key? key, required this.assetGroup}) : super(key: key);
 | 
					  const AssetGridByMonth({Key? key, required this.assetGroup})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    return SliverGrid(
 | 
					    return SliverGrid(
 | 
				
			||||||
 | 
				
			|||||||
@ -8,12 +8,15 @@ class MonthGroupTitle extends HookConsumerWidget {
 | 
				
			|||||||
  final String month;
 | 
					  final String month;
 | 
				
			||||||
  final List<ImmichAsset> assetGroup;
 | 
					  final List<ImmichAsset> assetGroup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const MonthGroupTitle({Key? key, required this.month, required this.assetGroup}) : super(key: key);
 | 
					  const MonthGroupTitle(
 | 
				
			||||||
 | 
					      {Key? key, required this.month, required this.assetGroup})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
 | 
					    final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
 | 
				
			||||||
    final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
					    final selectedAssets =
 | 
				
			||||||
 | 
					        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
				
			||||||
    final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
 | 
					    final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _handleTitleIconClick() {
 | 
					    _handleTitleIconClick() {
 | 
				
			||||||
@ -21,10 +24,16 @@ class MonthGroupTitle extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (isAlbumExist) {
 | 
					      if (isAlbumExist) {
 | 
				
			||||||
        if (selectedDateGroup.contains(month)) {
 | 
					        if (selectedDateGroup.contains(month)) {
 | 
				
			||||||
          ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, []);
 | 
					          ref
 | 
				
			||||||
          ref.watch(assetSelectionProvider.notifier).removeSelectedAdditionalAssets(assetGroup);
 | 
					              .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					              .removeAssetsInMonth(month, []);
 | 
				
			||||||
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					              .removeSelectedAdditionalAssets(assetGroup);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          ref.watch(assetSelectionProvider.notifier).addAllAssetsInMonth(month, []);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					              .addAllAssetsInMonth(month, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Deep clone assetGroup
 | 
					          // Deep clone assetGroup
 | 
				
			||||||
          var assetGroupWithNewItems = [...assetGroup];
 | 
					          var assetGroupWithNewItems = [...assetGroup];
 | 
				
			||||||
@ -33,13 +42,19 @@ class MonthGroupTitle extends HookConsumerWidget {
 | 
				
			|||||||
            assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
 | 
					            assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          ref.watch(assetSelectionProvider.notifier).addAdditionalAssets(assetGroupWithNewItems);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					              .addAdditionalAssets(assetGroupWithNewItems);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        if (selectedDateGroup.contains(month)) {
 | 
					        if (selectedDateGroup.contains(month)) {
 | 
				
			||||||
          ref.watch(assetSelectionProvider.notifier).removeAssetsInMonth(month, assetGroup);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					              .removeAssetsInMonth(month, assetGroup);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          ref.watch(assetSelectionProvider.notifier).addAllAssetsInMonth(month, assetGroup);
 | 
					          ref
 | 
				
			||||||
 | 
					              .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					              .addAllAssetsInMonth(month, assetGroup);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -59,7 +74,8 @@ class MonthGroupTitle extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return SliverToBoxAdapter(
 | 
					    return SliverToBoxAdapter(
 | 
				
			||||||
      child: Padding(
 | 
					      child: Padding(
 | 
				
			||||||
        padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
 | 
					        padding: const EdgeInsets.only(
 | 
				
			||||||
 | 
					            top: 29.0, bottom: 29.0, left: 14.0, right: 8.0),
 | 
				
			||||||
        child: Row(
 | 
					        child: Row(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            GestureDetector(
 | 
					            GestureDetector(
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,8 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			|||||||
class SelectionThumbnailImage extends HookConsumerWidget {
 | 
					class SelectionThumbnailImage extends HookConsumerWidget {
 | 
				
			||||||
  final ImmichAsset asset;
 | 
					  final ImmichAsset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const SelectionThumbnailImage({Key? key, required this.asset}) : super(key: key);
 | 
					  const SelectionThumbnailImage({Key? key, required this.asset})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
@ -18,8 +19,10 @@ class SelectionThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
    var box = Hive.box(userInfoBox);
 | 
					    var box = Hive.box(userInfoBox);
 | 
				
			||||||
    var thumbnailRequestUrl =
 | 
					    var thumbnailRequestUrl =
 | 
				
			||||||
        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
 | 
					        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
 | 
				
			||||||
    var selectedAsset = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
					    var selectedAsset =
 | 
				
			||||||
    var newAssetsForAlbum = ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
 | 
					        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
				
			||||||
 | 
					    var newAssetsForAlbum =
 | 
				
			||||||
 | 
					        ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
 | 
				
			||||||
    var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
 | 
					    var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Widget _buildSelectionIcon(ImmichAsset asset) {
 | 
					    Widget _buildSelectionIcon(ImmichAsset asset) {
 | 
				
			||||||
@ -72,15 +75,21 @@ class SelectionThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
          // Operation for existing album
 | 
					          // Operation for existing album
 | 
				
			||||||
          if (!selectedAsset.contains(asset)) {
 | 
					          if (!selectedAsset.contains(asset)) {
 | 
				
			||||||
            if (newAssetsForAlbum.contains(asset)) {
 | 
					            if (newAssetsForAlbum.contains(asset)) {
 | 
				
			||||||
              ref.watch(assetSelectionProvider.notifier).removeSelectedAdditionalAssets([asset]);
 | 
					              ref
 | 
				
			||||||
 | 
					                  .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					                  .removeSelectedAdditionalAssets([asset]);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
              ref.watch(assetSelectionProvider.notifier).addAdditionalAssets([asset]);
 | 
					              ref
 | 
				
			||||||
 | 
					                  .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					                  .addAdditionalAssets([asset]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          // Operation for new album
 | 
					          // Operation for new album
 | 
				
			||||||
          if (selectedAsset.contains(asset)) {
 | 
					          if (selectedAsset.contains(asset)) {
 | 
				
			||||||
            ref.watch(assetSelectionProvider.notifier).removeSelectedNewAssets([asset]);
 | 
					            ref
 | 
				
			||||||
 | 
					                .watch(assetSelectionProvider.notifier)
 | 
				
			||||||
 | 
					                .removeSelectedNewAssets([asset]);
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
 | 
					            ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -97,11 +106,15 @@ class SelectionThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
              memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
 | 
					              memCacheHeight: asset.type == 'IMAGE' ? 150 : 150,
 | 
				
			||||||
              fit: BoxFit.cover,
 | 
					              fit: BoxFit.cover,
 | 
				
			||||||
              imageUrl: thumbnailRequestUrl,
 | 
					              imageUrl: thumbnailRequestUrl,
 | 
				
			||||||
              httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					              httpHeaders: {
 | 
				
			||||||
 | 
					                "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
              fadeInDuration: const Duration(milliseconds: 250),
 | 
					              fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
              progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
 | 
					              progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
				
			||||||
 | 
					                  Transform.scale(
 | 
				
			||||||
                scale: 0.2,
 | 
					                scale: 0.2,
 | 
				
			||||||
                child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
					                child:
 | 
				
			||||||
 | 
					                    CircularProgressIndicator(value: downloadProgress.progress),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              errorWidget: (context, url, error) {
 | 
					              errorWidget: (context, url, error) {
 | 
				
			||||||
                return Icon(
 | 
					                return Icon(
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,8 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
				
			|||||||
class SharedAlbumThumbnailImage extends HookConsumerWidget {
 | 
					class SharedAlbumThumbnailImage extends HookConsumerWidget {
 | 
				
			||||||
  final ImmichAsset asset;
 | 
					  final ImmichAsset asset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const SharedAlbumThumbnailImage({Key? key, required this.asset}) : super(key: key);
 | 
					  const SharedAlbumThumbnailImage({Key? key, required this.asset})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
@ -34,9 +35,11 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
 | 
				
			|||||||
            imageUrl: thumbnailRequestUrl,
 | 
					            imageUrl: thumbnailRequestUrl,
 | 
				
			||||||
            httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					            httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
				
			||||||
            fadeInDuration: const Duration(milliseconds: 250),
 | 
					            fadeInDuration: const Duration(milliseconds: 250),
 | 
				
			||||||
            progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
 | 
					            progressIndicatorBuilder: (context, url, downloadProgress) =>
 | 
				
			||||||
 | 
					                Transform.scale(
 | 
				
			||||||
              scale: 0.2,
 | 
					              scale: 0.2,
 | 
				
			||||||
              child: CircularProgressIndicator(value: downloadProgress.progress),
 | 
					              child:
 | 
				
			||||||
 | 
					                  CircularProgressIndicator(value: downloadProgress.progress),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            errorWidget: (context, url, error) {
 | 
					            errorWidget: (context, url, error) {
 | 
				
			||||||
              return Icon(
 | 
					              return Icon(
 | 
				
			||||||
 | 
				
			|||||||
@ -37,11 +37,13 @@ class SharingSliverAppBar extends StatelessWidget {
 | 
				
			|||||||
                  padding: const EdgeInsets.only(right: 4.0),
 | 
					                  padding: const EdgeInsets.only(right: 4.0),
 | 
				
			||||||
                  child: TextButton.icon(
 | 
					                  child: TextButton.icon(
 | 
				
			||||||
                    style: ButtonStyle(
 | 
					                    style: ButtonStyle(
 | 
				
			||||||
                      backgroundColor: MaterialStateProperty.all(Theme.of(context).primaryColor.withAlpha(20)),
 | 
					                      backgroundColor: MaterialStateProperty.all(
 | 
				
			||||||
 | 
					                          Theme.of(context).primaryColor.withAlpha(20)),
 | 
				
			||||||
                      // foregroundColor: MaterialStateProperty.all(Colors.white),
 | 
					                      // foregroundColor: MaterialStateProperty.all(Colors.white),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    onPressed: () {
 | 
					                    onPressed: () {
 | 
				
			||||||
                      AutoRouter.of(context).push(const CreateSharedAlbumRoute());
 | 
					                      AutoRouter.of(context)
 | 
				
			||||||
 | 
					                          .push(const CreateSharedAlbumRoute());
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    icon: const Icon(
 | 
					                    icon: const Icon(
 | 
				
			||||||
                      Icons.photo_album_outlined,
 | 
					                      Icons.photo_album_outlined,
 | 
				
			||||||
@ -49,7 +51,8 @@ class SharingSliverAppBar extends StatelessWidget {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    label: const Text(
 | 
					                    label: const Text(
 | 
				
			||||||
                      "Create shared album",
 | 
					                      "Create shared album",
 | 
				
			||||||
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
 | 
					                      style:
 | 
				
			||||||
 | 
					                          TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
@ -59,7 +62,8 @@ class SharingSliverAppBar extends StatelessWidget {
 | 
				
			|||||||
                  padding: const EdgeInsets.only(left: 4.0),
 | 
					                  padding: const EdgeInsets.only(left: 4.0),
 | 
				
			||||||
                  child: TextButton.icon(
 | 
					                  child: TextButton.icon(
 | 
				
			||||||
                    style: ButtonStyle(
 | 
					                    style: ButtonStyle(
 | 
				
			||||||
                      backgroundColor: MaterialStateProperty.all(Theme.of(context).primaryColor.withAlpha(20)),
 | 
					                      backgroundColor: MaterialStateProperty.all(
 | 
				
			||||||
 | 
					                          Theme.of(context).primaryColor.withAlpha(20)),
 | 
				
			||||||
                      // foregroundColor: MaterialStateProperty.all(Colors.white),
 | 
					                      // foregroundColor: MaterialStateProperty.all(Colors.white),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    onPressed: null,
 | 
					                    onPressed: null,
 | 
				
			||||||
@ -69,7 +73,8 @@ class SharingSliverAppBar extends StatelessWidget {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    label: const Text(
 | 
					                    label: const Text(
 | 
				
			||||||
                      "Share with partner",
 | 
					                      "Share with partner",
 | 
				
			||||||
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
 | 
					                      style:
 | 
				
			||||||
 | 
					                          TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
				
			|||||||
@ -10,15 +10,18 @@ import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
				
			|||||||
class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
					class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
				
			||||||
  final SharedAlbum albumInfo;
 | 
					  final SharedAlbum albumInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo}) : super(key: key);
 | 
					  const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
 | 
				
			||||||
 | 
					      : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    AsyncValue<List<User>> suggestedShareUsers = ref.watch(suggestedSharedUsersProvider);
 | 
					    AsyncValue<List<User>> suggestedShareUsers =
 | 
				
			||||||
 | 
					        ref.watch(suggestedSharedUsersProvider);
 | 
				
			||||||
    final sharedUsersList = useState<Set<User>>({});
 | 
					    final sharedUsersList = useState<Set<User>>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _addNewUsersHandler() {
 | 
					    _addNewUsersHandler() {
 | 
				
			||||||
      AutoRouter.of(context).pop(sharedUsersList.value.map((e) => e.id).toList());
 | 
					      AutoRouter.of(context)
 | 
				
			||||||
 | 
					          .pop(sharedUsersList.value.map((e) => e.id).toList());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _buildTileIcon(User user) {
 | 
					    _buildTileIcon(User user) {
 | 
				
			||||||
@ -32,7 +35,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return CircleAvatar(
 | 
					        return CircleAvatar(
 | 
				
			||||||
          backgroundImage: const AssetImage('assets/immich-logo-no-outline.png'),
 | 
					          backgroundImage:
 | 
				
			||||||
 | 
					              const AssetImage('assets/immich-logo-no-outline.png'),
 | 
				
			||||||
          backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
 | 
					          backgroundColor: Theme.of(context).primaryColor.withAlpha(50),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -49,7 +53,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
				
			|||||||
              backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
 | 
					              backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
 | 
				
			||||||
              label: Text(
 | 
					              label: Text(
 | 
				
			||||||
                user.email,
 | 
					                user.email,
 | 
				
			||||||
                style: const TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.bold),
 | 
					                style: const TextStyle(
 | 
				
			||||||
 | 
					                    fontSize: 12,
 | 
				
			||||||
 | 
					                    color: Colors.black87,
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@ -65,7 +72,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
				
			|||||||
            padding: EdgeInsets.all(16.0),
 | 
					            padding: EdgeInsets.all(16.0),
 | 
				
			||||||
            child: Text(
 | 
					            child: Text(
 | 
				
			||||||
              'Suggestions',
 | 
					              'Suggestions',
 | 
				
			||||||
              style: TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold),
 | 
					              style: TextStyle(
 | 
				
			||||||
 | 
					                  fontSize: 14,
 | 
				
			||||||
 | 
					                  color: Colors.grey,
 | 
				
			||||||
 | 
					                  fontWeight: FontWeight.bold),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          ListView.builder(
 | 
					          ListView.builder(
 | 
				
			||||||
@ -75,14 +85,20 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
				
			|||||||
                leading: _buildTileIcon(users[index]),
 | 
					                leading: _buildTileIcon(users[index]),
 | 
				
			||||||
                title: Text(
 | 
					                title: Text(
 | 
				
			||||||
                  users[index].email,
 | 
					                  users[index].email,
 | 
				
			||||||
                  style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
					                  style: const TextStyle(
 | 
				
			||||||
 | 
					                      fontSize: 14, fontWeight: FontWeight.bold),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  if (sharedUsersList.value.contains(users[index])) {
 | 
					                  if (sharedUsersList.value.contains(users[index])) {
 | 
				
			||||||
                    sharedUsersList.value =
 | 
					                    sharedUsersList.value = sharedUsersList.value
 | 
				
			||||||
                        sharedUsersList.value.where((selectedUser) => selectedUser.id != users[index].id).toSet();
 | 
					                        .where((selectedUser) =>
 | 
				
			||||||
 | 
					                            selectedUser.id != users[index].id)
 | 
				
			||||||
 | 
					                        .toSet();
 | 
				
			||||||
                  } else {
 | 
					                  } else {
 | 
				
			||||||
                    sharedUsersList.value = {...sharedUsersList.value, users[index]};
 | 
					                    sharedUsersList.value = {
 | 
				
			||||||
 | 
					                      ...sharedUsersList.value,
 | 
				
			||||||
 | 
					                      users[index]
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
@ -109,7 +125,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
        actions: [
 | 
					        actions: [
 | 
				
			||||||
          TextButton(
 | 
					          TextButton(
 | 
				
			||||||
            onPressed: sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
 | 
					            onPressed:
 | 
				
			||||||
 | 
					                sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
 | 
				
			||||||
            child: const Text(
 | 
					            child: const Text(
 | 
				
			||||||
              "Add",
 | 
					              "Add",
 | 
				
			||||||
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
					              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
 | 
				
			||||||
@ -120,7 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 | 
				
			|||||||
      body: suggestedShareUsers.when(
 | 
					      body: suggestedShareUsers.when(
 | 
				
			||||||
        data: (users) {
 | 
					        data: (users) {
 | 
				
			||||||
          for (var sharedUsers in albumInfo.sharedUsers) {
 | 
					          for (var sharedUsers in albumInfo.sharedUsers) {
 | 
				
			||||||
            users.removeWhere((u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId);
 | 
					            users.removeWhere(
 | 
				
			||||||
 | 
					                (u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return _buildUserList(users);
 | 
					          return _buildUserList(users);
 | 
				
			||||||
 | 
				
			|||||||
@ -29,12 +29,14 @@ class SharingPage extends HookConsumerWidget {
 | 
				
			|||||||
      return SliverList(
 | 
					      return SliverList(
 | 
				
			||||||
        delegate: SliverChildBuilderDelegate(
 | 
					        delegate: SliverChildBuilderDelegate(
 | 
				
			||||||
          (BuildContext context, int index) {
 | 
					          (BuildContext context, int index) {
 | 
				
			||||||
            String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId != null
 | 
					            String thumbnailUrl = sharedAlbums[index].albumThumbnailAssetId !=
 | 
				
			||||||
 | 
					                    null
 | 
				
			||||||
                ? "$thumbnailRequestUrl/${sharedAlbums[index].albumThumbnailAssetId}"
 | 
					                ? "$thumbnailRequestUrl/${sharedAlbums[index].albumThumbnailAssetId}"
 | 
				
			||||||
                : "https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60";
 | 
					                : "https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return ListTile(
 | 
					            return ListTile(
 | 
				
			||||||
              contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
 | 
					              contentPadding:
 | 
				
			||||||
 | 
					                  const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
 | 
				
			||||||
              leading: ClipRRect(
 | 
					              leading: ClipRRect(
 | 
				
			||||||
                borderRadius: BorderRadius.circular(8),
 | 
					                borderRadius: BorderRadius.circular(8),
 | 
				
			||||||
                child: FadeInImage(
 | 
					                child: FadeInImage(
 | 
				
			||||||
@ -44,7 +46,9 @@ class SharingPage extends HookConsumerWidget {
 | 
				
			|||||||
                  placeholder: MemoryImage(kTransparentImage),
 | 
					                  placeholder: MemoryImage(kTransparentImage),
 | 
				
			||||||
                  image: NetworkImage(
 | 
					                  image: NetworkImage(
 | 
				
			||||||
                    thumbnailUrl,
 | 
					                    thumbnailUrl,
 | 
				
			||||||
                    headers: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
 | 
					                    headers: {
 | 
				
			||||||
 | 
					                      "Authorization": "Bearer ${box.get(accessTokenKey)}"
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  fadeInDuration: const Duration(milliseconds: 200),
 | 
					                  fadeInDuration: const Duration(milliseconds: 200),
 | 
				
			||||||
                  fadeOutDuration: const Duration(milliseconds: 200),
 | 
					                  fadeOutDuration: const Duration(milliseconds: 200),
 | 
				
			||||||
@ -54,10 +58,14 @@ class SharingPage extends HookConsumerWidget {
 | 
				
			|||||||
                sharedAlbums[index].albumName,
 | 
					                sharedAlbums[index].albumName,
 | 
				
			||||||
                maxLines: 1,
 | 
					                maxLines: 1,
 | 
				
			||||||
                overflow: TextOverflow.ellipsis,
 | 
					                overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey.shade800),
 | 
					                style: TextStyle(
 | 
				
			||||||
 | 
					                    fontSize: 16,
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                    color: Colors.grey.shade800),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                AutoRouter.of(context).push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
 | 
					                AutoRouter.of(context)
 | 
				
			||||||
 | 
					                    .push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -134,7 +142,9 @@ class SharingPage extends HookConsumerWidget {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          sharedAlbums.isNotEmpty ? _buildAlbumList() : _buildEmptyListIndication()
 | 
					          sharedAlbums.isNotEmpty
 | 
				
			||||||
 | 
					              ? _buildAlbumList()
 | 
				
			||||||
 | 
					              : _buildEmptyListIndication()
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
				
			|||||||
@ -149,7 +149,8 @@ class ImmichExif {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ImmichExif.fromJson(String source) => ImmichExif.fromMap(json.decode(source));
 | 
					  factory ImmichExif.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ImmichExif.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -95,7 +95,8 @@ class ImmichAsset extends Equatable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ImmichAsset.fromJson(String source) => ImmichAsset.fromMap(json.decode(source));
 | 
					  factory ImmichAsset.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ImmichAsset.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -85,13 +85,15 @@ class ImmichAssetWithExif {
 | 
				
			|||||||
      originalPath: map['originalPath'] ?? '',
 | 
					      originalPath: map['originalPath'] ?? '',
 | 
				
			||||||
      isFavorite: map['isFavorite'] ?? false,
 | 
					      isFavorite: map['isFavorite'] ?? false,
 | 
				
			||||||
      duration: map['duration'],
 | 
					      duration: map['duration'],
 | 
				
			||||||
      exifInfo: map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null,
 | 
					      exifInfo:
 | 
				
			||||||
 | 
					          map['exifInfo'] != null ? ImmichExif.fromMap(map['exifInfo']) : null,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ImmichAssetWithExif.fromJson(String source) => ImmichAssetWithExif.fromMap(json.decode(source));
 | 
					  factory ImmichAssetWithExif.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ImmichAssetWithExif.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -34,16 +34,20 @@ class MapboxInfo {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory MapboxInfo.fromJson(String source) => MapboxInfo.fromMap(json.decode(source));
 | 
					  factory MapboxInfo.fromJson(String source) =>
 | 
				
			||||||
 | 
					      MapboxInfo.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'MapboxInfo(isEnable: $isEnable, mapboxSecret: $mapboxSecret)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is MapboxInfo && other.isEnable == isEnable && other.mapboxSecret == mapboxSecret;
 | 
					    return other is MapboxInfo &&
 | 
				
			||||||
 | 
					        other.isEnable == isEnable &&
 | 
				
			||||||
 | 
					        other.mapboxSecret == mapboxSecret;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -64,7 +64,8 @@ class ServerInfo {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ServerInfo.fromJson(String source) => ServerInfo.fromMap(json.decode(source));
 | 
					  factory ServerInfo.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ServerInfo.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,8 @@ class ServerInfoState {
 | 
				
			|||||||
      mapboxInfo: mapboxInfo ?? this.mapboxInfo,
 | 
					      mapboxInfo: mapboxInfo ?? this.mapboxInfo,
 | 
				
			||||||
      serverVersion: serverVersion ?? this.serverVersion,
 | 
					      serverVersion: serverVersion ?? this.serverVersion,
 | 
				
			||||||
      isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
 | 
					      isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
 | 
				
			||||||
      versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
 | 
					      versionMismatchErrorMessage:
 | 
				
			||||||
 | 
					          versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -50,7 +51,8 @@ class ServerInfoState {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
 | 
					  factory ServerInfoState.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ServerInfoState.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -47,7 +47,8 @@ class ServerVersion {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory ServerVersion.fromJson(String source) => ServerVersion.fromMap(json.decode(source));
 | 
					  factory ServerVersion.fromJson(String source) =>
 | 
				
			||||||
 | 
					      ServerVersion.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
 | 
				
			|||||||
@ -36,16 +36,20 @@ class UploadProfileImageResponse {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  String toJson() => json.encode(toMap());
 | 
					  String toJson() => json.encode(toMap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory UploadProfileImageResponse.fromJson(String source) => UploadProfileImageResponse.fromMap(json.decode(source));
 | 
					  factory UploadProfileImageResponse.fromJson(String source) =>
 | 
				
			||||||
 | 
					      UploadProfileImageResponse.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is UploadProfileImageResponse && other.userId == userId && other.profileImagePath == profileImagePath;
 | 
					    return other is UploadProfileImageResponse &&
 | 
				
			||||||
 | 
					        other.userId == userId &&
 | 
				
			||||||
 | 
					        other.profileImagePath == profileImagePath;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
				
			|||||||
@ -56,7 +56,8 @@ class User {
 | 
				
			|||||||
  factory User.fromJson(String source) => User.fromMap(json.decode(source));
 | 
					  factory User.fromJson(String source) => User.fromMap(json.decode(source));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UserInfo(id: $id, email: $email, createdAt: $createdAt)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'UserInfo(id: $id, email: $email, createdAt: $createdAt)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
				
			|||||||
@ -29,13 +29,16 @@ class WebscoketState {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'WebscoketState(socket: $socket, isConnected: $isConnected)';
 | 
					  String toString() =>
 | 
				
			||||||
 | 
					      'WebscoketState(socket: $socket, isConnected: $isConnected)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    if (identical(this, other)) return true;
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return other is WebscoketState && other.socket == socket && other.isConnected == isConnected;
 | 
					    return other is WebscoketState &&
 | 
				
			||||||
 | 
					        other.socket == socket &&
 | 
				
			||||||
 | 
					        other.isConnected == isConnected;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -43,7 +46,8 @@ class WebscoketState {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WebsocketNotifier extends StateNotifier<WebscoketState> {
 | 
					class WebsocketNotifier extends StateNotifier<WebscoketState> {
 | 
				
			||||||
  WebsocketNotifier(this.ref) : super(WebscoketState(socket: null, isConnected: false)) {
 | 
					  WebsocketNotifier(this.ref)
 | 
				
			||||||
 | 
					      : super(WebscoketState(socket: null, isConnected: false)) {
 | 
				
			||||||
    debugPrint("Init websocket instance");
 | 
					    debugPrint("Init websocket instance");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -122,6 +126,7 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
 | 
					final websocketProvider =
 | 
				
			||||||
 | 
					    StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
 | 
				
			||||||
  return WebsocketNotifier(ref);
 | 
					  return WebsocketNotifier(ref);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,8 @@ import 'dart:math';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImmichSliverPersistentAppBarDelegate extends SliverPersistentHeaderDelegate {
 | 
					class ImmichSliverPersistentAppBarDelegate
 | 
				
			||||||
 | 
					    extends SliverPersistentHeaderDelegate {
 | 
				
			||||||
  final double minHeight;
 | 
					  final double minHeight;
 | 
				
			||||||
  final double maxHeight;
 | 
					  final double maxHeight;
 | 
				
			||||||
  final Widget child;
 | 
					  final Widget child;
 | 
				
			||||||
@ -20,12 +21,15 @@ class ImmichSliverPersistentAppBarDelegate extends SliverPersistentHeaderDelegat
 | 
				
			|||||||
  double get maxExtent => max(maxHeight, minHeight);
 | 
					  double get maxExtent => max(maxHeight, minHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
 | 
					  Widget build(
 | 
				
			||||||
 | 
					      BuildContext context, double shrinkOffset, bool overlapsContent) {
 | 
				
			||||||
    return SizedBox.expand(child: child);
 | 
					    return SizedBox.expand(child: child);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool shouldRebuild(ImmichSliverPersistentAppBarDelegate oldDelegate) {
 | 
					  bool shouldRebuild(ImmichSliverPersistentAppBarDelegate oldDelegate) {
 | 
				
			||||||
    return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child;
 | 
					    return maxHeight != oldDelegate.maxHeight ||
 | 
				
			||||||
 | 
					        minHeight != oldDelegate.minHeight ||
 | 
				
			||||||
 | 
					        child != oldDelegate.child;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -15,12 +15,14 @@ class SplashScreenPage extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    HiveSavedLoginInfo? loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
 | 
					    HiveSavedLoginInfo? loginInfo =
 | 
				
			||||||
 | 
					        Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void performLoggingIn() async {
 | 
					    void performLoggingIn() async {
 | 
				
			||||||
      var isAuthenticated = await ref
 | 
					      var isAuthenticated = await ref
 | 
				
			||||||
          .read(authenticationProvider.notifier)
 | 
					          .read(authenticationProvider.notifier)
 | 
				
			||||||
          .login(loginInfo!.email, loginInfo.password, loginInfo.serverUrl, true);
 | 
					          .login(
 | 
				
			||||||
 | 
					              loginInfo!.email, loginInfo.password, loginInfo.serverUrl, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isAuthenticated) {
 | 
					      if (isAuthenticated) {
 | 
				
			||||||
        // Resume backup (if enable) then navigate
 | 
					        // Resume backup (if enable) then navigate
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,8 @@ class TabControllerPage extends ConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
					    var isMultiSelectEnable =
 | 
				
			||||||
 | 
					        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AutoTabsRouter(
 | 
					    return AutoTabsRouter(
 | 
				
			||||||
      routes: [
 | 
					      routes: [
 | 
				
			||||||
@ -32,16 +33,21 @@ class TabControllerPage extends ConsumerWidget {
 | 
				
			|||||||
            bottomNavigationBar: isMultiSelectEnable
 | 
					            bottomNavigationBar: isMultiSelectEnable
 | 
				
			||||||
                ? null
 | 
					                ? null
 | 
				
			||||||
                : BottomNavigationBar(
 | 
					                : BottomNavigationBar(
 | 
				
			||||||
                    selectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
 | 
					                    selectedLabelStyle: const TextStyle(
 | 
				
			||||||
                    unselectedLabelStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
 | 
					                        fontSize: 15, fontWeight: FontWeight.w600),
 | 
				
			||||||
 | 
					                    unselectedLabelStyle: const TextStyle(
 | 
				
			||||||
 | 
					                        fontSize: 15, fontWeight: FontWeight.w600),
 | 
				
			||||||
                    currentIndex: tabsRouter.activeIndex,
 | 
					                    currentIndex: tabsRouter.activeIndex,
 | 
				
			||||||
                    onTap: (index) {
 | 
					                    onTap: (index) {
 | 
				
			||||||
                      tabsRouter.setActiveIndex(index);
 | 
					                      tabsRouter.setActiveIndex(index);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    items: const [
 | 
					                    items: const [
 | 
				
			||||||
                      BottomNavigationBarItem(label: 'Photos', icon: Icon(Icons.photo)),
 | 
					                      BottomNavigationBarItem(
 | 
				
			||||||
                      BottomNavigationBarItem(label: 'Search', icon: Icon(Icons.search)),
 | 
					                          label: 'Photos', icon: Icon(Icons.photo)),
 | 
				
			||||||
                      BottomNavigationBarItem(label: 'Sharing', icon: Icon(Icons.group_outlined)),
 | 
					                      BottomNavigationBarItem(
 | 
				
			||||||
 | 
					                          label: 'Search', icon: Icon(Icons.search)),
 | 
				
			||||||
 | 
					                      BottomNavigationBarItem(
 | 
				
			||||||
 | 
					                          label: 'Sharing', icon: Icon(Icons.group_outlined)),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user