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