Browse Source

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
Alex 3 years ago
parent
commit
40a8115101
63 changed files with 677 additions and 300 deletions
  1. 8 4
      mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart
  2. 5 2
      mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart
  3. 5 3
      mobile/lib/modules/asset_viewer/services/image_viewer.service.dart
  4. 5 2
      mobile/lib/modules/backup/models/available_album.model.dart
  5. 8 7
      mobile/lib/modules/backup/models/backup_state.model.dart
  6. 4 2
      mobile/lib/modules/backup/models/hive_backup_albums.model.dart
  7. 23 20
      mobile/lib/modules/backup/providers/backup.provider.dart
  8. 41 17
      mobile/lib/modules/backup/ui/album_info_card.dart
  9. 6 1
      mobile/lib/modules/backup/ui/backup_info_card.dart
  10. 9 4
      mobile/lib/modules/backup/views/album_preview_page.dart
  11. 39 13
      mobile/lib/modules/backup/views/backup_album_selection_page.dart
  12. 13 13
      mobile/lib/modules/backup/views/backup_controller_page.dart
  13. 5 2
      mobile/lib/modules/home/models/delete_asset_response.model.dart
  14. 13 6
      mobile/lib/modules/home/models/get_all_asset_response.model.dart
  15. 8 3
      mobile/lib/modules/home/models/home_page_state.model.dart
  16. 8 4
      mobile/lib/modules/home/providers/home_page_state.provider.dart
  17. 7 2
      mobile/lib/modules/home/ui/control_bottom_app_bar.dart
  18. 35 13
      mobile/lib/modules/home/ui/daily_title_text.dart
  19. 5 2
      mobile/lib/modules/home/ui/delete_diaglog.dart
  20. 4 1
      mobile/lib/modules/home/ui/image_grid.dart
  21. 28 11
      mobile/lib/modules/home/ui/thumbnail_image.dart
  22. 2 1
      mobile/lib/modules/login/models/authentication_state.model.dart
  23. 5 1
      mobile/lib/modules/login/models/hive_saved_login_info.model.dart
  24. 2 1
      mobile/lib/modules/login/models/login_response.model.dart
  25. 33 14
      mobile/lib/modules/login/ui/login_form.dart
  26. 7 2
      mobile/lib/modules/search/models/curated_location.model.dart
  27. 7 2
      mobile/lib/modules/search/models/curated_object.model.dart
  28. 6 3
      mobile/lib/modules/search/models/search_page_state.model.dart
  29. 8 3
      mobile/lib/modules/search/models/search_result_page_state.model.dart
  30. 3 1
      mobile/lib/modules/search/ui/search_bar.dart
  31. 7 3
      mobile/lib/modules/search/ui/search_suggestion_list.dart
  32. 8 2
      mobile/lib/modules/search/ui/thumbnail_with_info.dart
  33. 7 3
      mobile/lib/modules/sharing/models/album_viewer_page_state.model.dart
  34. 18 8
      mobile/lib/modules/sharing/models/asset_selection_page_result.model.dart
  35. 32 16
      mobile/lib/modules/sharing/models/asset_selection_state.model.dart
  36. 8 4
      mobile/lib/modules/sharing/models/shared_album.model.dart
  37. 2 1
      mobile/lib/modules/sharing/providers/album_title.provider.dart
  38. 25 8
      mobile/lib/modules/sharing/providers/asset_selection.provider.dart
  39. 7 2
      mobile/lib/modules/sharing/ui/album_action_outlined_button.dart
  40. 2 1
      mobile/lib/modules/sharing/ui/album_title_text_field.dart
  41. 8 3
      mobile/lib/modules/sharing/ui/album_viewer_editable_title.dart
  42. 20 8
      mobile/lib/modules/sharing/ui/album_viewer_thumbnail.dart
  43. 2 1
      mobile/lib/modules/sharing/ui/asset_grid_by_month.dart
  44. 25 9
      mobile/lib/modules/sharing/ui/month_group_title.dart
  45. 22 9
      mobile/lib/modules/sharing/ui/selection_thumbnail_image.dart
  46. 6 3
      mobile/lib/modules/sharing/ui/shared_album_thumbnail_image.dart
  47. 10 5
      mobile/lib/modules/sharing/ui/sharing_sliver_appbar.dart
  48. 30 12
      mobile/lib/modules/sharing/views/select_additional_user_for_sharing_page.dart
  49. 16 6
      mobile/lib/modules/sharing/views/sharing_page.dart
  50. 2 1
      mobile/lib/shared/models/exif.model.dart
  51. 2 1
      mobile/lib/shared/models/immich_asset.model.dart
  52. 4 2
      mobile/lib/shared/models/immich_asset_with_exif.model.dart
  53. 7 3
      mobile/lib/shared/models/mapbox_info.model.dart
  54. 2 1
      mobile/lib/shared/models/server_info.model.dart
  55. 4 2
      mobile/lib/shared/models/server_info_state.model.dart
  56. 2 1
      mobile/lib/shared/models/server_version.model.dart
  57. 7 3
      mobile/lib/shared/models/upload_profile_image_repsonse.model.dart
  58. 7 6
      mobile/lib/shared/models/user.model.dart
  59. 1 1
      mobile/lib/shared/providers/asset.provider.dart
  60. 9 4
      mobile/lib/shared/providers/websocket.provider.dart
  61. 7 3
      mobile/lib/shared/ui/immich_sliver_persistent_app_bar_delegate.dart
  62. 4 2
      mobile/lib/shared/views/splash_screen.dart
  63. 12 6
      mobile/lib/shared/views/tab_controller_page.dart

+ 8 - 4
mobile/lib/modules/asset_viewer/models/image_viewer_page_state.model.dart

@@ -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

+ 5 - 2
mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart

@@ -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()));

+ 5 - 3
mobile/lib/modules/asset_viewer/services/image_viewer.service.dart

@@ -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;

+ 5 - 2
mobile/lib/modules/backup/models/available_album.model.dart

@@ -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

+ 8 - 7
mobile/lib/modules/backup/models/backup_state.model.dart

@@ -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,

+ 4 - 2
mobile/lib/modules/backup/models/hive_backup_albums.model.dart

@@ -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) {

+ 23 - 20
mobile/lib/modules/backup/providers/backup.provider.dart

@@ -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

+ 41 - 17
mobile/lib/modules/backup/ui/album_info_card.dart

@@ -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,

+ 6 - 1
mobile/lib/modules/backup/ui/backup_info_card.dart

@@ -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) {

+ 9 - 4
mobile/lib/modules/backup/views/album_preview_page.dart

@@ -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,

+ 39 - 13
mobile/lib/modules/backup/views/backup_album_selection_page.dart

@@ -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]),
                             ),
                           ],
                         ),

+ 13 - 13
mobile/lib/modules/backup/views/backup_controller_page.dart

@@ -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)
-                            .setAutoBackup(true);
+                    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}",
             ),

+ 5 - 2
mobile/lib/modules/home/models/delete_asset_response.model.dart

@@ -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

+ 13 - 6
mobile/lib/modules/home/models/get_all_asset_response.model.dart

@@ -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) {

+ 8 - 3
mobile/lib/modules/home/models/home_page_state.model.dart

@@ -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;
 }

+ 8 - 4
mobile/lib/modules/home/providers/home_page_state.provider.dart

@@ -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()));

+ 7 - 2
mobile/lib/modules/home/ui/control_bottom_app_bar.dart

@@ -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;

+ 35 - 13
mobile/lib/modules/home/ui/daily_title_text.dart

@@ -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(

+ 5 - 2
mobile/lib/modules/home/ui/delete_diaglog.dart

@@ -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();

+ 4 - 1
mobile/lib/modules/home/ui/image_grid.dart

@@ -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,

+ 28 - 11
mobile/lib/modules/home/ui/thumbnail_image.dart

@@ -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,
               ),

+ 2 - 1
mobile/lib/modules/login/models/authentication_state.model.dart

@@ -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) {

+ 5 - 1
mobile/lib/modules/login/models/hive_saved_login_info.model.dart

@@ -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});
 }

+ 2 - 1
mobile/lib/modules/login/models/login_response.model.dart

@@ -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() {

+ 33 - 14
mobile/lib/modules/login/ui/login_form.dart

@@ -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,
             );
           }

+ 7 - 2
mobile/lib/modules/search/models/curated_location.model.dart

@@ -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;
   }
 }

+ 7 - 2
mobile/lib/modules/search/models/curated_object.model.dart

@@ -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;
   }
 }

+ 6 - 3
mobile/lib/modules/search/models/search_page_state.model.dart

@@ -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() {

+ 8 - 3
mobile/lib/modules/search/models/search_result_page_state.model.dart

@@ -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;
   }
 }

+ 3 - 1
mobile/lib/modules/search/ui/search_bar.dart

@@ -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;

+ 7 - 3
mobile/lib/modules/search/ui/search_suggestion_list.dart

@@ -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(

+ 8 - 2
mobile/lib/modules/search/ui/thumbnail_with_info.dart

@@ -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)}"
+                    },
                   ),
                 ),
               ),

+ 7 - 3
mobile/lib/modules/sharing/models/album_viewer_page_state.model.dart

@@ -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

+ 18 - 8
mobile/lib/modules/sharing/models/asset_selection_page_result.model.dart

@@ -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 - 16
mobile/lib/modules/sharing/models/asset_selection_state.model.dart

@@ -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;
   }

+ 8 - 4
mobile/lib/modules/sharing/models/shared_album.model.dart

@@ -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() {

+ 2 - 1
mobile/lib/modules/sharing/providers/album_title.provider.dart

@@ -12,4 +12,5 @@ class AlbumTitleNotifier extends StateNotifier<String> {
   }
 }
 
-final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>((ref) => AlbumTitleNotifier());
+final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
+    (ref) => AlbumTitleNotifier());

+ 25 - 8
mobile/lib/modules/sharing/providers/asset_selection.provider.dart

@@ -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();
 });

+ 7 - 2
mobile/lib/modules/sharing/ui/album_action_outlined_button.dart

@@ -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,
       ),

+ 2 - 1
mobile/lib/modules/sharing/ui/album_title_text_field.dart

@@ -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;

+ 8 - 3
mobile/lib/modules/sharing/ui/album_viewer_editable_title.dart

@@ -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') {

+ 20 - 8
mobile/lib/modules/sharing/ui/album_viewer_thumbnail.dart

@@ -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]);
       }
     }
 

+ 2 - 1
mobile/lib/modules/sharing/ui/asset_grid_by_month.dart

@@ -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(

+ 25 - 9
mobile/lib/modules/sharing/ui/month_group_title.dart

@@ -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(

+ 22 - 9
mobile/lib/modules/sharing/ui/selection_thumbnail_image.dart

@@ -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(

+ 6 - 3
mobile/lib/modules/sharing/ui/shared_album_thumbnail_image.dart

@@ -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(

+ 10 - 5
mobile/lib/modules/sharing/ui/sharing_sliver_appbar.dart

@@ -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),
                     ),
                   ),
                 ),

+ 30 - 12
mobile/lib/modules/sharing/views/select_additional_user_for_sharing_page.dart

@@ -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);

+ 16 - 6
mobile/lib/modules/sharing/views/sharing_page.dart

@@ -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()
         ],
       ),
     );

+ 2 - 1
mobile/lib/shared/models/exif.model.dart

@@ -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() {

+ 2 - 1
mobile/lib/shared/models/immich_asset.model.dart

@@ -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() {

+ 4 - 2
mobile/lib/shared/models/immich_asset_with_exif.model.dart

@@ -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() {

+ 7 - 3
mobile/lib/shared/models/mapbox_info.model.dart

@@ -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

+ 2 - 1
mobile/lib/shared/models/server_info.model.dart

@@ -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() {

+ 4 - 2
mobile/lib/shared/models/server_info_state.model.dart

@@ -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() {

+ 2 - 1
mobile/lib/shared/models/server_version.model.dart

@@ -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() {

+ 7 - 3
mobile/lib/shared/models/upload_profile_image_repsonse.model.dart

@@ -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

+ 7 - 6
mobile/lib/shared/models/user.model.dart

@@ -56,18 +56,19 @@ 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) {
     if (identical(this, other)) return true;
 
     return other is User &&
-      other.id == id &&
-      other.email == email &&
-      other.createdAt == createdAt &&
-      other.firstName == firstName &&
-      other.lastName == lastName;
+        other.id == id &&
+        other.email == email &&
+        other.createdAt == createdAt &&
+        other.firstName == firstName &&
+        other.lastName == lastName;
   }
 
   @override

+ 1 - 1
mobile/lib/shared/providers/asset.provider.dart

@@ -45,7 +45,7 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
         }
       }
     }
-    
+
     try {
       await PhotoManager.editor.deleteWithIds(deleteIdList);
     } catch (e) {

+ 9 - 4
mobile/lib/shared/providers/websocket.provider.dart

@@ -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);
 });

+ 7 - 3
mobile/lib/shared/ui/immich_sliver_persistent_app_bar_delegate.dart

@@ -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;
   }
 }

+ 4 - 2
mobile/lib/shared/views/splash_screen.dart

@@ -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

+ 12 - 6
mobile/lib/shared/views/tab_controller_page.dart

@@ -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)),
                     ],
                   ),
           ),