Просмотр исходного кода

Merge pull request #410 from ente-io/rewrite_device_sync

Rewrite Local Import
Neeraj Gupta 2 лет назад
Родитель
Сommit
123bb011b9
100 измененных файлов с 2996 добавлено и 1896 удалено
  1. 6 14
      ios/Runner.xcodeproj/project.pbxproj
  2. 2 0
      ios/Runner/Info.plist
  3. 3 5
      lib/core/cache/image_cache.dart
  4. 5 7
      lib/core/cache/lru_map.dart
  5. 8 10
      lib/core/cache/thumbnail_cache.dart
  6. 0 2
      lib/core/cache/video_cache_manager.dart
  7. 107 101
      lib/core/configuration.dart
  8. 22 27
      lib/core/constants.dart
  9. 1 1
      lib/core/error-reporting/super_logging.dart
  10. 0 2
      lib/core/errors.dart
  11. 1 3
      lib/core/network.dart
  12. 22 18
      lib/data/holidays.dart
  13. 7 4
      lib/data/months.dart
  14. 0 2
      lib/data/years.dart
  15. 9 24
      lib/db/collections_db.dart
  16. 376 0
      lib/db/device_files_db.dart
  17. 7 9
      lib/db/file_updation_db.dart
  18. 272 241
      lib/db/files_db.dart
  19. 38 1
      lib/db/ignored_files_db.dart
  20. 4 5
      lib/db/public_keys_db.dart
  21. 3 2
      lib/db/trash_db.dart
  22. 4 5
      lib/db/upload_locks_db.dart
  23. 70 97
      lib/ente_theme_data.dart
  24. 8 0
      lib/events/local_import_progress.dart
  25. 5 12
      lib/events/sync_status_update_event.dart
  26. 0 7
      lib/events/tab_changed_event.dart
  27. 2 2
      lib/main.dart
  28. 0 2
      lib/models/backup_status.dart
  29. 17 137
      lib/models/billing_plan.dart
  30. 43 182
      lib/models/collection.dart
  31. 4 6
      lib/models/collection_file_item.dart
  32. 1 11
      lib/models/collection_items.dart
  33. 2 6
      lib/models/delete_account.dart
  34. 3 4
      lib/models/derived_key_result.dart
  35. 24 0
      lib/models/device_collection.dart
  36. 0 15
      lib/models/device_folder.dart
  37. 2 2
      lib/models/duplicate_files.dart
  38. 10 7
      lib/models/encryption_result.dart
  39. 0 6
      lib/models/ente_file.dart
  40. 60 59
      lib/models/file.dart
  41. 0 2
      lib/models/file_load_result.dart
  42. 13 2
      lib/models/file_type.dart
  43. 0 1
      lib/models/filters/gallery_items_filter.dart
  44. 0 3
      lib/models/gallery_type.dart
  45. 7 14
      lib/models/ignored_file.dart
  46. 12 14
      lib/models/key_attributes.dart
  47. 0 2
      lib/models/key_gen_result.dart
  48. 2 4
      lib/models/location.dart
  49. 16 37
      lib/models/magic_metadata.dart
  50. 0 2
      lib/models/memory.dart
  51. 0 2
      lib/models/private_key_attributes.dart
  52. 0 2
      lib/models/public_key.dart
  53. 23 3
      lib/models/search/album_search_result.dart
  54. 22 3
      lib/models/search/file_search_result.dart
  55. 30 0
      lib/models/search/generic_search_result.dart
  56. 0 17
      lib/models/search/holiday_search_result.dart
  57. 13 22
      lib/models/search/location_api_response.dart
  58. 0 11
      lib/models/search/location_search_result.dart
  59. 0 16
      lib/models/search/month_search_result.dart
  60. 26 0
      lib/models/search/search_result.dart
  61. 0 3
      lib/models/search/search_results.dart
  62. 0 11
      lib/models/search/year_search_result.dart
  63. 4 8
      lib/models/selected_files.dart
  64. 3 100
      lib/models/sessions.dart
  65. 5 24
      lib/models/set_keys_request.dart
  66. 0 18
      lib/models/set_recovery_key_request.dart
  67. 15 109
      lib/models/subscription.dart
  68. 3 5
      lib/models/trash_file.dart
  69. 1 5
      lib/models/trash_item_request.dart
  70. 30 0
      lib/models/upload_strategy.dart
  71. 0 4
      lib/models/upload_url.dart
  72. 12 43
      lib/models/user_details.dart
  73. 0 2
      lib/services/app_lifecycle_service.dart
  74. 54 16
      lib/services/collections_service.dart
  75. 9 64
      lib/services/feature_flag_service.dart
  76. 6 4
      lib/services/file_magic_service.dart
  77. 37 0
      lib/services/files_service.dart
  78. 31 1
      lib/services/ignored_files_service.dart
  79. 333 0
      lib/services/local/local_sync_util.dart
  80. 11 11
      lib/services/local_file_update_service.dart
  81. 177 61
      lib/services/local_sync_service.dart
  82. 1 1
      lib/services/memories_service.dart
  83. 1 3
      lib/services/notification_service.dart
  84. 1 2
      lib/services/push_service.dart
  85. 197 46
      lib/services/remote_sync_service.dart
  86. 204 55
      lib/services/search_service.dart
  87. 10 1
      lib/services/sync_service.dart
  88. 1 0
      lib/services/trash_sync_service.dart
  89. 1 1
      lib/services/update_service.dart
  90. 156 0
      lib/theme/colors.dart
  91. 51 0
      lib/theme/effects.dart
  92. 36 0
      lib/theme/ente_theme.dart
  93. 117 0
      lib/theme/text_style.dart
  94. 0 1
      lib/ui/account/delete_account_page.dart
  95. 2 2
      lib/ui/account/recovery_key_page.dart
  96. 8 5
      lib/ui/account/verify_recovery_page.dart
  97. 80 59
      lib/ui/backup_folder_selection_page.dart
  98. 2 2
      lib/ui/collections/collection_item_widget.dart
  99. 12 14
      lib/ui/collections/device_folder_icon_widget.dart
  100. 73 20
      lib/ui/collections/device_folders_grid_view_widget.dart

+ 6 - 14
ios/Runner.xcodeproj/project.pbxproj

@@ -16,7 +16,7 @@
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
 		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-		DA6BE5E826B3BC8600656280 /* BuildFile in Resources */ = {isa = PBXBuildFile; };
+		DA6BE5E826B3BC8600656280 /* (null) in Resources */ = {isa = PBXBuildFile; };
 /* End PBXBuildFile section */
 
 /* Begin PBXCopyFilesBuildPhase section */
@@ -213,7 +213,7 @@
 				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
 				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
 				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
-				DA6BE5E826B3BC8600656280 /* BuildFile in Resources */,
+				DA6BE5E826B3BC8600656280 /* (null) in Resources */,
 				277218A0270F596900FFE3CC /* GoogleService-Info.plist in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -497,10 +497,7 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -633,6 +630,7 @@
 				STRIP_STYLE = "non-global";
 				STRIP_SWIFT_SYMBOLS = NO;
 				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				TARGETED_DEVICE_FAMILY = "1,2";
 				VALIDATE_PRODUCT = YES;
@@ -656,10 +654,7 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",
@@ -693,10 +688,7 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Flutter",

+ 2 - 0
ios/Runner/Info.plist

@@ -60,6 +60,8 @@
 			<key>NSAllowsArbitraryLoadsInWebContent</key>
 			<true/>
 		</dict>
+		<key>ITSAppUsesNonExemptEncryption</key>
+		<false/>
 		<key>NSFaceIDUsageDescription</key>
 		<string>Please allow ente to lock itself with FaceID or TouchID</string>
 		<key>NSPhotoLibraryUsageDescription</key>

+ 3 - 5
lib/core/cache/image_cache.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io' as io;
 
 import 'package:photos/core/cache/lru_map.dart';
@@ -7,11 +5,11 @@ import 'package:photos/core/cache/lru_map.dart';
 class FileLruCache {
   static final LRUMap<String, io.File> _map = LRUMap(25);
 
-  static io.File get(String key) {
+  static io.File? get(String key) {
     return _map.get(key);
   }
 
-  static void put(String key, io.File imageData) {
-    _map.put(key, imageData);
+  static void put(String key, io.File value) {
+    _map.put(key, value);
   }
 }

+ 5 - 7
lib/core/cache/lru_map.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:collection';
 
 typedef EvictionHandler<K, V> = Function(K key, V value);
@@ -7,12 +5,12 @@ typedef EvictionHandler<K, V> = Function(K key, V value);
 class LRUMap<K, V> {
   final LinkedHashMap<K, V> _map = LinkedHashMap<K, V>();
   final int _maxSize;
-  final EvictionHandler<K, V> _handler;
+  final EvictionHandler<K, V?>? _handler;
 
   LRUMap(this._maxSize, [this._handler]);
 
-  V get(K key) {
-    final V value = _map.remove(key);
+  V? get(K key) {
+    final V? value = _map.remove(key);
     if (value != null) {
       _map[key] = value;
     }
@@ -24,9 +22,9 @@ class LRUMap<K, V> {
     _map[key] = value;
     if (_map.length > _maxSize) {
       final K evictedKey = _map.keys.first;
-      final V evictedValue = _map.remove(evictedKey);
+      final V? evictedValue = _map.remove(evictedKey);
       if (_handler != null) {
-        _handler(evictedKey, evictedValue);
+        _handler!(evictedKey, evictedValue);
       }
     }
   }

+ 8 - 10
lib/core/cache/thumbnail_cache.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:typed_data';
 
 import 'package:photos/core/cache/lru_map.dart';
@@ -7,35 +5,35 @@ import 'package:photos/core/constants.dart';
 import 'package:photos/models/ente_file.dart';
 
 class ThumbnailLruCache {
-  static final LRUMap<String, Uint8List> _map = LRUMap(1000);
+  static final LRUMap<String, Uint8List?> _map = LRUMap(1000);
 
-  static Uint8List get(EnteFile enteFile, [int size]) {
+  static Uint8List? get(EnteFile enteFile, [int? size]) {
     return _map.get(
       enteFile.cacheKey() +
           "_" +
-          (size != null ? size.toString() : kThumbnailLargeSize.toString()),
+          (size != null ? size.toString() : thumbnailLargeSize.toString()),
     );
   }
 
   static void put(
     EnteFile enteFile,
-    Uint8List imageData, [
-    int size,
+    Uint8List? imageData, [
+    int? size,
   ]) {
     _map.put(
       enteFile.cacheKey() +
           "_" +
-          (size != null ? size.toString() : kThumbnailLargeSize.toString()),
+          (size != null ? size.toString() : thumbnailLargeSize.toString()),
       imageData,
     );
   }
 
   static void clearCache(EnteFile enteFile) {
     _map.remove(
-      enteFile.cacheKey() + "_" + kThumbnailLargeSize.toString(),
+      enteFile.cacheKey() + "_" + thumbnailLargeSize.toString(),
     );
     _map.remove(
-      enteFile.cacheKey() + "_" + kThumbnailSmallSize.toString(),
+      enteFile.cacheKey() + "_" + thumbnailSmallSize.toString(),
     );
   }
 }

+ 0 - 2
lib/core/cache/video_cache_manager.dart

@@ -1,5 +1,3 @@
-
-
 import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 
 class VideoCacheManager {

+ 107 - 101
lib/core/configuration.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 import 'dart:io' as io;
 import 'dart:typed_data';
@@ -10,14 +8,19 @@ import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:photos/core/constants.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/core/error-reporting/super_logging.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/collections_db.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/db/files_db.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/db/ignored_files_db.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/db/memories_db.dart';
 import 'package:photos/db/public_keys_db.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/db/trash_db.dart';
 import 'package:photos/db/upload_locks_db.dart';
 import 'package:photos/events/signed_in_event.dart';
@@ -25,11 +28,17 @@ import 'package:photos/events/user_logged_out_event.dart';
 import 'package:photos/models/key_attributes.dart';
 import 'package:photos/models/key_gen_result.dart';
 import 'package:photos/models/private_key_attributes.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/billing_service.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/collections_service.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/favorites_service.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/memories_service.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/search_service.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/sync_service.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/validator_util.dart';
@@ -57,8 +66,8 @@ class Configuration {
   static const keyShouldKeepDeviceAwake = "should_keep_device_awake";
   static const keyShouldHideFromRecents = "should_hide_from_recents";
   static const keyShouldShowLockScreen = "should_show_lock_screen";
-  static const keyHasSkippedBackupFolderSelection =
-      "has_skipped_backup_folder_selection";
+  static const keyHasSelectedAnyBackupFolder =
+      "has_selected_any_folder_for_backup";
   static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time";
   static const nameKey = "name";
   static const secretKeyKey = "secret_key";
@@ -75,21 +84,21 @@ class Configuration {
 
   static final _logger = Logger("Configuration");
 
-  String _cachedToken;
-  String _documentsDirectory;
-  String _key;
-  SharedPreferences _preferences;
-  String _secretKey;
-  FlutterSecureStorage _secureStorage;
-  String _tempDirectory;
-  String _thumbnailCacheDirectory;
+  String? _cachedToken;
+  late String _documentsDirectory;
+  String? _key;
+  late SharedPreferences _preferences;
+  String? _secretKey;
+  late FlutterSecureStorage _secureStorage;
+  late String _tempDirectory;
+  late String _thumbnailCacheDirectory;
   // 6th July 22: Remove this after 3 months. Hopefully, active users
   // will migrate to newer version of the app, where shared media is stored
   // on appSupport directory which OS won't clean up automatically
-  String _sharedTempMediaDirectory;
+  late String _sharedTempMediaDirectory;
 
-  String _sharedDocumentsMediaDirectory;
-  String _volatilePassword;
+  late String _sharedDocumentsMediaDirectory;
+  String? _volatilePassword;
 
   final _secureStorageOptionsIOS =
       const IOSOptions(accessibility: IOSAccessibility.first_unlock);
@@ -133,12 +142,15 @@ class Configuration {
         key: secretKeyKey,
         iOptions: _secureStorageOptionsIOS,
       );
+      if (_key == null) {
+        await logout(autoLogout: true);
+      }
       await _migrateSecurityStorageToFirstUnlock();
     }
     SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
   }
 
-  Future<void> logout() async {
+  Future<void> logout({bool autoLogout = false}) async {
     if (SyncService.instance.isSyncInProgress()) {
       SyncService.instance.stopSync();
       try {
@@ -161,12 +173,24 @@ class Configuration {
     await UploadLocksDB.instance.clearTable();
     await IgnoredFilesDB.instance.clearTable();
     await TrashDB.instance.clearTable();
-    CollectionsService.instance.clearCache();
-    FavoritesService.instance.clearCache();
-    MemoriesService.instance.clearCache();
-    BillingService.instance.clearCache();
-    SearchService.instance.clearCache();
-    Bus.instance.fire(UserLoggedOutEvent());
+    if (!autoLogout) {
+      CollectionsService.instance.clearCache();
+      FavoritesService.instance.clearCache();
+      MemoriesService.instance.clearCache();
+      BillingService.instance.clearCache();
+      SearchService.instance.clearCache();
+      Bus.instance.fire(UserLoggedOutEvent());
+    } else {
+      _preferences.setBool("auto_logout", true);
+    }
+  }
+
+  bool showAutoLogoutDialog() {
+    return _preferences.containsKey("auto_logout");
+  }
+
+  Future<bool> clearAutoLogoutFlag() {
+    return _preferences.remove("auto_logout");
   }
 
   Future<KeyGenResult> generateKey(String password) async {
@@ -183,8 +207,10 @@ class Configuration {
     // Derive a key from the password that will be used to encrypt and
     // decrypt the master key
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
-    final derivedKeyResult =
-        await CryptoUtil.deriveSensitiveKey(utf8.encode(password), kekSalt);
+    final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
+      utf8.encode(password) as Uint8List,
+      kekSalt,
+    );
 
     // Encrypt the key with this derived key
     final encryptedKeyData =
@@ -197,17 +223,17 @@ class Configuration {
 
     final attributes = KeyAttributes(
       Sodium.bin2base64(kekSalt),
-      Sodium.bin2base64(encryptedKeyData.encryptedData),
-      Sodium.bin2base64(encryptedKeyData.nonce),
+      Sodium.bin2base64(encryptedKeyData.encryptedData!),
+      Sodium.bin2base64(encryptedKeyData.nonce!),
       Sodium.bin2base64(keyPair.pk),
-      Sodium.bin2base64(encryptedSecretKeyData.encryptedData),
-      Sodium.bin2base64(encryptedSecretKeyData.nonce),
+      Sodium.bin2base64(encryptedSecretKeyData.encryptedData!),
+      Sodium.bin2base64(encryptedSecretKeyData.nonce!),
       derivedKeyResult.memLimit,
       derivedKeyResult.opsLimit,
-      Sodium.bin2base64(encryptedMasterKey.encryptedData),
-      Sodium.bin2base64(encryptedMasterKey.nonce),
-      Sodium.bin2base64(encryptedRecoveryKey.encryptedData),
-      Sodium.bin2base64(encryptedRecoveryKey.nonce),
+      Sodium.bin2base64(encryptedMasterKey.encryptedData!),
+      Sodium.bin2base64(encryptedMasterKey.nonce!),
+      Sodium.bin2base64(encryptedRecoveryKey.encryptedData!),
+      Sodium.bin2base64(encryptedRecoveryKey.nonce!),
     );
     final privateAttributes = PrivateKeyAttributes(
       Sodium.bin2base64(masterKey),
@@ -224,19 +250,21 @@ class Configuration {
     // Derive a key from the password that will be used to encrypt and
     // decrypt the master key
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
-    final derivedKeyResult =
-        await CryptoUtil.deriveSensitiveKey(utf8.encode(password), kekSalt);
+    final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
+      utf8.encode(password) as Uint8List,
+      kekSalt,
+    );
 
     // Encrypt the key with this derived key
     final encryptedKeyData =
-        CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
+        CryptoUtil.encryptSync(masterKey!, derivedKeyResult.key);
 
     final existingAttributes = getKeyAttributes();
 
-    return existingAttributes.copyWith(
+    return existingAttributes!.copyWith(
       kekSalt: Sodium.bin2base64(kekSalt),
-      encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData),
-      keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce),
+      encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
+      keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
       memLimit: derivedKeyResult.memLimit,
       opsLimit: derivedKeyResult.opsLimit,
     );
@@ -254,7 +282,7 @@ class Configuration {
     );
     _logger.info('state validation done');
     final kek = await CryptoUtil.deriveKey(
-      utf8.encode(password),
+      utf8.encode(password) as Uint8List,
       Sodium.base642bin(attributes.kekSalt),
       attributes.memLimit,
       attributes.opsLimit,
@@ -285,7 +313,7 @@ class Configuration {
     _logger.info("secret-key done");
     await setSecretKey(Sodium.bin2base64(secretKey));
     final token = CryptoUtil.openSealSync(
-      Sodium.base642bin(getEncryptedToken()),
+      Sodium.base642bin(getEncryptedToken()!),
       Sodium.base642bin(attributes.publicKey),
       secretKey,
     );
@@ -296,7 +324,7 @@ class Configuration {
   }
 
   Future<KeyAttributes> createNewRecoveryKey() async {
-    final masterKey = getKey();
+    final masterKey = getKey()!;
     final existingAttributes = getKeyAttributes();
 
     // Create a recovery key
@@ -306,22 +334,23 @@ class Configuration {
     final encryptedMasterKey = CryptoUtil.encryptSync(masterKey, recoveryKey);
     final encryptedRecoveryKey = CryptoUtil.encryptSync(recoveryKey, masterKey);
 
-    return existingAttributes.copyWith(
+    return existingAttributes!.copyWith(
       masterKeyEncryptedWithRecoveryKey:
-          Sodium.bin2base64(encryptedMasterKey.encryptedData),
-      masterKeyDecryptionNonce: Sodium.bin2base64(encryptedMasterKey.nonce),
+          Sodium.bin2base64(encryptedMasterKey.encryptedData!),
+      masterKeyDecryptionNonce: Sodium.bin2base64(encryptedMasterKey.nonce!),
       recoveryKeyEncryptedWithMasterKey:
-          Sodium.bin2base64(encryptedRecoveryKey.encryptedData),
-      recoveryKeyDecryptionNonce: Sodium.bin2base64(encryptedRecoveryKey.nonce),
+          Sodium.bin2base64(encryptedRecoveryKey.encryptedData!),
+      recoveryKeyDecryptionNonce:
+          Sodium.bin2base64(encryptedRecoveryKey.nonce!),
     );
   }
 
   Future<void> recover(String recoveryKey) async {
     // check if user has entered mnemonic code
     if (recoveryKey.contains(' ')) {
-      if (recoveryKey.split(' ').length != kMnemonicKeyWordCount) {
+      if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
         throw AssertionError(
-          'recovery code should have $kMnemonicKeyWordCount words',
+          'recovery code should have $mnemonicKeyWordCount words',
         );
       }
       recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
@@ -330,7 +359,7 @@ class Configuration {
     Uint8List masterKey;
     try {
       masterKey = await CryptoUtil.decrypt(
-        Sodium.base642bin(attributes.masterKeyEncryptedWithRecoveryKey),
+        Sodium.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
         Sodium.hex2bin(recoveryKey),
         Sodium.base642bin(attributes.masterKeyDecryptionNonce),
       );
@@ -346,7 +375,7 @@ class Configuration {
     );
     await setSecretKey(Sodium.bin2base64(secretKey));
     final token = CryptoUtil.openSealSync(
-      Sodium.base642bin(getEncryptedToken()),
+      Sodium.base642bin(getEncryptedToken()!),
       Sodium.base642bin(attributes.publicKey),
       secretKey,
     );
@@ -359,7 +388,7 @@ class Configuration {
     return endpoint;
   }
 
-  String getToken() {
+  String? getToken() {
     _cachedToken ??= _preferences.getString(tokenKey);
     return _cachedToken;
   }
@@ -374,11 +403,11 @@ class Configuration {
     await _preferences.setString(encryptedTokenKey, encryptedToken);
   }
 
-  String getEncryptedToken() {
+  String? getEncryptedToken() {
     return _preferences.getString(encryptedTokenKey);
   }
 
-  String getEmail() {
+  String? getEmail() {
     return _preferences.getString(emailKey);
   }
 
@@ -386,7 +415,7 @@ class Configuration {
     await _preferences.setString(emailKey, email);
   }
 
-  String getName() {
+  String? getName() {
     return _preferences.getString(nameKey);
   }
 
@@ -394,7 +423,7 @@ class Configuration {
     await _preferences.setString(nameKey, name);
   }
 
-  int getUserID() {
+  int? getUserID() {
     return _preferences.getInt(userIDKey);
   }
 
@@ -404,33 +433,17 @@ class Configuration {
 
   Set<String> getPathsToBackUp() {
     if (_preferences.containsKey(foldersToBackUpKey)) {
-      return _preferences.getStringList(foldersToBackUpKey).toSet();
+      return _preferences.getStringList(foldersToBackUpKey)!.toSet();
     } else {
       return <String>{};
     }
   }
 
-  Future<void> setPathsToBackUp(Set<String> newPaths) async {
-    await _preferences.setStringList(foldersToBackUpKey, newPaths.toList());
-    final allFolders = (await FilesDB.instance.getLatestLocalFiles())
-        .map((file) => file.deviceFolder)
-        .toList();
-    await _setSelectAllFoldersForBackup(newPaths.length == allFolders.length);
-    SyncService.instance.onFoldersSet(newPaths);
-    SyncService.instance.sync();
-  }
-
-  Future<void> addPathToFoldersToBeBackedUp(String path) async {
-    final currentPaths = getPathsToBackUp();
-    currentPaths.add(path);
-    return setPathsToBackUp(currentPaths);
-  }
-
   Future<void> setKeyAttributes(KeyAttributes attributes) async {
-    await _preferences.setString(keyAttributesKey, attributes?.toJson());
+    await _preferences.setString(keyAttributesKey, attributes.toJson());
   }
 
-  KeyAttributes getKeyAttributes() {
+  KeyAttributes? getKeyAttributes() {
     final jsonValue = _preferences.getString(keyAttributesKey);
     if (jsonValue == null) {
       return null;
@@ -439,7 +452,7 @@ class Configuration {
     }
   }
 
-  Future<void> setKey(String key) async {
+  Future<void> setKey(String? key) async {
     _key = key;
     if (key == null) {
       await _secureStorage.delete(
@@ -455,7 +468,7 @@ class Configuration {
     }
   }
 
-  Future<void> setSecretKey(String secretKey) async {
+  Future<void> setSecretKey(String? secretKey) async {
     _secretKey = secretKey;
     if (secretKey == null) {
       await _secureStorage.delete(
@@ -471,16 +484,16 @@ class Configuration {
     }
   }
 
-  Uint8List getKey() {
-    return _key == null ? null : Sodium.base642bin(_key);
+  Uint8List? getKey() {
+    return _key == null ? null : Sodium.base642bin(_key!);
   }
 
-  Uint8List getSecretKey() {
-    return _secretKey == null ? null : Sodium.base642bin(_secretKey);
+  Uint8List? getSecretKey() {
+    return _secretKey == null ? null : Sodium.base642bin(_secretKey!);
   }
 
   Uint8List getRecoveryKey() {
-    final keyAttributes = getKeyAttributes();
+    final keyAttributes = getKeyAttributes()!;
     return CryptoUtil.decryptSync(
       Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
       getKey(),
@@ -488,10 +501,6 @@ class Configuration {
     );
   }
 
-  String getDocumentsDirectory() {
-    return _documentsDirectory;
-  }
-
   // Caution: This directory is cleared on app start
   String getTempDirectory() {
     return _tempDirectory;
@@ -515,7 +524,7 @@ class Configuration {
 
   bool shouldBackupOverMobileData() {
     if (_preferences.containsKey(keyShouldBackupOverMobileData)) {
-      return _preferences.getBool(keyShouldBackupOverMobileData);
+      return _preferences.getBool(keyShouldBackupOverMobileData)!;
     } else {
       return false;
     }
@@ -530,14 +539,15 @@ class Configuration {
 
   bool shouldBackupVideos() {
     if (_preferences.containsKey(keyShouldBackupVideos)) {
-      return _preferences.getBool(keyShouldBackupVideos);
+      return _preferences.getBool(keyShouldBackupVideos)!;
     } else {
       return true;
     }
   }
 
   bool shouldKeepDeviceAwake() {
-    return _preferences.get(keyShouldKeepDeviceAwake) ?? false;
+    final keepAwake = _preferences.get(keyShouldKeepDeviceAwake);
+    return keepAwake == null ? false : keepAwake as bool;
   }
 
   Future<void> setShouldKeepDeviceAwake(bool value) async {
@@ -556,7 +566,7 @@ class Configuration {
 
   bool shouldShowLockScreen() {
     if (_preferences.containsKey(keyShouldShowLockScreen)) {
-      return _preferences.getBool(keyShouldShowLockScreen);
+      return _preferences.getBool(keyShouldShowLockScreen)!;
     } else {
       return false;
     }
@@ -567,11 +577,7 @@ class Configuration {
   }
 
   bool shouldHideFromRecents() {
-    if (_preferences.containsKey(keyShouldHideFromRecents)) {
-      return _preferences.getBool(keyShouldHideFromRecents);
-    } else {
-      return false;
-    }
+    return _preferences.getBool(keyShouldHideFromRecents) ?? false;
   }
 
   Future<void> setShouldHideFromRecents(bool value) {
@@ -582,23 +588,23 @@ class Configuration {
     _volatilePassword = volatilePassword;
   }
 
-  String getVolatilePassword() {
+  String? getVolatilePassword() {
     return _volatilePassword;
   }
 
-  Future<void> skipBackupFolderSelection() async {
-    await _preferences.setBool(keyHasSkippedBackupFolderSelection, true);
+  Future<void> setHasSelectedAnyBackupFolder(bool val) async {
+    await _preferences.setBool(keyHasSelectedAnyBackupFolder, val);
   }
 
-  bool hasSkippedBackupFolderSelection() {
-    return _preferences.getBool(keyHasSkippedBackupFolderSelection) ?? false;
+  bool hasSelectedAnyBackupFolder() {
+    return _preferences.getBool(keyHasSelectedAnyBackupFolder) ?? false;
   }
 
   bool hasSelectedAllFoldersForBackup() {
     return _preferences.getBool(hasSelectedAllFoldersForBackupKey) ?? false;
   }
 
-  Future<void> _setSelectAllFoldersForBackup(bool value) async {
+  Future<void> setSelectAllFoldersForBackup(bool value) async {
     await _preferences.setBool(hasSelectedAllFoldersForBackupKey, value);
   }
 
@@ -630,6 +636,6 @@ class Configuration {
       //ignore: prefer_const_constructors
       await _preferences.setString(anonymousUserIDKey, Uuid().v4());
     }
-    return _preferences.getString(anonymousUserIDKey);
+    return _preferences.getString(anonymousUserIDKey)!;
   }
 }

+ 22 - 27
lib/core/constants.dart

@@ -1,48 +1,43 @@
-// @dart = 2.7
-
-const int kThumbnailSmallSize = 256;
-const int kThumbnailQuality = 50;
-const int kThumbnailLargeSize = 512;
-const int kCompressedThumbnailResolution = 1080;
-const int kThumbnailDataLimit = 100 * 1024;
-const String kSentryDSN =
+const int thumbnailSmallSize = 256;
+const int thumbnailQuality = 50;
+const int thumbnailLargeSize = 512;
+const int compressedThumbnailResolution = 1080;
+const int thumbnailDataLimit = 100 * 1024;
+const String sentryDSN =
     "https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4";
-const String kSentryDebugDSN =
+const String sentryDebugDSN =
     "https://ca5e686dd7f149d9bf94e620564cceba@sentry.ente.io/3";
-const String kSentryTunnel = "https://sentry-reporter.ente.io";
-const String kRoadmapURL = "https://roadmap.ente.io";
-const int kMicroSecondsInDay = 86400000000;
-const int kAndroid11SDKINT = 30;
-const int kGalleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
-const int kGalleryLoadEndTime = 9223372036854775807; // 2^63 -1
+const String sentryTunnel = "https://sentry-reporter.ente.io";
+const String roadmapURL = "https://roadmap.ente.io";
+const int microSecondsInDay = 86400000000;
+const int android11SDKINT = 30;
+const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
+const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 
 // used to identify which ente file are available in app cache
 // todo: 6Jun22: delete old media identifier after 3 months
-const String kOldSharedMediaIdentifier = 'ente-shared://';
-const String kSharedMediaIdentifier = 'ente-shared-media://';
+const String oldSharedMediaIdentifier = 'ente-shared://';
+const String sharedMediaIdentifier = 'ente-shared-media://';
 
-const int kMaxLivePhotoToastCount = 2;
-const String kLivePhotoToastCounterKey = "show_live_photo_toast";
+const int maxLivePhotoToastCount = 2;
+const String livePhotoToastCounterKey = "show_live_photo_toast";
 
-const kThumbnailDiskLoadDeferDuration = Duration(milliseconds: 40);
-const kThumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
+const thumbnailDiskLoadDeferDuration = Duration(milliseconds: 40);
+const thumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
 
 // 256 bit key maps to 24 words
 // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#Generating_the_mnemonic
-const kMnemonicKeyWordCount = 24;
+const mnemonicKeyWordCount = 24;
 
 // https://stackoverflow.com/a/61162219
-const kDragSensitivity = 8;
+const dragSensitivity = 8;
 
-const kSupportEmail = 'support@ente.io';
+const supportEmail = 'support@ente.io';
 
 // Default values for various feature flags
 class FFDefault {
   static const bool enableStripe = true;
-  static const bool disableUrlSharing = false;
   static const bool disableCFWorker = false;
-  static const bool enableMissingLocationMigration = false;
-  static const bool enableSearch = false;
 }
 
 const kDefaultProductionEndpoint = 'https://api.ente.io';

+ 1 - 1
lib/core/error-reporting/super_logging.dart

@@ -209,7 +209,7 @@ class SuperLogging {
   }
 
   static void setUserID(String userID) async {
-    if (config.sentryDsn != null) {
+    if (config?.sentryDsn != null) {
       Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
       $.info("setting sentry user ID to: $userID");
     }

+ 0 - 2
lib/core/errors.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 class InvalidFileError extends ArgumentError {
   InvalidFileError(String message) : super(message);
 }

+ 1 - 3
lib/core/network.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io';
 import 'package:dio/dio.dart';
 import 'package:fk_user_agent/fk_user_agent.dart';
@@ -9,7 +7,7 @@ import 'package:uuid/uuid.dart';
 int kConnectTimeout = 15000;
 
 class Network {
-  Dio _dio;
+  late Dio _dio;
 
   Future<void> init() async {
     await FkUserAgent.init();

+ 22 - 18
lib/data/holidays.dart

@@ -1,22 +1,26 @@
-// @dart=2.9
+class HolidayData {
+  final String name;
+  final int month;
+  final int day;
 
-import 'package:photos/models/search/holiday_search_result.dart';
+  const HolidayData(this.name, {required this.month, required this.day});
+}
 
 const List<HolidayData> allHolidays = [
-  HolidayData('New Year', 1, 1),
-  HolidayData('Epiphany', 1, 6),
-  HolidayData('Pongal', 1, 14),
-  HolidayData('Makar Sankranthi', 1, 14),
-  HolidayData('Valentine\'s Day', 2, 14),
-  HolidayData('Nowruz', 3, 21),
-  HolidayData('Walpurgis Night', 4, 30),
-  HolidayData('Vappu', 4, 30),
-  HolidayData('May Day', 5, 1),
-  HolidayData('Midsummer\'s Eve', 6, 24),
-  HolidayData('Midsummer Day', 6, 25),
-  HolidayData('Christmas Eve', 12, 24),
-  HolidayData('Halloween', 10, 31),
-  HolidayData('Christmas', 12, 25),
-  HolidayData('Boxing Day', 12, 26),
-  HolidayData('New Year\'s Eve', 12, 31),
+  HolidayData('New Year', month: 1, day: 1),
+  HolidayData('Epiphany', month: 1, day: 6),
+  HolidayData('Pongal', month: 1, day: 14),
+  HolidayData('Makar Sankranthi', month: 1, day: 14),
+  HolidayData('Valentine\'s Day', month: 2, day: 14),
+  HolidayData('Nowruz', month: 3, day: 21),
+  HolidayData('Walpurgis Night', month: 4, day: 30),
+  HolidayData('Vappu', month: 4, day: 30),
+  HolidayData('May Day', month: 5, day: 1),
+  HolidayData('Midsummer\'s Eve', month: 6, day: 24),
+  HolidayData('Midsummer Day', month: 6, day: 25),
+  HolidayData('Christmas Eve', month: 12, day: 24),
+  HolidayData('Halloween', month: 10, day: 31),
+  HolidayData('Christmas', month: 12, day: 25),
+  HolidayData('Boxing Day', month: 12, day: 26),
+  HolidayData('New Year\'s Eve', month: 12, day: 31),
 ];

+ 7 - 4
lib/data/months.dart

@@ -1,7 +1,3 @@
-// @dart=2.9
-
-import 'package:photos/models/search/month_search_result.dart';
-
 List<MonthData> allMonths = [
   MonthData('January', 1),
   MonthData('February', 2),
@@ -16,3 +12,10 @@ List<MonthData> allMonths = [
   MonthData('November', 11),
   MonthData('December', 12),
 ];
+
+class MonthData {
+  final String name;
+  final int monthNumber;
+
+  MonthData(this.name, this.monthNumber);
+}

+ 0 - 2
lib/data/years.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:photos/utils/date_time_util.dart';
 
 class YearsData {

+ 9 - 24
lib/db/collections_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 import 'dart:io';
 
@@ -54,15 +52,16 @@ class CollectionsDB {
 
   static final CollectionsDB instance = CollectionsDB._privateConstructor();
 
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
 
   Future<Database> get database async {
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   Future<Database> _initDatabase() async {
-    final Directory documentsDirectory = await getApplicationDocumentsDirectory();
+    final Directory documentsDirectory =
+        await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     return await openDatabaseWithMigration(path, dbConfig);
   }
@@ -180,20 +179,6 @@ class CollectionsDB {
     return collections;
   }
 
-  Future<int> getLastCollectionUpdationTime() async {
-    final db = await instance.database;
-    final rows = await db.query(
-      table,
-      orderBy: '$columnUpdationTime DESC',
-      limit: 1,
-    );
-    if (rows.isNotEmpty) {
-      return int.parse(rows[0][columnUpdationTime]);
-    } else {
-      return null;
-    }
-  }
-
   Future<int> deleteCollection(int collectionID) async {
     final db = await instance.database;
     return db.delete(
@@ -206,7 +191,7 @@ class CollectionsDB {
   Map<String, dynamic> _getRowForCollection(Collection collection) {
     final row = <String, dynamic>{};
     row[columnID] = collection.id;
-    row[columnOwner] = collection.owner.toJson();
+    row[columnOwner] = collection.owner!.toJson();
     row[columnEncryptedKey] = collection.encryptedKey;
     row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce;
     row[columnName] = collection.name;
@@ -217,16 +202,16 @@ class CollectionsDB {
     row[columnPathDecryptionNonce] = collection.attributes.pathDecryptionNonce;
     row[columnVersion] = collection.attributes.version;
     row[columnSharees] =
-        json.encode(collection.sharees?.map((x) => x?.toMap())?.toList());
+        json.encode(collection.sharees?.map((x) => x?.toMap()).toList());
     row[columnPublicURLs] =
-        json.encode(collection.publicURLs?.map((x) => x?.toMap())?.toList());
+        json.encode(collection.publicURLs?.map((x) => x?.toMap()).toList());
     row[columnUpdationTime] = collection.updationTime;
-    if (collection.isDeleted ?? false) {
+    if (collection.isDeleted) {
       row[columnIsDeleted] = _sqlBoolTrue;
     } else {
       row[columnIsDeleted] = _sqlBoolFalse;
     }
-    row[columnMMdVersion] = collection.mMdVersion ?? 0;
+    row[columnMMdVersion] = collection.mMdVersion;
     row[columnMMdEncodedJson] = collection.mMdEncodedJson ?? '{}';
     return row;
   }

+ 376 - 0
lib/db/device_files_db.dart

@@ -0,0 +1,376 @@
+// @dart = 2.9
+import 'package:flutter/foundation.dart';
+import 'package:logging/logging.dart';
+import 'package:photo_manager/photo_manager.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/models/device_collection.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/file_load_result.dart';
+import 'package:photos/models/upload_strategy.dart';
+import 'package:photos/services/local/local_sync_util.dart';
+import 'package:sqflite/sqlite_api.dart';
+import 'package:tuple/tuple.dart';
+
+extension DeviceFiles on FilesDB {
+  static final Logger _logger = Logger("DeviceFilesDB");
+  static const _sqlBoolTrue = 1;
+  static const _sqlBoolFalse = 0;
+
+  Future<void> insertPathIDToLocalIDMapping(
+    Map<String, Set<String>> mappingToAdd, {
+    ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore,
+  }) async {
+    debugPrint("Inserting missing PathIDToLocalIDMapping");
+    final db = await database;
+    var batch = db.batch();
+    int batchCounter = 0;
+    for (MapEntry e in mappingToAdd.entries) {
+      final String pathID = e.key;
+      for (String localID in e.value) {
+        if (batchCounter == 400) {
+          await batch.commit(noResult: true);
+          batch = db.batch();
+          batchCounter = 0;
+        }
+        batch.insert(
+          "device_files",
+          {
+            "id": localID,
+            "path_id": pathID,
+          },
+          conflictAlgorithm: conflictAlgorithm,
+        );
+        batchCounter++;
+      }
+    }
+    await batch.commit(noResult: true);
+  }
+
+  Future<void> deletePathIDToLocalIDMapping(
+    Map<String, Set<String>> mappingsToRemove,
+  ) async {
+    debugPrint("removing PathIDToLocalIDMapping");
+    final db = await database;
+    var batch = db.batch();
+    int batchCounter = 0;
+    for (MapEntry e in mappingsToRemove.entries) {
+      final String pathID = e.key;
+      for (String localID in e.value) {
+        if (batchCounter == 400) {
+          await batch.commit(noResult: true);
+          batch = db.batch();
+          batchCounter = 0;
+        }
+        batch.delete(
+          "device_files",
+          where: 'id = ? AND path_id = ?',
+          whereArgs: [localID, pathID],
+        );
+        batchCounter++;
+      }
+    }
+    await batch.commit(noResult: true);
+  }
+
+  Future<Map<String, int>> getDevicePathIDToImportedFileCount() async {
+    try {
+      final db = await database;
+      final rows = await db.rawQuery(
+        '''
+      SELECT count(*) as count, path_id
+      FROM device_files
+      GROUP BY path_id
+    ''',
+      );
+      final result = <String, int>{};
+      for (final row in rows) {
+        result[row['path_id']] = row["count"];
+      }
+      return result;
+    } catch (e) {
+      _logger.severe("failed to getDevicePathIDToImportedFileCount", e);
+      rethrow;
+    }
+  }
+
+  Future<Map<String, Set<String>>> getDevicePathIDToLocalIDMap() async {
+    try {
+      final db = await database;
+      final rows = await db.rawQuery(
+        ''' SELECT id, path_id FROM device_files; ''',
+      );
+      final result = <String, Set<String>>{};
+      for (final row in rows) {
+        final String pathID = row['path_id'];
+        if (!result.containsKey(pathID)) {
+          result[pathID] = <String>{};
+        }
+        result[pathID].add(row['id']);
+      }
+      return result;
+    } catch (e) {
+      _logger.severe("failed to getDevicePathIDToLocalIDMap", e);
+      rethrow;
+    }
+  }
+
+  Future<Set<String>> getDevicePathIDs() async {
+    final Database db = await database;
+    final rows = await db.rawQuery(
+      '''
+      SELECT id FROM device_collections
+      ''',
+    );
+    final Set<String> result = <String>{};
+    for (final row in rows) {
+      result.add(row['id']);
+    }
+    return result;
+  }
+
+  Future<void> insertLocalAssets(
+    List<LocalPathAsset> localPathAssets, {
+    bool shouldAutoBackup = false,
+  }) async {
+    final Database db = await database;
+    final Map<String, Set<String>> pathIDToLocalIDsMap = {};
+    try {
+      final batch = db.batch();
+      final Set<String> existingPathIds = await getDevicePathIDs();
+      for (LocalPathAsset localPathAsset in localPathAssets) {
+        pathIDToLocalIDsMap[localPathAsset.pathID] = localPathAsset.localIDs;
+        if (existingPathIds.contains(localPathAsset.pathID)) {
+          batch.rawUpdate(
+            "UPDATE device_collections SET name = ? where id = "
+            "?",
+            [localPathAsset.pathName, localPathAsset.pathID],
+          );
+        } else {
+          batch.insert(
+            "device_collections",
+            {
+              "id": localPathAsset.pathID,
+              "name": localPathAsset.pathName,
+              "should_backup": shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse
+            },
+            conflictAlgorithm: ConflictAlgorithm.ignore,
+          );
+        }
+      }
+      await batch.commit(noResult: true);
+      // add the mappings for localIDs
+      if (pathIDToLocalIDsMap.isNotEmpty) {
+        await insertPathIDToLocalIDMapping(pathIDToLocalIDsMap);
+      }
+    } catch (e) {
+      _logger.severe("failed to save path names", e);
+      rethrow;
+    }
+  }
+
+  Future<bool> updateDeviceCoverWithCount(
+    List<Tuple2<AssetPathEntity, String>> devicePathInfo, {
+    bool shouldBackup = false,
+  }) async {
+    bool hasUpdated = false;
+    try {
+      final Database db = await database;
+      final Set<String> existingPathIds = await getDevicePathIDs();
+      for (Tuple2<AssetPathEntity, String> tup in devicePathInfo) {
+        final AssetPathEntity pathEntity = tup.item1;
+        final String localID = tup.item2;
+        final bool shouldUpdate = existingPathIds.contains(pathEntity.id);
+        if (shouldUpdate) {
+          final rowUpdated = await db.rawUpdate(
+            "UPDATE device_collections SET name = ?, cover_id = ?, count"
+            " = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)",
+            [
+              pathEntity.name,
+              localID,
+              pathEntity.assetCount,
+              pathEntity.id,
+              pathEntity.name,
+              localID,
+              pathEntity.assetCount,
+            ],
+          );
+          if (rowUpdated > 0) {
+            _logger.fine("Updated $rowUpdated rows for ${pathEntity.name}");
+            hasUpdated = true;
+          }
+        } else {
+          hasUpdated = true;
+          await db.insert(
+            "device_collections",
+            {
+              "id": pathEntity.id,
+              "name": pathEntity.name,
+              "count": pathEntity.assetCount,
+              "cover_id": localID,
+              "should_backup": shouldBackup ? _sqlBoolTrue : _sqlBoolFalse
+            },
+          );
+        }
+      }
+      // delete existing pathIDs which are missing on device
+      existingPathIds.removeAll(devicePathInfo.map((e) => e.item1.id).toSet());
+      if (existingPathIds.isNotEmpty) {
+        hasUpdated = true;
+        _logger.info('Deleting following pathIds from local $existingPathIds ');
+        for (String pathID in existingPathIds) {
+          await db.delete(
+            "device_collections",
+            where: 'id = ?',
+            whereArgs: [pathID],
+          );
+          await db.delete(
+            "device_files",
+            where: 'path_id = ?',
+            whereArgs: [pathID],
+          );
+        }
+      }
+      return hasUpdated;
+    } catch (e) {
+      _logger.severe("failed to save path names", e);
+      rethrow;
+    }
+  }
+
+  // getDeviceSyncCollectionIDs returns the collectionIDs for the
+  // deviceCollections which are marked for auto-backup
+  Future<Set<int>> getDeviceSyncCollectionIDs() async {
+    final Database db = await database;
+    final rows = await db.rawQuery(
+      '''
+      SELECT collection_id FROM device_collections where should_backup = 
+      $_sqlBoolTrue 
+      and collection_id != -1;
+      ''',
+    );
+    final Set<int> result = <int>{};
+    for (final row in rows) {
+      result.add(row['collection_id']);
+    }
+    return result;
+  }
+
+  Future<void> updateDevicePathSyncStatus(Map<String, bool> syncStatus) async {
+    final db = await database;
+    var batch = db.batch();
+    int batchCounter = 0;
+    for (MapEntry e in syncStatus.entries) {
+      final String pathID = e.key;
+      if (batchCounter == 400) {
+        await batch.commit(noResult: true);
+        batch = db.batch();
+        batchCounter = 0;
+      }
+      batch.update(
+        "device_collections",
+        {
+          "should_backup": e.value ? _sqlBoolTrue : _sqlBoolFalse,
+        },
+        where: 'id = ?',
+        whereArgs: [pathID],
+      );
+      batchCounter++;
+    }
+    await batch.commit(noResult: true);
+  }
+
+  Future<void> updateDeviceCollection(
+    String pathID,
+    int collectionID,
+  ) async {
+    final db = await database;
+    await db.update(
+      "device_collections",
+      {"collection_id": collectionID},
+      where: 'id = ?',
+      whereArgs: [pathID],
+    );
+    return;
+  }
+
+  Future<FileLoadResult> getFilesInDeviceCollection(
+    DeviceCollection deviceCollection,
+    int startTime,
+    int endTime, {
+    int limit,
+    bool asc,
+  }) async {
+    final db = await database;
+    final order = (asc ?? false ? 'ASC' : 'DESC');
+    final String rawQuery = '''
+    SELECT *
+          FROM ${FilesDB.filesTable}
+          WHERE ${FilesDB.columnLocalID} IS NOT NULL AND
+          ${FilesDB.columnCreationTime} >= $startTime AND 
+          ${FilesDB.columnCreationTime} <= $endTime AND 
+          ${FilesDB.columnLocalID} IN 
+          (SELECT id FROM device_files where path_id = '${deviceCollection.id}' ) 
+          ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order
+         ''' +
+        (limit != null ? ' limit $limit;' : ';');
+    final results = await db.rawQuery(rawQuery);
+    final files = convertToFiles(results);
+    final dedupe = deduplicateByLocalID(files);
+    return FileLoadResult(dedupe, files.length == limit);
+  }
+
+  Future<List<DeviceCollection>> getDeviceCollections({
+    bool includeCoverThumbnail = false,
+  }) async {
+    debugPrint(
+      "Fetching DeviceCollections From DB with thumbnail = "
+      "$includeCoverThumbnail",
+    );
+    try {
+      final db = await database;
+      final coverFiles = <File>[];
+      if (includeCoverThumbnail) {
+        final fileRows = await db.rawQuery(
+          '''SELECT * FROM FILES where local_id in (select cover_id from device_collections) group by local_id;
+          ''',
+        );
+        final files = convertToFiles(fileRows);
+        coverFiles.addAll(files);
+      }
+      final deviceCollectionRows = await db.rawQuery(
+        '''SELECT * from device_collections''',
+      );
+      final List<DeviceCollection> deviceCollections = [];
+      for (var row in deviceCollectionRows) {
+        final DeviceCollection deviceCollection = DeviceCollection(
+          row["id"],
+          row['name'],
+          count: row['count'],
+          collectionID: row["collection_id"],
+          coverId: row["cover_id"],
+          shouldBackup: (row["should_backup"] ?? _sqlBoolFalse) == _sqlBoolTrue,
+          uploadStrategy: getUploadType(row["upload_strategy"] ?? 0),
+        );
+        if (includeCoverThumbnail) {
+          deviceCollection.thumbnail = coverFiles.firstWhere(
+            (element) => element.localID == deviceCollection.coverId,
+            orElse: () => null,
+          );
+          if (deviceCollection.thumbnail == null) {
+            //todo: find another image which is already imported in db for
+            // this collection
+            _logger.warning(
+              'Failed to find coverThumbnail for ${deviceCollection.name}',
+            );
+            continue;
+          }
+        }
+        deviceCollections.add(deviceCollection);
+      }
+      return deviceCollections;
+    } catch (e) {
+      _logger.severe('Failed to getDeviceCollections', e);
+      rethrow;
+    }
+  }
+}

+ 7 - 9
lib/db/file_updation_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io';
 
 import 'package:flutter/foundation.dart';
@@ -56,12 +54,12 @@ class FileUpdationDB {
   static final FileUpdationDB instance = FileUpdationDB._privateConstructor();
 
   // only have a single app-wide reference to the database
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
 
   Future<Database> get database async {
     // lazily instantiate the db the first time it is accessed
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   // this opens the database (and creates it if it doesn't exist)
@@ -129,25 +127,25 @@ class FileUpdationDB {
     );
   }
 
-  Future<List<String>> getLocalIDsForPotentialReUpload(
+  Future<List<String?>> getLocalIDsForPotentialReUpload(
     int limit,
     String reason,
   ) async {
     final db = await instance.database;
-    String whereClause = '$columnReason = "$reason"';
+    final String whereClause = '$columnReason = "$reason"';
     final rows = await db.query(
       tableName,
       limit: limit,
       where: whereClause,
     );
-    final result = <String>[];
+    final result = <String?>[];
     for (final row in rows) {
-      result.add(row[columnLocalID]);
+      result.add(row[columnLocalID] as String?);
     }
     return result;
   }
 
-  Map<String, dynamic> _getRowForReUploadTable(String localID, String reason) {
+  Map<String, dynamic> _getRowForReUploadTable(String? localID, String reason) {
     assert(localID != null);
     final row = <String, dynamic>{};
     row[columnLocalID] = localID;

Разница между файлами не показана из-за своего большого размера
+ 272 - 241
lib/db/files_db.dart


+ 38 - 1
lib/db/ignored_files_db.dart

@@ -54,7 +54,8 @@ class IgnoredFilesDB {
 
   // this opens the database (and creates it if it doesn't exist)
   Future<Database> _initDatabase() async {
-    final Directory documentsDirectory = await getApplicationDocumentsDirectory();
+    final Directory documentsDirectory =
+        await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     return await openDatabase(
       path,
@@ -108,6 +109,42 @@ class IgnoredFilesDB {
     return result;
   }
 
+  Future<void> removeIgnoredEntries(List<IgnoredFile> ignoredFiles) async {
+    final startTime = DateTime.now();
+    final db = await instance.database;
+    var batch = db.batch();
+    int batchCounter = 0;
+    for (IgnoredFile file in ignoredFiles) {
+      if (batchCounter == 400) {
+        await batch.commit(noResult: true);
+        batch = db.batch();
+        batchCounter = 0;
+      }
+      // on Android, we track device folder and title to track files to ignore.
+      // See IgnoredFileService#_getIgnoreID method for more detail
+      if (Platform.isAndroid) {
+        batch.rawDelete(
+          "DELETE from $tableName WHERE  $columnDeviceFolder = '${file.deviceFolder}' AND $columnTitle = '${file.title}' ",
+        );
+      } else {
+        batch.rawDelete(
+          "DELETE from $tableName WHERE $columnLocalID = '${file.localID}' ",
+        );
+      }
+      batchCounter++;
+    }
+    await batch.commit(noResult: true);
+    final endTime = DateTime.now();
+    final duration = Duration(
+      microseconds:
+          endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
+    );
+    _logger.info(
+      "Batch delete for ${ignoredFiles.length} "
+      "took ${duration.inMilliseconds} ms.",
+    );
+  }
+
   IgnoredFile _getIgnoredFileFromRow(Map<String, dynamic> row) {
     return IgnoredFile(
       row[columnLocalID],

+ 4 - 5
lib/db/public_keys_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 
@@ -20,15 +18,16 @@ class PublicKeysDB {
   PublicKeysDB._privateConstructor();
   static final PublicKeysDB instance = PublicKeysDB._privateConstructor();
 
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
 
   Future<Database> get database async {
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   Future<Database> _initDatabase() async {
-    final Directory documentsDirectory = await getApplicationDocumentsDirectory();
+    final Directory documentsDirectory =
+        await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     return await openDatabase(
       path,

+ 3 - 2
lib/db/trash_db.dart

@@ -87,7 +87,8 @@ class TrashDB {
 
   // this opens the database (and creates it if it doesn't exist)
   Future<Database> _initDatabase() async {
-    final Directory documentsDirectory = await getApplicationDocumentsDirectory();
+    final Directory documentsDirectory =
+        await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     _logger.info("DB path " + path);
     return await openDatabase(
@@ -263,7 +264,7 @@ class TrashDB {
 
     row[columnLocalID] = trash.localID;
     row[columnCreationTime] = trash.creationTime;
-    row[columnFileMetadata] = jsonEncode(trash.getMetadata());
+    row[columnFileMetadata] = jsonEncode(trash.metadata);
 
     row[columnMMdVersion] = trash.mMdVersion ?? 0;
     row[columnMMdEncodedJson] = trash.mMdEncodedJson ?? '{}';

+ 4 - 5
lib/db/upload_locks_db.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:async';
 import 'dart:io';
 
@@ -19,14 +17,15 @@ class UploadLocksDB {
   UploadLocksDB._privateConstructor();
   static final UploadLocksDB instance = UploadLocksDB._privateConstructor();
 
-  static Future<Database> _dbFuture;
+  static Future<Database>? _dbFuture;
   Future<Database> get database async {
     _dbFuture ??= _initDatabase();
-    return _dbFuture;
+    return _dbFuture!;
   }
 
   Future<Database> _initDatabase() async {
-    final Directory documentsDirectory = await getApplicationDocumentsDirectory();
+    final Directory documentsDirectory =
+        await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     return await openDatabase(
       path,

+ 70 - 97
lib/ente_theme_data.dart

@@ -1,14 +1,14 @@
-// @dart=2.9
-
 import 'package:flutter/material.dart';
 import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
 
 final lightThemeData = ThemeData(
   fontFamily: 'Inter',
   brightness: Brightness.light,
-  hintColor: Colors.grey,
-  primaryColor: Colors.deepOrangeAccent,
-  primaryColorLight: Colors.black54,
+  hintColor: const Color.fromRGBO(158, 158, 158, 1),
+  primaryColor: const Color.fromRGBO(255, 110, 64, 1),
+  primaryColorLight: const Color.fromRGBO(0, 0, 0, 0.541),
   iconTheme: const IconThemeData(color: Colors.black),
   primaryIconTheme:
       const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
@@ -18,18 +18,18 @@ final lightThemeData = ThemeData(
   ),
   accentColor: const Color.fromRGBO(0, 0, 0, 0.6),
   outlinedButtonTheme: buildOutlinedButtonThemeData(
-    bgDisabled: Colors.grey.shade500,
-    bgEnabled: Colors.black,
-    fgDisabled: Colors.white,
-    fgEnabled: Colors.white,
+    bgDisabled: const Color.fromRGBO(158, 158, 158, 1),
+    bgEnabled: const Color.fromRGBO(0, 0, 0, 1),
+    fgDisabled: const Color.fromRGBO(255, 255, 255, 1),
+    fgEnabled: const Color.fromRGBO(255, 255, 255, 1),
   ),
   elevatedButtonTheme: buildElevatedButtonThemeData(
-    onPrimary: Colors.white,
-    primary: Colors.black,
+    onPrimary: const Color.fromRGBO(255, 255, 255, 1),
+    primary: const Color.fromRGBO(0, 0, 0, 1),
   ),
-  toggleableActiveColor: Colors.green[400],
-  scaffoldBackgroundColor: Colors.white,
-  backgroundColor: Colors.white,
+  toggleableActiveColor: const Color.fromRGBO(102, 187, 106, 1),
+  scaffoldBackgroundColor: const Color.fromRGBO(255, 255, 255, 1),
+  backgroundColor: const Color.fromRGBO(255, 255, 255, 1),
   appBarTheme: const AppBarTheme().copyWith(
     backgroundColor: Colors.white,
     foregroundColor: Colors.black,
@@ -37,7 +37,7 @@ final lightThemeData = ThemeData(
     elevation: 0,
   ),
   //https://api.flutter.dev/flutter/material/TextTheme-class.html
-  textTheme: _buildTextTheme(Colors.black),
+  textTheme: _buildTextTheme(const Color.fromRGBO(0, 0, 0, 1)),
   primaryTextTheme: const TextTheme().copyWith(
     bodyText2: const TextStyle(color: Colors.yellow),
     bodyText1: const TextStyle(color: Colors.orange),
@@ -72,13 +72,13 @@ final lightThemeData = ThemeData(
     ),
     fillColor: MaterialStateProperty.resolveWith((states) {
       return states.contains(MaterialState.selected)
-          ? Colors.black
-          : Colors.white;
+          ? const Color.fromRGBO(0, 0, 0, 1)
+          : const Color.fromRGBO(255, 255, 255, 1);
     }),
     checkColor: MaterialStateProperty.resolveWith((states) {
       return states.contains(MaterialState.selected)
-          ? Colors.white
-          : Colors.black;
+          ? const Color.fromRGBO(255, 255, 255, 1)
+          : const Color.fromRGBO(0, 0, 0, 1);
     }),
   ),
 );
@@ -86,30 +86,30 @@ final lightThemeData = ThemeData(
 final darkThemeData = ThemeData(
   fontFamily: 'Inter',
   brightness: Brightness.dark,
-  primaryColorLight: Colors.white70,
+  primaryColorLight: const Color.fromRGBO(255, 255, 255, 0.702),
   iconTheme: const IconThemeData(color: Colors.white),
   primaryIconTheme:
       const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
-  hintColor: Colors.grey,
+  hintColor: const Color.fromRGBO(158, 158, 158, 1),
   colorScheme: const ColorScheme.dark(primary: Colors.white),
   accentColor: const Color.fromRGBO(45, 194, 98, 0.2),
   buttonTheme: const ButtonThemeData().copyWith(
     buttonColor: const Color.fromRGBO(45, 194, 98, 1.0),
   ),
-  textTheme: _buildTextTheme(Colors.white),
-  toggleableActiveColor: Colors.green[400],
+  textTheme: _buildTextTheme(const Color.fromRGBO(255, 255, 255, 1)),
+  toggleableActiveColor: const Color.fromRGBO(102, 187, 106, 1),
   outlinedButtonTheme: buildOutlinedButtonThemeData(
-    bgDisabled: Colors.grey.shade500,
-    bgEnabled: Colors.white,
-    fgDisabled: Colors.white,
-    fgEnabled: Colors.black,
+    bgDisabled: const Color.fromRGBO(158, 158, 158, 1),
+    bgEnabled: const Color.fromRGBO(255, 255, 255, 1),
+    fgDisabled: const Color.fromRGBO(255, 255, 255, 1),
+    fgEnabled: const Color.fromRGBO(0, 0, 0, 1),
   ),
   elevatedButtonTheme: buildElevatedButtonThemeData(
-    onPrimary: Colors.black,
-    primary: Colors.white,
+    onPrimary: const Color.fromRGBO(0, 0, 0, 1),
+    primary: const Color.fromRGBO(255, 255, 255, 1),
   ),
-  scaffoldBackgroundColor: Colors.black,
-  backgroundColor: Colors.black,
+  scaffoldBackgroundColor: const Color.fromRGBO(0, 0, 0, 1),
+  backgroundColor: const Color.fromRGBO(0, 0, 0, 1),
   appBarTheme: const AppBarTheme().copyWith(
     color: Colors.black,
     elevation: 0,
@@ -144,16 +144,16 @@ final darkThemeData = ThemeData(
     ),
     fillColor: MaterialStateProperty.resolveWith((states) {
       if (states.contains(MaterialState.selected)) {
-        return Colors.grey;
+        return const Color.fromRGBO(158, 158, 158, 1);
       } else {
-        return Colors.black;
+        return const Color.fromRGBO(0, 0, 0, 1);
       }
     }),
     checkColor: MaterialStateProperty.resolveWith((states) {
       if (states.contains(MaterialState.selected)) {
-        return Colors.black;
+        return const Color.fromRGBO(0, 0, 0, 1);
       } else {
-        return Colors.grey;
+        return const Color.fromRGBO(158, 158, 158, 1);
       }
     }),
   ),
@@ -220,19 +220,16 @@ TextTheme _buildTextTheme(Color textColor) {
 
 extension CustomColorScheme on ColorScheme {
   Color get defaultBackgroundColor =>
-      brightness == Brightness.light ? Colors.white : Colors.black;
+      brightness == Brightness.light ? backgroundBaseLight : backgroundBaseDark;
+
+  Color get inverseBackgroundColor =>
+      brightness != Brightness.light ? backgroundBaseLight : backgroundBaseDark;
 
   Color get defaultTextColor =>
-      brightness == Brightness.light ? Colors.black : Colors.white;
+      brightness == Brightness.light ? textBaseLight : textBaseDark;
 
   Color get inverseTextColor =>
-      brightness == Brightness.light ? Colors.white : Colors.black;
-
-  Color get inverseIconColor =>
-      brightness == Brightness.light ? Colors.white : Colors.black;
-
-  Color get inverseBackgroundColor =>
-      brightness == Brightness.light ? Colors.black : Colors.white;
+      brightness != Brightness.light ? textBaseLight : textBaseDark;
 
   Color get boxSelectColor => brightness == Brightness.light
       ? const Color.fromRGBO(67, 186, 108, 1)
@@ -244,13 +241,15 @@ extension CustomColorScheme on ColorScheme {
 
   Color get greenAlternative => const Color.fromRGBO(45, 194, 98, 1.0);
 
-  Color get dynamicFABBackgroundColor =>
-      brightness == Brightness.light ? Colors.black : Colors.grey[850];
+  Color get dynamicFABBackgroundColor => brightness == Brightness.light
+      ? const Color.fromRGBO(0, 0, 0, 1)
+      : const Color.fromRGBO(48, 48, 48, 1);
 
-  Color get dynamicFABTextColor => Colors.white; //same for both themes
+  Color get dynamicFABTextColor =>
+      const Color.fromRGBO(255, 255, 255, 1); //same for both themes
 
   // todo: use brightness == Brightness.light for changing color for dark/light theme
-  ButtonStyle get optionalActionButtonStyle => buildElevatedButtonThemeData(
+  ButtonStyle? get optionalActionButtonStyle => buildElevatedButtonThemeData(
         onPrimary: const Color(0xFF777777),
         primary: const Color(0xFFF0F0F0),
         elevation: 0,
@@ -265,18 +264,18 @@ extension CustomColorScheme on ColorScheme {
       : const Color.fromRGBO(48, 48, 48, 0.5);
 
   Color get iconColor => brightness == Brightness.light
-      ? Colors.black.withOpacity(0.75)
-      : Colors.white;
+      ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.75)
+      : const Color.fromRGBO(255, 255, 255, 1);
 
   Color get bgColorForQuestions => brightness == Brightness.light
-      ? Colors.white
+      ? const Color.fromRGBO(255, 255, 255, 1)
       : const Color.fromRGBO(10, 15, 15, 1.0);
 
   Color get greenText => const Color.fromARGB(255, 40, 190, 113);
 
   Color get cupertinoPickerTopColor => brightness == Brightness.light
       ? const Color.fromARGB(255, 238, 238, 238)
-      : Colors.white.withOpacity(0.1);
+      : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.1);
 
   DatePickerTheme get dateTimePickertheme => brightness == Brightness.light
       ? const DatePickerTheme(
@@ -315,23 +314,24 @@ extension CustomColorScheme on ColorScheme {
       : const Color.fromRGBO(20, 20, 20, 1);
 
   Color get galleryThumbDrawColor => brightness == Brightness.light
-      ? Colors.black.withOpacity(0.8)
-      : Colors.white.withOpacity(0.5);
+      ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.8)
+      : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.5);
 
   Color get backupEnabledBgColor => brightness == Brightness.light
       ? const Color.fromRGBO(230, 230, 230, 0.95)
       : const Color.fromRGBO(10, 40, 40, 0.3);
 
   Color get dotsIndicatorActiveColor => brightness == Brightness.light
-      ? Colors.black.withOpacity(0.5)
-      : Colors.white.withOpacity(0.5);
+      ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.5)
+      : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.5);
 
   Color get dotsIndicatorInactiveColor => brightness == Brightness.light
-      ? Colors.black.withOpacity(0.12)
-      : Colors.white.withOpacity(0.12);
+      ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.12)
+      : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.12);
 
-  Color get toastTextColor =>
-      brightness == Brightness.light ? Colors.white : Colors.black;
+  Color get toastTextColor => brightness == Brightness.light
+      ? const Color.fromRGBO(255, 255, 255, 1)
+      : const Color.fromRGBO(0, 0, 0, 1);
 
   Color get toastBackgroundColor => brightness == Brightness.light
       ? const Color.fromRGBO(24, 24, 24, 0.95)
@@ -341,16 +341,9 @@ extension CustomColorScheme on ColorScheme {
       ? const Color.fromRGBO(180, 180, 180, 1)
       : const Color.fromRGBO(100, 100, 100, 1);
 
-  Color get themeSwitchIndicatorColor => brightness == Brightness.light
-      ? Colors.black.withOpacity(0.75)
-      : Colors.white;
-
-  Color get themeSwitchActiveIconColor =>
-      brightness == Brightness.light ? Colors.white : Colors.black;
-
   Color get themeSwitchInactiveIconColor => brightness == Brightness.light
-      ? Colors.black.withOpacity(0.5)
-      : Colors.white.withOpacity(0.5);
+      ? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.5)
+      : const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.5);
 
   Color get searchResultsColor => brightness == Brightness.light
       ? const Color.fromRGBO(245, 245, 245, 1.0)
@@ -364,35 +357,15 @@ extension CustomColorScheme on ColorScheme {
       ? Colors.black.withOpacity(0.32)
       : Colors.black.withOpacity(0.64);
 
-  Color get fillFaint => brightness == Brightness.light
-      ? Colors.white.withOpacity(0.04)
-      : Colors.black.withOpacity(0.12);
-  Color get warning500 => const Color.fromRGBO(255, 101, 101, 1);
-
-  List<BoxShadow> get shadowMenu => brightness == Brightness.light
-      ? [
-          BoxShadow(blurRadius: 6, color: Colors.white.withOpacity(0.16)),
-          BoxShadow(
-            blurRadius: 6,
-            color: Colors.white.withOpacity(0.12),
-            offset: const Offset(0, 3),
-          ),
-        ]
-      : [
-          BoxShadow(blurRadius: 6, color: Colors.black.withOpacity(0.50)),
-          BoxShadow(
-            blurRadius: 6,
-            color: Colors.black.withOpacity(0.25),
-            offset: const Offset(0, 3),
-          ),
-        ];
+  EnteTheme get enteTheme =>
+      brightness == Brightness.light ? lightTheme : darkTheme;
 }
 
 OutlinedButtonThemeData buildOutlinedButtonThemeData({
-  Color bgDisabled,
-  Color bgEnabled,
-  Color fgDisabled,
-  Color fgEnabled,
+  required Color bgDisabled,
+  required Color bgEnabled,
+  required Color fgDisabled,
+  required Color fgEnabled,
 }) {
   return OutlinedButtonThemeData(
     style: OutlinedButton.styleFrom(
@@ -429,8 +402,8 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData({
 }
 
 ElevatedButtonThemeData buildElevatedButtonThemeData({
-  @required Color onPrimary, // text button color
-  @required Color primary,
+  required Color onPrimary, // text button color
+  required Color primary,
   double elevation = 2, // background color of button
 }) {
   return ElevatedButtonThemeData(

+ 8 - 0
lib/events/local_import_progress.dart

@@ -0,0 +1,8 @@
+import 'package:photos/events/event.dart';
+
+class LocalImportProgressEvent extends Event {
+  final String folderName;
+  final int count;
+
+  LocalImportProgressEvent(this.folderName, this.count);
+}

+ 5 - 12
lib/events/sync_status_update_event.dart

@@ -1,15 +1,13 @@
-// @dart=2.9
-
 import 'package:photos/events/event.dart';
 
 class SyncStatusUpdate extends Event {
-  final int completed;
-  final int total;
-  final bool wasStopped;
   final SyncStatus status;
+  final int? completed;
+  final int? total;
+  final bool wasStopped;
   final String reason;
-  final Error error;
-  int timestamp;
+  final Error? error;
+  late int timestamp;
 
   SyncStatusUpdate(
     this.status, {
@@ -21,11 +19,6 @@ class SyncStatusUpdate extends Event {
   }) {
     timestamp = DateTime.now().microsecondsSinceEpoch;
   }
-
-  @override
-  String toString() {
-    return 'SyncStatusUpdate(completed: $completed, total: $total, wasStopped: $wasStopped, status: $status, reason: $reason, error: $error)';
-  }
 }
 
 enum SyncStatus {

+ 0 - 7
lib/events/tab_changed_event.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:photos/events/event.dart';
 
 class TabChangedEvent extends Event {
@@ -10,11 +8,6 @@ class TabChangedEvent extends Event {
     this.selectedIndex,
     this.source,
   );
-
-  @override
-  String toString() {
-    return 'TabChangedEvent{selectedIndex: $selectedIndex, source: $source}';
-  }
 }
 
 enum TabChangedEventSource {

+ 2 - 2
lib/main.dart

@@ -177,8 +177,8 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
       body: function,
       logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
       maxLogFiles: 5,
-      sentryDsn: kDebugMode ? kSentryDebugDSN : kSentryDSN,
-      tunnel: kSentryTunnel,
+      sentryDsn: kDebugMode ? sentryDebugDSN : sentryDSN,
+      tunnel: sentryTunnel,
       enableInDebugMode: true,
       prefix: prefix,
     ),

+ 0 - 2
lib/models/backup_status.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 class BackupStatus {
   final List<String> localIDs;
   final int size;

+ 17 - 137
lib/models/billing_plan.dart

@@ -1,36 +1,22 @@
-// @dart=2.9
-
 import 'dart:convert';
 
-import 'package:flutter/foundation.dart';
-
 class BillingPlans {
   final List<BillingPlan> plans;
   final FreePlan freePlan;
 
   BillingPlans({
-    this.plans,
-    this.freePlan,
+    required this.plans,
+    required this.freePlan,
   });
 
-  BillingPlans copyWith({
-    List<BillingPlan> plans,
-    FreePlan freePlan,
-  }) {
-    return BillingPlans(
-      plans: plans ?? this.plans,
-      freePlan: freePlan ?? this.freePlan,
-    );
-  }
-
   Map<String, dynamic> toMap() {
     return {
-      'plans': plans?.map((x) => x?.toMap())?.toList(),
-      'freePlan': freePlan?.toMap(),
+      'plans': plans.map((x) => x.toMap()).toList(),
+      'freePlan': freePlan.toMap(),
     };
   }
 
-  factory BillingPlans.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
 
     return BillingPlans(
@@ -41,25 +27,8 @@ class BillingPlans {
     );
   }
 
-  String toJson() => json.encode(toMap());
-
   factory BillingPlans.fromJson(String source) =>
       BillingPlans.fromMap(json.decode(source));
-
-  @override
-  String toString() => 'BillingPlans(plans: $plans, freePlan: $freePlan)';
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is BillingPlans &&
-        listEquals(o.plans, plans) &&
-        o.freePlan == freePlan;
-  }
-
-  @override
-  int get hashCode => plans.hashCode ^ freePlan.hashCode;
 }
 
 class FreePlan {
@@ -67,23 +36,11 @@ class FreePlan {
   final int duration;
   final String period;
   FreePlan({
-    this.storage,
-    this.duration,
-    this.period,
+    required this.storage,
+    required this.duration,
+    required this.period,
   });
 
-  FreePlan copyWith({
-    int storage,
-    int duration,
-    String period,
-  }) {
-    return FreePlan(
-      storage: storage ?? this.storage,
-      duration: duration ?? this.duration,
-      period: period ?? this.period,
-    );
-  }
-
   Map<String, dynamic> toMap() {
     return {
       'storage': storage,
@@ -92,7 +49,7 @@ class FreePlan {
     };
   }
 
-  factory FreePlan.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
 
     return FreePlan(
@@ -101,28 +58,6 @@ class FreePlan {
       period: map['period'],
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory FreePlan.fromJson(String source) =>
-      FreePlan.fromMap(json.decode(source));
-
-  @override
-  String toString() =>
-      'FreePlan(storage: $storage, duration: $duration, period: $period)';
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is FreePlan &&
-        o.storage == storage &&
-        o.duration == duration &&
-        o.period == period;
-  }
-
-  @override
-  int get hashCode => storage.hashCode ^ duration.hashCode ^ period.hashCode;
 }
 
 class BillingPlan {
@@ -135,35 +70,15 @@ class BillingPlan {
   final String period;
 
   BillingPlan({
-    this.id,
-    this.androidID,
-    this.iosID,
-    this.stripeID,
-    this.storage,
-    this.price,
-    this.period,
+    required this.id,
+    required this.androidID,
+    required this.iosID,
+    required this.stripeID,
+    required this.storage,
+    required this.price,
+    required this.period,
   });
 
-  BillingPlan copyWith({
-    String id,
-    String androidID,
-    String iosID,
-    String stripeID,
-    int storage,
-    String price,
-    String period,
-  }) {
-    return BillingPlan(
-      id: id ?? this.id,
-      androidID: androidID ?? this.androidID,
-      iosID: iosID ?? this.iosID,
-      stripeID: stripeID ?? this.stripeID,
-      storage: storage ?? this.storage,
-      price: price ?? this.price,
-      period: period ?? this.period,
-    );
-  }
-
   Map<String, dynamic> toMap() {
     return {
       'id': id,
@@ -176,7 +91,7 @@ class BillingPlan {
     };
   }
 
-  factory BillingPlan.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
 
     return BillingPlan(
@@ -189,39 +104,4 @@ class BillingPlan {
       period: map['period'],
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory BillingPlan.fromJson(String source) =>
-      BillingPlan.fromMap(json.decode(source));
-
-  @override
-  String toString() {
-    return 'BillingPlan(id: $id, androidID: $androidID, iosID: $iosID, stripeID: $stripeID, storage: $storage, price: $price, period: $period)';
-  }
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is BillingPlan &&
-        o.id == id &&
-        o.androidID == androidID &&
-        o.iosID == iosID &&
-        o.stripeID == stripeID &&
-        o.storage == storage &&
-        o.price == price &&
-        o.period == period;
-  }
-
-  @override
-  int get hashCode {
-    return id.hashCode ^
-        androidID.hashCode ^
-        iosID.hashCode ^
-        stripeID.hashCode ^
-        storage.hashCode ^
-        price.hashCode ^
-        period.hashCode;
-  }
 }

+ 43 - 182
lib/models/collection.dart

@@ -1,28 +1,25 @@
-// @dart=2.9
-
 import 'dart:convert';
 import 'dart:core';
 
-import 'package:flutter/foundation.dart';
 import 'package:photos/models/magic_metadata.dart';
 
 class Collection {
   final int id;
-  final User owner;
+  final User? owner;
   final String encryptedKey;
-  final String keyDecryptionNonce;
-  final String name;
+  final String? keyDecryptionNonce;
+  final String? name;
   final String encryptedName;
   final String nameDecryptionNonce;
   final CollectionType type;
   final CollectionAttributes attributes;
-  final List<User> sharees;
-  final List<PublicURL> publicURLs;
+  final List<User?>? sharees;
+  final List<PublicURL?>? publicURLs;
   final int updationTime;
   final bool isDeleted;
-  String mMdEncodedJson;
+  String? mMdEncodedJson;
   int mMdVersion = 0;
-  CollectionMagicMetadata _mmd;
+  CollectionMagicMetadata? _mmd;
 
   CollectionMagicMetadata get magicMetadata =>
       _mmd ?? CollectionMagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
@@ -46,7 +43,7 @@ class Collection {
   });
 
   bool isArchived() {
-    return mMdVersion > 0 && magicMetadata.visibility == kVisibilityArchive;
+    return mMdVersion > 0 && magicMetadata.visibility == visibilityArchive;
   }
 
   static CollectionType typeFromString(String type) {
@@ -71,21 +68,21 @@ class Collection {
   }
 
   Collection copyWith({
-    int id,
-    User owner,
-    String encryptedKey,
-    String keyDecryptionNonce,
-    String name,
-    String encryptedName,
-    String nameDecryptionNonce,
-    CollectionType type,
-    CollectionAttributes attributes,
-    List<User> sharees,
-    List<PublicURL> publicURLs,
-    int updationTime,
-    bool isDeleted,
-    String mMdEncodedJson,
-    int mMdVersion,
+    int? id,
+    User? owner,
+    String? encryptedKey,
+    String? keyDecryptionNonce,
+    String? name,
+    String? encryptedName,
+    String? nameDecryptionNonce,
+    CollectionType? type,
+    CollectionAttributes? attributes,
+    List<User>? sharees,
+    List<PublicURL>? publicURLs,
+    int? updationTime,
+    bool? isDeleted,
+    String? mMdEncodedJson,
+    int? mMdVersion,
   }) {
     final Collection result = Collection(
       id ?? this.id,
@@ -117,15 +114,15 @@ class Collection {
       'encryptedName': encryptedName,
       'nameDecryptionNonce': nameDecryptionNonce,
       'type': typeToString(type),
-      'attributes': attributes?.toMap(),
-      'sharees': sharees?.map((x) => x?.toMap())?.toList(),
-      'publicURLs': publicURLs?.map((x) => x?.toMap())?.toList(),
+      'attributes': attributes.toMap(),
+      'sharees': sharees?.map((x) => x?.toMap()).toList(),
+      'publicURLs': publicURLs?.map((x) => x?.toMap()).toList(),
       'updationTime': updationTime,
       'isDeleted': isDeleted,
     };
   }
 
-  factory Collection.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
     final sharees = (map['sharees'] == null || map['sharees'].length == 0)
         ? <User>[]
@@ -152,53 +149,6 @@ class Collection {
       isDeleted: map['isDeleted'] ?? false,
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory Collection.fromJson(String source) =>
-      Collection.fromMap(json.decode(source));
-
-  @override
-  String toString() {
-    return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, encryptedName: $encryptedName, nameDecryptionNonce: $nameDecryptionNonce, type: $type, attributes: $attributes, sharees: $sharees, publicURLs: $publicURLs, updationTime: $updationTime, isDeleted: $isDeleted)';
-  }
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is Collection &&
-        o.id == id &&
-        o.owner == owner &&
-        o.encryptedKey == encryptedKey &&
-        o.keyDecryptionNonce == keyDecryptionNonce &&
-        o.name == name &&
-        o.encryptedName == encryptedName &&
-        o.nameDecryptionNonce == nameDecryptionNonce &&
-        o.type == type &&
-        o.attributes == attributes &&
-        listEquals(o.sharees, sharees) &&
-        listEquals(o.publicURLs, publicURLs) &&
-        o.updationTime == updationTime &&
-        o.isDeleted == isDeleted;
-  }
-
-  @override
-  int get hashCode {
-    return id.hashCode ^
-        owner.hashCode ^
-        encryptedKey.hashCode ^
-        keyDecryptionNonce.hashCode ^
-        name.hashCode ^
-        encryptedName.hashCode ^
-        nameDecryptionNonce.hashCode ^
-        type.hashCode ^
-        attributes.hashCode ^
-        sharees.hashCode ^
-        publicURLs.hashCode ^
-        updationTime.hashCode ^
-        isDeleted.hashCode;
-  }
 }
 
 enum CollectionType {
@@ -208,9 +158,9 @@ enum CollectionType {
 }
 
 class CollectionAttributes {
-  final String encryptedPath;
-  final String pathDecryptionNonce;
-  final int version;
+  final String? encryptedPath;
+  final String? pathDecryptionNonce;
+  final int? version;
 
   CollectionAttributes({
     this.encryptedPath,
@@ -218,18 +168,6 @@ class CollectionAttributes {
     this.version,
   });
 
-  CollectionAttributes copyWith({
-    String encryptedPath,
-    String pathDecryptionNonce,
-    int version,
-  }) {
-    return CollectionAttributes(
-      encryptedPath: encryptedPath ?? this.encryptedPath,
-      pathDecryptionNonce: pathDecryptionNonce ?? this.pathDecryptionNonce,
-      version: version ?? this.version,
-    );
-  }
-
   Map<String, dynamic> toMap() {
     final map = <String, dynamic>{};
     if (encryptedPath != null) {
@@ -242,7 +180,7 @@ class CollectionAttributes {
     return map;
   }
 
-  factory CollectionAttributes.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
 
     return CollectionAttributes(
@@ -251,54 +189,19 @@ class CollectionAttributes {
       version: map['version'] ?? 0,
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory CollectionAttributes.fromJson(String source) =>
-      CollectionAttributes.fromMap(json.decode(source));
-
-  @override
-  String toString() =>
-      'CollectionAttributes(encryptedPath: $encryptedPath, pathDecryptionNonce: $pathDecryptionNonce, version: $version)';
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is CollectionAttributes &&
-        o.encryptedPath == encryptedPath &&
-        o.pathDecryptionNonce == pathDecryptionNonce &&
-        o.version == version;
-  }
-
-  @override
-  int get hashCode =>
-      encryptedPath.hashCode ^ pathDecryptionNonce.hashCode ^ version.hashCode;
 }
 
 class User {
-  int id;
+  int? id;
   String email;
-  String name;
+  String? name;
 
   User({
     this.id,
-    this.email,
+    required this.email,
     this.name,
   });
 
-  User copyWith({
-    int id,
-    String email,
-    String name,
-  }) {
-    return User(
-      id: id ?? this.id,
-      email: email ?? this.email,
-      name: name ?? this.name,
-    );
-  }
-
   Map<String, dynamic> toMap() {
     return {
       'id': id,
@@ -307,7 +210,7 @@ class User {
     };
   }
 
-  factory User.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
 
     return User(
@@ -320,34 +223,21 @@ class User {
   String toJson() => json.encode(toMap());
 
   factory User.fromJson(String source) => User.fromMap(json.decode(source));
-
-  @override
-  String toString() => 'CollectionOwner(id: $id, email: $email, name: $name)';
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is User && o.id == id && o.email == email && o.name == name;
-  }
-
-  @override
-  int get hashCode => id.hashCode ^ email.hashCode ^ name.hashCode;
 }
 
 class PublicURL {
   String url;
   int deviceLimit;
   int validTill;
-  bool enableDownload = true;
-  bool passwordEnabled = false;
+  bool enableDownload;
+  bool passwordEnabled;
 
   PublicURL({
-    this.url,
-    this.deviceLimit,
-    this.validTill,
-    this.enableDownload,
-    this.passwordEnabled,
+    required this.url,
+    required this.deviceLimit,
+    required this.validTill,
+    this.enableDownload = true,
+    this.passwordEnabled = false,
   });
 
   Map<String, dynamic> toMap() {
@@ -360,7 +250,7 @@ class PublicURL {
     };
   }
 
-  factory PublicURL.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
 
     return PublicURL(
@@ -371,33 +261,4 @@ class PublicURL {
       passwordEnabled: map['passwordEnabled'] ?? false,
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory PublicURL.fromJson(String source) =>
-      PublicURL.fromMap(json.decode(source));
-
-  @override
-  String toString() =>
-      'PublicUrl( url: $url, deviceLimit: $deviceLimit, validTill: $validTill, , enableDownload: $enableDownload, , passwordEnabled: $passwordEnabled)';
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is PublicURL &&
-        o.deviceLimit == deviceLimit &&
-        o.url == url &&
-        o.validTill == validTill &&
-        o.enableDownload == enableDownload &&
-        o.passwordEnabled == passwordEnabled;
-  }
-
-  @override
-  int get hashCode =>
-      deviceLimit.hashCode ^
-      url.hashCode ^
-      validTill.hashCode ^
-      enableDownload.hashCode ^
-      passwordEnabled.hashCode;
 }

+ 4 - 6
lib/models/collection_file_item.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 
 class CollectionFileItem {
@@ -14,9 +12,9 @@ class CollectionFileItem {
   );
 
   CollectionFileItem copyWith({
-    int id,
-    String encryptedKey,
-    String keyDecryptionNonce,
+    int? id,
+    String? encryptedKey,
+    String? keyDecryptionNonce,
   }) {
     return CollectionFileItem(
       id ?? this.id,
@@ -33,7 +31,7 @@ class CollectionFileItem {
     };
   }
 
-  factory CollectionFileItem.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
 
     return CollectionFileItem(

+ 1 - 11
lib/models/collection_items.dart

@@ -1,19 +1,9 @@
-// @dart=2.9
-
 import 'package:photos/models/collection.dart';
-import 'package:photos/models/device_folder.dart';
 import 'package:photos/models/file.dart';
 
-class CollectionItems {
-  final List<DeviceFolder> folders;
-  final List<CollectionWithThumbnail> collections;
-
-  CollectionItems(this.folders, this.collections);
-}
-
 class CollectionWithThumbnail {
   final Collection collection;
-  final File thumbnail;
+  final File? thumbnail;
 
   CollectionWithThumbnail(
     this.collection,

+ 2 - 6
lib/models/delete_account.dart

@@ -1,13 +1,9 @@
-// @dart=2.9
-
-import 'package:flutter/foundation.dart';
-
 class DeleteChallengeResponse {
   final bool allowDelete;
   final String encryptedChallenge;
 
   DeleteChallengeResponse({
-    @required this.allowDelete,
-    this.encryptedChallenge,
+    required this.allowDelete,
+    required this.encryptedChallenge,
   });
 }

+ 3 - 4
lib/models/derived_key_result.dart

@@ -1,10 +1,9 @@
-// @dart=2.9
-
 import 'dart:typed_data';
 
 class DerivedKeyResult {
   final Uint8List key;
-  final Uint8List salt;
+  final int memLimit;
+  final int opsLimit;
 
-  DerivedKeyResult(this.key, this.salt);
+  DerivedKeyResult(this.key, this.memLimit, this.opsLimit);
 }

+ 24 - 0
lib/models/device_collection.dart

@@ -0,0 +1,24 @@
+import 'package:photos/models/file.dart';
+import 'package:photos/models/upload_strategy.dart';
+
+class DeviceCollection {
+  final String id;
+  final String name;
+  final int count;
+  final bool shouldBackup;
+  UploadStrategy uploadStrategy;
+  final String? coverId;
+  int? collectionID;
+  File? thumbnail;
+
+  DeviceCollection(
+    this.id,
+    this.name, {
+    this.coverId,
+    this.count = 0,
+    this.collectionID,
+    this.thumbnail,
+    this.uploadStrategy = UploadStrategy.ifMissing,
+    this.shouldBackup = false,
+  });
+}

+ 0 - 15
lib/models/device_folder.dart

@@ -1,15 +0,0 @@
-// @dart=2.9
-
-import 'package:photos/models/file.dart';
-
-class DeviceFolder {
-  final String name;
-  final String path;
-  final File thumbnail;
-
-  DeviceFolder(
-    this.name,
-    this.path,
-    this.thumbnail,
-  );
-}

+ 2 - 2
lib/models/duplicate_files.dart

@@ -58,9 +58,9 @@ class DuplicateFiles {
   sortByCollectionName() {
     files.sort((first, second) {
       final firstName =
-          collectionsService.getCollectionNameByID(first.collectionID);
+          collectionsService.getCollectionByID(first.collectionID).name;
       final secondName =
-          collectionsService.getCollectionNameByID(second.collectionID);
+          collectionsService.getCollectionByID(second.collectionID).name;
       return firstName.compareTo(secondName);
     });
   }

+ 10 - 7
lib/models/encryption_result.dart

@@ -1,12 +1,15 @@
-// @dart=2.9
-
 import 'dart:typed_data';
 
 class EncryptionResult {
-  final Uint8List encryptedData;
-  final Uint8List key;
-  final Uint8List header;
-  final Uint8List nonce;
+  final Uint8List? encryptedData;
+  final Uint8List? key;
+  final Uint8List? header;
+  final Uint8List? nonce;
 
-  EncryptionResult({this.encryptedData, this.key, this.header, this.nonce});
+  EncryptionResult({
+    this.encryptedData,
+    this.key,
+    this.header,
+    this.nonce,
+  });
 }

+ 0 - 6
lib/models/ente_file.dart

@@ -1,14 +1,8 @@
 // EnteFile is base file entry for various type of files
 // like DeviceFile,RemoteFile or TrashedFile
 
-// @dart=2.9
-
 abstract class EnteFile {
   // returns cacheKey which should be used while caching entry related to
   // this file.
   String cacheKey();
-
-  // returns localIdentifier for the file on the host OS.
-  // Can be null if the file only exist on remote
-  String localIdentifier();
 }

+ 60 - 59
lib/models/file.dart

@@ -1,5 +1,4 @@
-// @dart=2.9
-
+import 'package:logging/logging.dart';
 import 'package:path/path.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/configuration.dart';
@@ -8,37 +7,41 @@ import 'package:photos/models/ente_file.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/location.dart';
 import 'package:photos/models/magic_metadata.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/feature_flag_service.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/utils/exif_util.dart';
+// ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/utils/file_uploader_util.dart';
 
 class File extends EnteFile {
-  int generatedID;
-  int uploadedFileID;
-  int ownerID;
-  int collectionID;
-  String localID;
-  String title;
-  String deviceFolder;
-  int creationTime;
-  int modificationTime;
-  int updationTime;
-  Location location;
-  FileType fileType;
-  int fileSubType;
-  int duration;
-  String exif;
-  String hash;
-  int metadataVersion;
-  String encryptedKey;
-  String keyDecryptionNonce;
-  String fileDecryptionHeader;
-  String thumbnailDecryptionHeader;
-  String metadataDecryptionHeader;
+  int? generatedID;
+  int? uploadedFileID;
+  int? ownerID;
+  int? collectionID;
+  String? localID;
+  String? title;
+  String? deviceFolder;
+  int? creationTime;
+  int? modificationTime;
+  int? updationTime;
+  Location? location;
+  late FileType fileType;
+  int? fileSubType;
+  int? duration;
+  String? exif;
+  String? hash;
+  int? metadataVersion;
+  String? encryptedKey;
+  String? keyDecryptionNonce;
+  String? fileDecryptionHeader;
+  String? thumbnailDecryptionHeader;
+  String? metadataDecryptionHeader;
+  int? fileSize;
 
-  String mMdEncodedJson;
+  String? mMdEncodedJson;
   int mMdVersion = 0;
-  MagicMetadata _mmd;
+  MagicMetadata? _mmd;
 
   MagicMetadata get magicMetadata =>
       _mmd ?? MagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
@@ -46,11 +49,11 @@ class File extends EnteFile {
   set magicMetadata(val) => _mmd = val;
 
   // public magic metadata is shared if during file/album sharing
-  String pubMmdEncodedJson;
+  String? pubMmdEncodedJson;
   int pubMmdVersion = 0;
-  PubMagicMetadata _pubMmd;
+  PubMagicMetadata? _pubMmd;
 
-  PubMagicMetadata get pubMagicMetadata =>
+  PubMagicMetadata? get pubMagicMetadata =>
       _pubMmd ?? PubMagicMetadata.fromEncodedJson(pubMmdEncodedJson ?? '{}');
 
   set pubMagicMetadata(val) => _pubMmd = val;
@@ -59,6 +62,8 @@ class File extends EnteFile {
   // in V2: LivePhoto hash is stored as imgHash:vidHash
   static const kCurrentMetadataVersion = 2;
 
+  static final _logger = Logger('File');
+
   File();
 
   static Future<File> fromAsset(String pathName, AssetEntity asset) async {
@@ -72,7 +77,7 @@ class File extends EnteFile {
     if (file.creationTime == 0) {
       try {
         final parsedDateTime = DateTime.parse(
-          basenameWithoutExtension(file.title)
+          basenameWithoutExtension(file.title!)
               .replaceAll("IMG_", "")
               .replaceAll("VID_", "")
               .replaceAll("DCIM_", "")
@@ -112,11 +117,11 @@ class File extends EnteFile {
     return type;
   }
 
-  Future<AssetEntity> getAsset() {
+  Future<AssetEntity?> get getAsset {
     if (localID == null) {
       return Future.value(null);
     }
-    return AssetEntity.fromId(localID);
+    return AssetEntity.fromId(localID!);
   }
 
   void applyMetadata(Map<String, dynamic> metadata) {
@@ -152,7 +157,7 @@ class File extends EnteFile {
   Future<Map<String, dynamic>> getMetadataForUpload(
     MediaUploadData mediaUploadData,
   ) async {
-    final asset = await getAsset();
+    final asset = await getAsset;
     // asset can be null for files shared to app
     if (asset != null) {
       fileSubType = asset.subtype;
@@ -168,22 +173,22 @@ class File extends EnteFile {
       }
     }
     hash = mediaUploadData.hashData?.fileHash;
-    return getMetadata();
+    return metadata;
   }
 
-  Map<String, dynamic> getMetadata() {
+  Map<String, dynamic> get metadata {
     final metadata = <String, dynamic>{};
-    metadata["localID"] = isSharedMediaToAppSandbox() ? null : localID;
+    metadata["localID"] = isSharedMediaToAppSandbox ? null : localID;
     metadata["title"] = title;
     metadata["deviceFolder"] = deviceFolder;
     metadata["creationTime"] = creationTime;
     metadata["modificationTime"] = modificationTime;
     metadata["fileType"] = fileType.index;
     if (location != null &&
-        location.latitude != null &&
-        location.longitude != null) {
-      metadata["latitude"] = location.latitude;
-      metadata["longitude"] = location.longitude;
+        location!.latitude != null &&
+        location!.longitude != null) {
+      metadata["latitude"] = location!.latitude;
+      metadata["longitude"] = location!.longitude;
     }
     if (fileSubType != null) {
       metadata["subType"] = fileSubType;
@@ -200,7 +205,7 @@ class File extends EnteFile {
     return metadata;
   }
 
-  String getDownloadUrl() {
+  String get downloadUrl {
     final endpoint = Configuration.instance.getHttpEndpoint();
     if (endpoint != kDefaultProductionEndpoint ||
         FeatureFlagService.instance.disableCFWorker()) {
@@ -210,7 +215,7 @@ class File extends EnteFile {
     }
   }
 
-  String getThumbnailUrl() {
+  String get thumbnailUrl {
     final endpoint = Configuration.instance.getHttpEndpoint();
     if (endpoint != kDefaultProductionEndpoint ||
         FeatureFlagService.instance.disableCFWorker()) {
@@ -220,27 +225,28 @@ class File extends EnteFile {
     }
   }
 
-  String getDisplayName() {
-    if (pubMagicMetadata != null && pubMagicMetadata.editedName != null) {
-      return pubMagicMetadata.editedName;
+  String get displayName {
+    if (pubMagicMetadata != null && pubMagicMetadata!.editedName != null) {
+      return pubMagicMetadata!.editedName!;
     }
-    return title;
+    if (title == null) _logger.severe('File title is null');
+    return title ?? '';
   }
 
   // returns true if the file isn't available in the user's gallery
-  bool isRemoteFile() {
+  bool get isRemoteFile {
     return localID == null && uploadedFileID != null;
   }
 
-  bool isSharedMediaToAppSandbox() {
+  bool get isSharedMediaToAppSandbox {
     return localID != null &&
-        (localID.startsWith(kOldSharedMediaIdentifier) ||
-            localID.startsWith(kSharedMediaIdentifier));
+        (localID!.startsWith(oldSharedMediaIdentifier) ||
+            localID!.startsWith(sharedMediaIdentifier));
   }
 
-  bool hasLocation() {
+  bool get hasLocation {
     return location != null &&
-        (location.longitude != 0 || location.latitude != 0);
+        (location!.longitude != 0 || location!.latitude != 0);
   }
 
   @override
@@ -265,7 +271,7 @@ class File extends EnteFile {
     return generatedID.hashCode ^ uploadedFileID.hashCode ^ localID.hashCode;
   }
 
-  String tag() {
+  String get tag {
     return "local_" +
         localID.toString() +
         ":remote_" +
@@ -277,11 +283,6 @@ class File extends EnteFile {
   @override
   String cacheKey() {
     // todo: Neeraj: 19thJuly'22: evaluate and add fileHash as the key?
-    return localID ?? uploadedFileID?.toString() ?? generatedID?.toString();
-  }
-
-  @override
-  String localIdentifier() {
-    return localID;
+    return localID ?? uploadedFileID?.toString() ?? generatedID.toString();
   }
 }

+ 0 - 2
lib/models/file_load_result.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:photos/models/file.dart';
 
 class FileLoadResult {

+ 13 - 2
lib/models/file_type.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 enum FileType {
   image,
   video,
@@ -32,3 +30,16 @@ FileType getFileType(int fileType) {
       return FileType.other;
   }
 }
+
+String getHumanReadableString(FileType fileType) {
+  switch (fileType) {
+    case FileType.image:
+      return "Image";
+    case FileType.video:
+      return "Video";
+    case FileType.livePhoto:
+      return "Live Photo";
+    default:
+      return fileType.name.toUpperCase();
+  }
+}

+ 0 - 1
lib/models/filters/gallery_items_filter.dart

@@ -1,5 +1,4 @@
 // @dart=2.9
-
 import 'package:photos/models/file.dart';
 
 class GalleryItemsFilter {

+ 0 - 3
lib/models/gallery_type.dart

@@ -1,11 +1,8 @@
-
-
 enum GalleryType {
   homepage,
   archive,
   trash,
   localFolder,
-  localAll, // used for gallery view displaying all local photos on the device
   // indicator for gallery view of collections shared with the user
   sharedCollection,
   ownedCollection,

+ 7 - 14
lib/models/ignored_file.dart

@@ -1,26 +1,24 @@
-// @dart=2.9
-
 import 'package:photos/models/trash_file.dart';
 
 const kIgnoreReasonTrash = "trash";
 const kIgnoreReasonInvalidFile = "invalidFile";
 
 class IgnoredFile {
-  final String localID;
-  final String title;
-  final String deviceFolder;
+  final String? localID;
+  final String? title;
+  final String? deviceFolder;
   String reason;
 
   IgnoredFile(this.localID, this.title, this.deviceFolder, this.reason);
 
-  factory IgnoredFile.fromTrashItem(TrashFile trashFile) {
+  static fromTrashItem(TrashFile? trashFile) {
     if (trashFile == null) return null;
     if (trashFile.localID == null ||
-        trashFile.localID.isEmpty ||
+        trashFile.localID!.isEmpty ||
         trashFile.title == null ||
-        trashFile.title.isEmpty ||
+        trashFile.title!.isEmpty ||
         trashFile.deviceFolder == null ||
-        trashFile.deviceFolder.isEmpty) {
+        trashFile.deviceFolder!.isEmpty) {
       return null;
     }
 
@@ -31,9 +29,4 @@ class IgnoredFile {
       kIgnoreReasonTrash,
     );
   }
-
-  @override
-  String toString() {
-    return 'IgnoredFile{localID: $localID, title: $title, deviceFolder: $deviceFolder, reason: $reason}';
-  }
 }

+ 12 - 14
lib/models/key_attributes.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 
 class KeyAttributes {
@@ -71,18 +69,18 @@ class KeyAttributes {
       KeyAttributes.fromMap(json.decode(source));
 
   KeyAttributes copyWith({
-    String kekSalt,
-    String encryptedKey,
-    String keyDecryptionNonce,
-    String publicKey,
-    String encryptedSecretKey,
-    String secretKeyDecryptionNonce,
-    int memLimit,
-    int opsLimit,
-    String masterKeyEncryptedWithRecoveryKey,
-    String masterKeyDecryptionNonce,
-    String recoveryKeyEncryptedWithMasterKey,
-    String recoveryKeyDecryptionNonce,
+    String? kekSalt,
+    String? encryptedKey,
+    String? keyDecryptionNonce,
+    String? publicKey,
+    String? encryptedSecretKey,
+    String? secretKeyDecryptionNonce,
+    int? memLimit,
+    int? opsLimit,
+    String? masterKeyEncryptedWithRecoveryKey,
+    String? masterKeyDecryptionNonce,
+    String? recoveryKeyEncryptedWithMasterKey,
+    String? recoveryKeyDecryptionNonce,
   }) {
     return KeyAttributes(
       kekSalt ?? this.kekSalt,

+ 0 - 2
lib/models/key_gen_result.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:photos/models/key_attributes.dart';
 import 'package:photos/models/private_key_attributes.dart';
 

+ 2 - 4
lib/models/location.dart

@@ -1,8 +1,6 @@
-// @dart=2.9
-
 class Location {
-  final double latitude;
-  final double longitude;
+  final double? latitude;
+  final double? longitude;
 
   Location(this.latitude, this.longitude);
 

+ 16 - 37
lib/models/magic_metadata.dart

@@ -1,14 +1,12 @@
-// @dart=2.9
-
 import 'dart:convert';
 
-const kVisibilityVisible = 0;
-const kVisibilityArchive = 1;
+const visibilityVisible = 0;
+const visibilityArchive = 1;
 
-const kMagicKeyVisibility = 'visibility';
+const magicKeyVisibility = 'visibility';
 
-const kPubMagicKeyEditedTime = 'editedTime';
-const kPubMagicKeyEditedName = 'editedName';
+const pubMagicKeyEditedTime = 'editedTime';
+const pubMagicKeyEditedName = 'editedName';
 
 class MagicMetadata {
   // 0 -> visible
@@ -16,30 +14,24 @@ class MagicMetadata {
   // 2 -> hidden etc?
   int visibility;
 
-  MagicMetadata({this.visibility});
+  MagicMetadata({required this.visibility});
 
   factory MagicMetadata.fromEncodedJson(String encodedJson) =>
       MagicMetadata.fromJson(jsonDecode(encodedJson));
 
   factory MagicMetadata.fromJson(dynamic json) => MagicMetadata.fromMap(json);
 
-  Map<String, dynamic> toJson() {
-    final map = <String, dynamic>{};
-    map[kMagicKeyVisibility] = visibility;
-    return map;
-  }
-
-  factory MagicMetadata.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
     return MagicMetadata(
-      visibility: map[kMagicKeyVisibility] ?? kVisibilityVisible,
+      visibility: map[magicKeyVisibility] ?? visibilityVisible,
     );
   }
 }
 
 class PubMagicMetadata {
-  int editedTime;
-  String editedName;
+  int? editedTime;
+  String? editedName;
 
   PubMagicMetadata({this.editedTime, this.editedName});
 
@@ -49,18 +41,11 @@ class PubMagicMetadata {
   factory PubMagicMetadata.fromJson(dynamic json) =>
       PubMagicMetadata.fromMap(json);
 
-  Map<String, dynamic> toJson() {
-    final map = <String, dynamic>{};
-    map[kPubMagicKeyEditedTime] = editedTime;
-    map[kPubMagicKeyEditedName] = editedName;
-    return map;
-  }
-
-  factory PubMagicMetadata.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
     return PubMagicMetadata(
-      editedTime: map[kPubMagicKeyEditedTime],
-      editedName: map[kPubMagicKeyEditedName],
+      editedTime: map[pubMagicKeyEditedTime],
+      editedName: map[pubMagicKeyEditedName],
     );
   }
 }
@@ -71,7 +56,7 @@ class CollectionMagicMetadata {
   // 2 -> hidden etc?
   int visibility;
 
-  CollectionMagicMetadata({this.visibility});
+  CollectionMagicMetadata({required this.visibility});
 
   factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) =>
       CollectionMagicMetadata.fromJson(jsonDecode(encodedJson));
@@ -79,16 +64,10 @@ class CollectionMagicMetadata {
   factory CollectionMagicMetadata.fromJson(dynamic json) =>
       CollectionMagicMetadata.fromMap(json);
 
-  Map<String, dynamic> toJson() {
-    final map = <String, dynamic>{};
-    map[kMagicKeyVisibility] = visibility;
-    return map;
-  }
-
-  factory CollectionMagicMetadata.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
     return CollectionMagicMetadata(
-      visibility: map[kMagicKeyVisibility] ?? kVisibilityVisible,
+      visibility: map[magicKeyVisibility] ?? visibilityVisible,
     );
   }
 }

+ 0 - 2
lib/models/memory.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:photos/models/file.dart';
 
 class Memory {

+ 0 - 2
lib/models/private_key_attributes.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 class PrivateKeyAttributes {
   final String key;
   final String recoveryKey;

+ 0 - 2
lib/models/public_key.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 class PublicKey {
   final String email;
   final String publicKey;

+ 23 - 3
lib/models/search/album_search_result.dart

@@ -1,10 +1,30 @@
-// @dart=2.9
-
 import 'package:photos/models/collection_items.dart';
-import 'package:photos/models/search/search_results.dart';
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/search_result.dart';
 
 class AlbumSearchResult extends SearchResult {
   final CollectionWithThumbnail collectionWithThumbnail;
 
   AlbumSearchResult(this.collectionWithThumbnail);
+
+  @override
+  ResultType type() {
+    return ResultType.collection;
+  }
+
+  @override
+  String name() {
+    return collectionWithThumbnail.collection.name!;
+  }
+
+  @override
+  File previewThumbnail() {
+    return collectionWithThumbnail.thumbnail!;
+  }
+
+  @override
+  List<File> resultFiles() {
+    // for album search result, we should open the album page directly
+    throw UnimplementedError();
+  }
 }

+ 22 - 3
lib/models/search/file_search_result.dart

@@ -1,10 +1,29 @@
-// @dart=2.9
-
 import 'package:photos/models/file.dart';
-import 'package:photos/models/search/search_results.dart';
+import 'package:photos/models/search/search_result.dart';
 
 class FileSearchResult extends SearchResult {
   final File file;
 
   FileSearchResult(this.file);
+
+  @override
+  String name() {
+    return file.displayName;
+  }
+
+  @override
+  ResultType type() {
+    return ResultType.file;
+  }
+
+  @override
+  File previewThumbnail() {
+    return file;
+  }
+
+  @override
+  List<File> resultFiles() {
+    // for fileSearchResult, the file detailed page view will be opened
+    throw UnimplementedError();
+  }
 }

+ 30 - 0
lib/models/search/generic_search_result.dart

@@ -0,0 +1,30 @@
+import 'package:photos/models/file.dart';
+import 'package:photos/models/search/search_result.dart';
+
+class GenericSearchResult extends SearchResult {
+  final String _name;
+  final List<File> _files;
+  final ResultType _type;
+
+  GenericSearchResult(this._type, this._name, this._files);
+
+  @override
+  String name() {
+    return _name;
+  }
+
+  @override
+  ResultType type() {
+    return _type;
+  }
+
+  @override
+  File previewThumbnail() {
+    return _files.first;
+  }
+
+  @override
+  List<File> resultFiles() {
+    return _files;
+  }
+}

+ 0 - 17
lib/models/search/holiday_search_result.dart

@@ -1,17 +0,0 @@
-// @dart=2.9
-
-import 'package:photos/models/file.dart';
-import 'package:photos/models/search/search_results.dart';
-
-class HolidaySearchResult extends SearchResult {
-  final String holidayName;
-  final List<File> files;
-  HolidaySearchResult(this.holidayName, this.files);
-}
-
-class HolidayData {
-  final String name;
-  final int month;
-  final int day;
-  const HolidayData(this.name, this.month, this.day);
-}

+ 13 - 22
lib/models/search/location_api_response.dart

@@ -1,26 +1,27 @@
-// @dart=2.9
-
 class LocationApiResponse {
   final List<LocationDataFromResponse> results;
   LocationApiResponse({
-    this.results,
+    required this.results,
   });
 
   LocationApiResponse copyWith({
-    List<LocationDataFromResponse> results,
+    required List<LocationDataFromResponse> results,
   }) {
     return LocationApiResponse(
-      results: results ?? this.results,
+      results: results,
     );
   }
 
   factory LocationApiResponse.fromMap(Map<String, dynamic> map) {
     return LocationApiResponse(
-      results: List<LocationDataFromResponse>.from(
-        (map['results']).map(
-          (x) => LocationDataFromResponse.fromMap(x as Map<String, dynamic>),
-        ),
-      ),
+      results: (map['results']) == null
+          ? []
+          : List<LocationDataFromResponse>.from(
+              (map['results']).map(
+                (x) =>
+                    LocationDataFromResponse.fromMap(x as Map<String, dynamic>),
+              ),
+            ),
     );
   }
 }
@@ -29,20 +30,10 @@ class LocationDataFromResponse {
   final String place;
   final List<double> bbox;
   LocationDataFromResponse({
-    this.place,
-    this.bbox,
+    required this.place,
+    required this.bbox,
   });
 
-  LocationDataFromResponse copyWith({
-    String place,
-    List<double> bbox,
-  }) {
-    return LocationDataFromResponse(
-      place: place ?? this.place,
-      bbox: bbox ?? this.bbox,
-    );
-  }
-
   factory LocationDataFromResponse.fromMap(Map<String, dynamic> map) {
     return LocationDataFromResponse(
       place: map['place'] as String,

+ 0 - 11
lib/models/search/location_search_result.dart

@@ -1,11 +0,0 @@
-// @dart=2.9
-
-import 'package:photos/models/file.dart';
-import 'package:photos/models/search/search_results.dart';
-
-class LocationSearchResult extends SearchResult {
-  final String location;
-  final List<File> files;
-
-  LocationSearchResult(this.location, this.files);
-}

+ 0 - 16
lib/models/search/month_search_result.dart

@@ -1,16 +0,0 @@
-// @dart=2.9
-
-import 'package:photos/models/file.dart';
-import 'package:photos/models/search/search_results.dart';
-
-class MonthSearchResult extends SearchResult {
-  final String month;
-  final List<File> files;
-  MonthSearchResult(this.month, this.files);
-}
-
-class MonthData {
-  final String name;
-  final int monthNumber;
-  MonthData(this.name, this.monthNumber);
-}

+ 26 - 0
lib/models/search/search_result.dart

@@ -0,0 +1,26 @@
+import 'package:photos/models/file.dart';
+
+abstract class SearchResult {
+  ResultType type();
+
+  String name();
+
+  File previewThumbnail();
+
+  String heroTag() {
+    return '${type().toString()}_${name()}';
+  }
+
+  List<File> resultFiles();
+}
+
+enum ResultType {
+  collection,
+  file,
+  location,
+  month,
+  year,
+  fileType,
+  fileExtension,
+  event
+}

+ 0 - 3
lib/models/search/search_results.dart

@@ -1,3 +0,0 @@
-
-
-class SearchResult {}

+ 0 - 11
lib/models/search/year_search_result.dart

@@ -1,11 +0,0 @@
-// @dart=2.9
-
-import 'package:photos/models/file.dart';
-import 'package:photos/models/search/search_results.dart';
-
-class YearSearchResult extends SearchResult {
-  final String year;
-  final List<File> files;
-
-  YearSearchResult(this.year, this.files);
-}

+ 4 - 8
lib/models/selected_files.dart

@@ -1,5 +1,4 @@
-// @dart=2.9
-
+import 'package:collection/collection.dart' show IterableExtension;
 import 'package:flutter/foundation.dart';
 import 'package:photos/models/file.dart';
 
@@ -11,9 +10,8 @@ class SelectedFiles extends ChangeNotifier {
     // To handle the cases, where the file might have changed due to upload
     // or any other update, using file.generatedID to track if this file was already
     // selected or not
-    final File alreadySelected = files.firstWhere(
+    final File? alreadySelected = files.firstWhereOrNull(
       (element) => element.generatedID == file.generatedID,
-      orElse: () => null,
     );
     if (alreadySelected != null) {
       files.remove(alreadySelected);
@@ -26,17 +24,15 @@ class SelectedFiles extends ChangeNotifier {
   }
 
   bool isFileSelected(File file) {
-    final File alreadySelected = files.firstWhere(
+    final File? alreadySelected = files.firstWhereOrNull(
       (element) => element.generatedID == file.generatedID,
-      orElse: () => null,
     );
     return alreadySelected != null;
   }
 
   bool isPartOfLastSection(File file) {
-    final File alreadySelected = lastSelections.firstWhere(
+    final File? alreadySelected = lastSelections.firstWhereOrNull(
       (element) => element.generatedID == file.generatedID,
-      orElse: () => null,
     );
     return alreadySelected != null;
   }

+ 3 - 100
lib/models/sessions.dart

@@ -1,9 +1,3 @@
-// @dart=2.9
-
-import 'dart:convert';
-
-import 'package:flutter/foundation.dart';
-
 class Sessions {
   final List<Session> sessions;
 
@@ -11,43 +5,14 @@ class Sessions {
     this.sessions,
   );
 
-  Sessions copyWith({
-    List<Session> sessions,
-  }) {
-    return Sessions(
-      sessions ?? this.sessions,
-    );
-  }
-
-  Map<String, dynamic> toMap() {
-    return {
-      'sessions': sessions?.map((x) => x.toMap())?.toList(),
-    };
-  }
-
   factory Sessions.fromMap(Map<String, dynamic> map) {
+    if (map["sessions"] == null) {
+      throw Exception('\'map["sessions"]\' must not be null');
+    }
     return Sessions(
       List<Session>.from(map['sessions']?.map((x) => Session.fromMap(x))),
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory Sessions.fromJson(String source) =>
-      Sessions.fromMap(json.decode(source));
-
-  @override
-  String toString() => 'Sessions(sessions: $sessions)';
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is Sessions && listEquals(other.sessions, sessions);
-  }
-
-  @override
-  int get hashCode => sessions.hashCode;
 }
 
 class Session {
@@ -67,35 +32,6 @@ class Session {
     this.lastUsedTime,
   );
 
-  Session copyWith({
-    String token,
-    int creationTime,
-    String ip,
-    String ua,
-    String prettyUA,
-    int lastUsedTime,
-  }) {
-    return Session(
-      token ?? this.token,
-      creationTime ?? this.creationTime,
-      ip ?? this.ip,
-      ua ?? this.ua,
-      prettyUA ?? this.prettyUA,
-      lastUsedTime ?? this.lastUsedTime,
-    );
-  }
-
-  Map<String, dynamic> toMap() {
-    return {
-      'token': token,
-      'creationTime': creationTime,
-      'ip': ip,
-      'ua': ua,
-      'prettyUA': prettyUA,
-      'lastUsedTime': lastUsedTime,
-    };
-  }
-
   factory Session.fromMap(Map<String, dynamic> map) {
     return Session(
       map['token'],
@@ -106,37 +42,4 @@ class Session {
       map['lastUsedTime'],
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory Session.fromJson(String source) =>
-      Session.fromMap(json.decode(source));
-
-  @override
-  String toString() {
-    return 'Session(token: $token, creationTime: $creationTime, ip: $ip, ua: $ua, prettyUA: $prettyUA, lastUsedTime: $lastUsedTime)';
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is Session &&
-        other.token == token &&
-        other.creationTime == creationTime &&
-        other.ip == ip &&
-        other.ua == ua &&
-        other.prettyUA == prettyUA &&
-        other.lastUsedTime == lastUsedTime;
-  }
-
-  @override
-  int get hashCode {
-    return token.hashCode ^
-        creationTime.hashCode ^
-        ip.hashCode ^
-        ua.hashCode ^
-        prettyUA.hashCode ^
-        lastUsedTime.hashCode;
-  }
 }

+ 5 - 24
lib/models/set_keys_request.dart

@@ -1,7 +1,3 @@
-// @dart=2.9
-
-import 'dart:convert';
-
 class SetKeysRequest {
   final String kekSalt;
   final String encryptedKey;
@@ -10,11 +6,11 @@ class SetKeysRequest {
   final int opsLimit;
 
   SetKeysRequest({
-    this.kekSalt,
-    this.encryptedKey,
-    this.keyDecryptionNonce,
-    this.memLimit,
-    this.opsLimit,
+    required this.kekSalt,
+    required this.encryptedKey,
+    required this.keyDecryptionNonce,
+    required this.memLimit,
+    required this.opsLimit,
   });
 
   Map<String, dynamic> toMap() {
@@ -26,19 +22,4 @@ class SetKeysRequest {
       'opsLimit': opsLimit,
     };
   }
-
-  factory SetKeysRequest.fromMap(Map<String, dynamic> map) {
-    return SetKeysRequest(
-      kekSalt: map['kekSalt'],
-      encryptedKey: map['encryptedKey'],
-      keyDecryptionNonce: map['keyDecryptionNonce'],
-      memLimit: map['memLimit'],
-      opsLimit: map['opsLimit'],
-    );
-  }
-
-  String toJson() => json.encode(toMap());
-
-  factory SetKeysRequest.fromJson(String source) =>
-      SetKeysRequest.fromMap(json.decode(source));
 }

+ 0 - 18
lib/models/set_recovery_key_request.dart

@@ -1,7 +1,3 @@
-// @dart=2.9
-
-import 'dart:convert';
-
 class SetRecoveryKeyRequest {
   final String masterKeyEncryptedWithRecoveryKey;
   final String masterKeyDecryptionNonce;
@@ -23,18 +19,4 @@ class SetRecoveryKeyRequest {
       'recoveryKeyDecryptionNonce': recoveryKeyDecryptionNonce,
     };
   }
-
-  factory SetRecoveryKeyRequest.fromMap(Map<String, dynamic> map) {
-    return SetRecoveryKeyRequest(
-      map['masterKeyEncryptedWithRecoveryKey'],
-      map['masterKeyDecryptionNonce'],
-      map['recoveryKeyEncryptedWithMasterKey'],
-      map['recoveryKeyDecryptionNonce'],
-    );
-  }
-
-  String toJson() => json.encode(toMap());
-
-  factory SetRecoveryKeyRequest.fromJson(String source) =>
-      SetRecoveryKeyRequest.fromMap(json.decode(source));
 }

+ 15 - 109
lib/models/subscription.dart

@@ -1,14 +1,9 @@
-// @dart=2.9
-
-import 'dart:convert';
-
-const kFreeProductID = "free";
-const kStripe = "stripe";
-const kAppStore = "appstore";
-const kPlayStore = "playstore";
+const freeProductID = "free";
+const stripe = "stripe";
+const appStore = "appstore";
+const playStore = "playstore";
 
 class Subscription {
-  final int id;
   final String productID;
   final int storage;
   final String originalTransactionID;
@@ -16,17 +11,16 @@ class Subscription {
   final int expiryTime;
   final String price;
   final String period;
-  final Attributes attributes;
+  final Attributes? attributes;
 
   Subscription({
-    this.id,
-    this.productID,
-    this.storage,
-    this.originalTransactionID,
-    this.paymentProvider,
-    this.expiryTime,
-    this.price,
-    this.period,
+    required this.productID,
+    required this.storage,
+    required this.originalTransactionID,
+    required this.paymentProvider,
+    required this.expiryTime,
+    required this.price,
+    required this.period,
     this.attributes,
   });
 
@@ -38,48 +32,9 @@ class Subscription {
     return 'year' == period;
   }
 
-  Subscription copyWith({
-    int id,
-    String productID,
-    int storage,
-    String originalTransactionID,
-    String paymentProvider,
-    int expiryTime,
-    String price,
-    String period,
-  }) {
-    return Subscription(
-      id: id ?? this.id,
-      productID: productID ?? this.productID,
-      storage: storage ?? this.storage,
-      originalTransactionID:
-          originalTransactionID ?? this.originalTransactionID,
-      paymentProvider: paymentProvider ?? this.paymentProvider,
-      expiryTime: expiryTime ?? this.expiryTime,
-      price: price ?? this.price,
-      period: period ?? this.period,
-    );
-  }
-
-  Map<String, dynamic> toMap() {
-    final map = <String, dynamic>{
-      'id': id,
-      'productID': productID,
-      'storage': storage,
-      'originalTransactionID': originalTransactionID,
-      'paymentProvider': paymentProvider,
-      'expiryTime': expiryTime,
-      'price': price,
-      'period': period,
-      'attributes': attributes?.toJson()
-    };
-    return map;
-  }
-
-  factory Subscription.fromMap(Map<String, dynamic> map) {
+  static fromMap(Map<String, dynamic>? map) {
     if (map == null) return null;
     return Subscription(
-      id: map['id'],
       productID: map['productID'],
       storage: map['storage'],
       originalTransactionID: map['originalTransactionID'],
@@ -92,48 +47,11 @@ class Subscription {
           : null,
     );
   }
-
-  String toJson() => json.encode(toMap());
-
-  factory Subscription.fromJson(String source) =>
-      Subscription.fromMap(json.decode(source));
-
-  @override
-  String toString() {
-    return 'Subscription{id: $id, productID: $productID, storage: $storage, originalTransactionID: $originalTransactionID, paymentProvider: $paymentProvider, expiryTime: $expiryTime, price: $price, period: $period, attributes: $attributes}';
-  }
-
-  @override
-  bool operator ==(Object o) {
-    if (identical(this, o)) return true;
-
-    return o is Subscription &&
-        o.id == id &&
-        o.productID == productID &&
-        o.storage == storage &&
-        o.originalTransactionID == originalTransactionID &&
-        o.paymentProvider == paymentProvider &&
-        o.expiryTime == expiryTime &&
-        o.price == price &&
-        o.period == period;
-  }
-
-  @override
-  int get hashCode {
-    return id.hashCode ^
-        productID.hashCode ^
-        storage.hashCode ^
-        originalTransactionID.hashCode ^
-        paymentProvider.hashCode ^
-        expiryTime.hashCode ^
-        price.hashCode ^
-        period.hashCode;
-  }
 }
 
 class Attributes {
-  bool isCancelled;
-  String customerID;
+  bool? isCancelled;
+  String? customerID;
 
   Attributes({
     this.isCancelled,
@@ -144,16 +62,4 @@ class Attributes {
     isCancelled = json["isCancelled"];
     customerID = json["customerID"];
   }
-
-  Map<String, dynamic> toJson() {
-    final map = <String, dynamic>{};
-    map["isCancelled"] = isCancelled;
-    map["customerID"] = customerID;
-    return map;
-  }
-
-  @override
-  String toString() {
-    return 'Attributes{isCancelled: $isCancelled, customerID: $customerID}';
-  }
 }

+ 3 - 5
lib/models/trash_file.dart

@@ -1,16 +1,14 @@
-// @dart=2.9
-
 import 'package:photos/models/file.dart';
 
 class TrashFile extends File {
   // time when file was put in the trash for first time
-  int createdAt;
+  late int createdAt;
 
   // for non-deleted trash items, updateAt is usually equal to the latest time
   // when the file was moved to trash
-  int updateAt;
+  late int updateAt;
 
   // time after which will will be deleted from trash & user's storage usage
   // will go down
-  int deleteBy;
+  late int deleteBy;
 }

+ 1 - 5
lib/models/trash_item_request.dart

@@ -1,12 +1,8 @@
-// @dart=2.9
-
 class TrashRequest {
   final int fileID;
   final int collectionID;
 
-  TrashRequest(this.fileID, this.collectionID)
-      : assert(fileID != null),
-        assert(collectionID != null);
+  TrashRequest(this.fileID, this.collectionID);
 
   factory TrashRequest.fromJson(Map<String, dynamic> json) {
     return TrashRequest(json['fileID'], json['collectionID']);

+ 30 - 0
lib/models/upload_strategy.dart

@@ -0,0 +1,30 @@
+enum UploadStrategy {
+  // uploader will only try to upload the file in a collection if the file is
+  // not already uploaded
+  ifMissing,
+  // alwaysUpload will always try to upload or add the file to given collection
+  always,
+  other,
+}
+
+int getInt(UploadStrategy uploadType) {
+  switch (uploadType) {
+    case UploadStrategy.ifMissing:
+      return 0;
+    case UploadStrategy.always:
+      return 1;
+    default:
+      return -1;
+  }
+}
+
+UploadStrategy getUploadType(int uploadType) {
+  switch (uploadType) {
+    case 0:
+      return UploadStrategy.ifMissing;
+    case 1:
+      return UploadStrategy.always;
+    default:
+      return UploadStrategy.other;
+  }
+}

+ 0 - 4
lib/models/upload_url.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 
 class UploadURL {
@@ -15,8 +13,6 @@ class UploadURL {
   }
 
   factory UploadURL.fromMap(Map<String, dynamic> map) {
-    if (map == null) return null;
-
     return UploadURL(
       map['url'],
       map['objectKey'],

+ 12 - 43
lib/models/user_details.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:math';
 
 import 'package:collection/collection.dart';
@@ -11,7 +9,7 @@ class UserDetails {
   final int fileCount;
   final int sharedCollectionsCount;
   final Subscription subscription;
-  final FamilyData familyData;
+  final FamilyData? familyData;
 
   UserDetails(
     this.email,
@@ -28,8 +26,8 @@ class UserDetails {
 
   bool isFamilyAdmin() {
     assert(isPartOfFamily(), "verify user is part of family before calling");
-    final FamilyMember currentUserMember = familyData?.members
-        ?.firstWhere((element) => element.email.trim() == email.trim());
+    final FamilyMember currentUserMember = familyData!.members!
+        .firstWhere((element) => element.email.trim() == email.trim());
     return currentUserMember.isAdmin;
   }
 
@@ -37,47 +35,32 @@ class UserDetails {
   // belong to family group. Otherwise, it will return storage consumed by
   // current user
   int getFamilyOrPersonalUsage() {
-    return isPartOfFamily() ? familyData.getTotalUsage() : usage;
+    return isPartOfFamily() ? familyData!.getTotalUsage() : usage;
   }
 
   int getFreeStorage() {
     return max(
       isPartOfFamily()
-          ? (familyData.storage - familyData.getTotalUsage())
+          ? (familyData!.storage - familyData!.getTotalUsage())
           : (subscription.storage - (usage)),
       0,
     );
   }
 
   int getTotalStorage() {
-    return isPartOfFamily() ? familyData.storage : subscription.storage;
-  }
-
-  int getPersonalUsage() {
-    return usage;
+    return isPartOfFamily() ? familyData!.storage : subscription.storage;
   }
 
   factory UserDetails.fromMap(Map<String, dynamic> map) {
     return UserDetails(
       map['email'] as String,
       map['usage'] as int,
-      map['fileCount'] as int ?? 0,
-      map['sharedCollectionsCount'] as int ?? 0,
+      (map['fileCount'] ?? 0) as int,
+      (map['sharedCollectionsCount'] ?? 0) as int,
       Subscription.fromMap(map['subscription']),
       FamilyData.fromMap(map['familyData']),
     );
   }
-
-  Map<String, dynamic> toMap() {
-    return {
-      'email': email,
-      'usage': usage,
-      'fileCount': fileCount,
-      'sharedCollectionsCount': sharedCollectionsCount,
-      'subscription': subscription,
-      'familyData': familyData
-    };
-  }
 }
 
 class FamilyMember {
@@ -96,14 +79,10 @@ class FamilyMember {
       map['isAdmin'] as bool,
     );
   }
-
-  Map<String, dynamic> toMap() {
-    return {'email': email, 'usage': usage, 'id': id, 'isAdmin': isAdmin};
-  }
 }
 
 class FamilyData {
-  final List<FamilyMember> members;
+  final List<FamilyMember>? members;
 
   // Storage available based on the family plan
   final int storage;
@@ -112,13 +91,11 @@ class FamilyData {
   FamilyData(this.members, this.storage, this.expiryTime);
 
   int getTotalUsage() {
-    return members.map((e) => e.usage).toList().sum;
+    return members!.map((e) => e.usage).toList().sum;
   }
 
-  factory FamilyData.fromMap(Map<String, dynamic> map) {
-    if (map == null) {
-      return null;
-    }
+  static fromMap(Map<String, dynamic>? map) {
+    if (map == null) return null;
     assert(map['members'] != null && map['members'].length >= 0);
     final members = List<FamilyMember>.from(
       map['members'].map((x) => FamilyMember.fromMap(x)),
@@ -129,12 +106,4 @@ class FamilyData {
       map['expiryTime'] as int,
     );
   }
-
-  Map<String, dynamic> toMap() {
-    return {
-      'members': members.map((x) => x?.toMap())?.toList(),
-      'storage': storage,
-      'expiryTime': expiryTime
-    };
-  }
 }

+ 0 - 2
lib/services/app_lifecycle_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'package:logging/logging.dart';
 
 class AppLifecycleService {

+ 54 - 16
lib/services/collections_service.dart

@@ -1,5 +1,6 @@
 // @dart=2.9
 
+import 'dart:async';
 import 'dart:convert';
 import 'dart:math';
 import 'dart:typed_data';
@@ -13,6 +14,7 @@ import 'package:photos/core/errors.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/db/collections_db.dart';
+import 'package:photos/db/device_files_db.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/db/trash_db.dart';
 import 'package:photos/events/collection_updated_event.dart';
@@ -25,6 +27,7 @@ import 'package:photos/models/file.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/services/app_lifecycle_service.dart';
 import 'package:photos/services/file_magic_service.dart';
+import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/file_download_util.dart';
@@ -44,7 +47,7 @@ class CollectionsService {
   SharedPreferences _prefs;
   Future<List<File>> _cachedLatestFiles;
   final _dio = Network.instance.getDio();
-  final _localCollections = <String, Collection>{};
+  final _localPathToCollectionID = <String, int>{};
   final _collectionIDToCollections = <int, Collection>{};
   final _cachedKeys = <int, Uint8List>{};
 
@@ -117,7 +120,7 @@ class CollectionsService {
   }
 
   void clearCache() {
-    _localCollections.clear();
+    _localPathToCollectionID.clear();
     _collectionIDToCollections.clear();
     _cachedKeys.clear();
   }
@@ -160,10 +163,6 @@ class CollectionsService {
     return _prefs.setInt(key, time);
   }
 
-  Collection getCollectionForPath(String path) {
-    return _localCollections[path];
-  }
-
   // getActiveCollections returns list of collections which are not deleted yet
   List<Collection> getActiveCollections() {
     return _collectionIDToCollections.values
@@ -241,6 +240,40 @@ class CollectionsService {
     RemoteSyncService.instance.sync(silently: true);
   }
 
+  Future<void> trashCollection(Collection collection) async {
+    try {
+      final deviceCollections = await _filesDB.getDeviceCollections();
+      final Map<String, bool> deivcePathIDsToUnsync = Map.fromEntries(
+        deviceCollections
+            .where((e) => e.shouldBackup && e.collectionID == collection.id)
+            .map((e) => MapEntry(e.id, false)),
+      );
+
+      if (deivcePathIDsToUnsync.isNotEmpty) {
+        _logger.info(
+          'turning off backup status for folders $deivcePathIDsToUnsync',
+        );
+        await RemoteSyncService.instance
+            .updateDeviceFolderSyncStatus(deivcePathIDsToUnsync);
+      }
+      await _dio.delete(
+        Configuration.instance.getHttpEndpoint() +
+            "/collections/v2/${collection.id}",
+        options: Options(
+          headers: {"X-Auth-Token": Configuration.instance.getToken()},
+        ),
+      );
+      await _filesDB.deleteCollection(collection.id);
+      final deletedCollection = collection.copyWith(isDeleted: true);
+      _collectionIDToCollections[collection.id] = deletedCollection;
+      _db.insert([deletedCollection]);
+      unawaited(LocalSyncService.instance.syncAll());
+    } catch (e) {
+      _logger.severe('failed to trash collection', e);
+      rethrow;
+    }
+  }
+
   Uint8List getCollectionKey(int collectionID) {
     if (!_cachedKeys.containsKey(collectionID)) {
       final collection = _collectionIDToCollections[collectionID];
@@ -259,6 +292,9 @@ class CollectionsService {
   Uint8List _getDecryptedKey(Collection collection) {
     final encryptedKey = Sodium.base642bin(collection.encryptedKey);
     if (collection.owner.id == _config.getUserID()) {
+      if(_config.getKey() == null) {
+        throw Exception("key can not be null");
+      }
       return CryptoUtil.decryptSync(
         encryptedKey,
         _config.getKey(),
@@ -545,9 +581,14 @@ class CollectionsService {
   }
 
   Future<Collection> getOrCreateForPath(String path) async {
-    if (_localCollections.containsKey(path) &&
-        _localCollections[path].owner.id == _config.getUserID()) {
-      return _localCollections[path];
+    if (_localPathToCollectionID.containsKey(path)) {
+      final Collection cachedCollection =
+          _collectionIDToCollections[_localPathToCollectionID[path]];
+      if (cachedCollection != null &&
+          !cachedCollection.isDeleted &&
+          cachedCollection.owner.id == _config.getUserID()) {
+        return cachedCollection;
+      }
     }
     final key = CryptoUtil.generateKey();
     final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
@@ -789,10 +830,6 @@ class CollectionsService {
     Bus.instance.fire(CollectionUpdatedEvent(toCollectionID, files));
   }
 
-  String getCollectionNameByID(int collectionID) {
-    return getCollectionByID(collectionID).name;
-  }
-
   void _validateMoveRequest(
     int toCollectionID,
     int fromCollectionID,
@@ -853,9 +890,10 @@ class CollectionsService {
     final collectionWithDecryptedName =
         _getCollectionWithDecryptedName(collection);
     if (collection.attributes.encryptedPath != null &&
-        !(collection.isDeleted)) {
-      _localCollections[decryptCollectionPath(collection)] =
-          collectionWithDecryptedName;
+        !collection.isDeleted &&
+        collection.owner.id == _config.getUserID()) {
+      _localPathToCollectionID[decryptCollectionPath(collection)] =
+          collection.id;
     }
     _collectionIDToCollections[collection.id] = collectionWithDecryptedName;
     return collectionWithDecryptedName;

+ 9 - 64
lib/services/feature_flag_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:convert';
 import 'dart:io';
 
@@ -15,11 +13,11 @@ class FeatureFlagService {
 
   static final FeatureFlagService instance =
       FeatureFlagService._privateConstructor();
-  static const kBooleanFeatureFlagsKey = "feature_flags_key";
+  static const _featureFlagsKey = "feature_flags_key";
 
   final _logger = Logger("FeatureFlagService");
-  FeatureFlags _featureFlags;
-  SharedPreferences _prefs;
+  FeatureFlags? _featureFlags;
+  late SharedPreferences _prefs;
 
   Future<void> init() async {
     _prefs = await SharedPreferences.getInstance();
@@ -35,12 +33,12 @@ class FeatureFlagService {
 
   FeatureFlags _getFeatureFlags() {
     _featureFlags ??=
-        FeatureFlags.fromJson(_prefs.getString(kBooleanFeatureFlagsKey));
+        FeatureFlags.fromJson(_prefs.getString(_featureFlagsKey)!);
     // if nothing is cached, use defaults as temporary fallback
     if (_featureFlags == null) {
       return FeatureFlags.defaultFlags;
     }
-    return _featureFlags;
+    return _featureFlags!;
   }
 
   bool disableCFWorker() {
@@ -52,28 +50,6 @@ class FeatureFlagService {
     }
   }
 
-  bool disableUrlSharing() {
-    try {
-      return _getFeatureFlags().disableUrlSharing;
-    } catch (e) {
-      _logger.severe(e);
-      return FFDefault.disableUrlSharing;
-    }
-  }
-
-  bool enableMissingLocationMigration() {
-    // only needs to be enabled for android
-    if (!Platform.isAndroid) {
-      return false;
-    }
-    try {
-      return _getFeatureFlags().enableMissingLocationMigration;
-    } catch (e) {
-      _logger.severe(e);
-      return FFDefault.enableMissingLocationMigration;
-    }
-  }
-
   bool enableStripe() {
     if (Platform.isIOS) {
       return false;
@@ -86,17 +62,8 @@ class FeatureFlagService {
     }
   }
 
-  bool enableSearch() {
-    try {
-      return isInternalUserOrDebugBuild() || _getFeatureFlags().enableSearch;
-    } catch (e) {
-      _logger.severe("failed to getSearchFeatureFlag", e);
-      return FFDefault.enableSearch;
-    }
-  }
-
   bool isInternalUserOrDebugBuild() {
-    final String email = Configuration.instance.getEmail();
+    final String? email = Configuration.instance.getEmail();
     return (email != null && email.endsWith("@ente.io")) || kDebugMode;
   }
 
@@ -107,7 +74,7 @@ class FeatureFlagService {
           .get("https://static.ente.io/feature_flags.json");
       final flagsResponse = FeatureFlags.fromMap(response.data);
       if (flagsResponse != null) {
-        _prefs.setString(kBooleanFeatureFlagsKey, flagsResponse.toJson());
+        _prefs.setString(_featureFlagsKey, flagsResponse.toJson());
         _featureFlags = flagsResponse;
       }
     } catch (e) {
@@ -119,33 +86,21 @@ class FeatureFlagService {
 class FeatureFlags {
   static FeatureFlags defaultFlags = FeatureFlags(
     disableCFWorker: FFDefault.disableCFWorker,
-    disableUrlSharing: FFDefault.disableUrlSharing,
     enableStripe: FFDefault.enableStripe,
-    enableMissingLocationMigration: FFDefault.enableMissingLocationMigration,
-    enableSearch: FFDefault.enableSearch,
   );
 
   final bool disableCFWorker;
-  final bool disableUrlSharing;
   final bool enableStripe;
-  final bool enableMissingLocationMigration;
-  final bool enableSearch;
 
   FeatureFlags({
-    @required this.disableCFWorker,
-    @required this.disableUrlSharing,
-    @required this.enableStripe,
-    @required this.enableMissingLocationMigration,
-    @required this.enableSearch,
+    required this.disableCFWorker,
+    required this.enableStripe,
   });
 
   Map<String, dynamic> toMap() {
     return {
       "disableCFWorker": disableCFWorker,
-      "disableUrlSharing": disableUrlSharing,
       "enableStripe": enableStripe,
-      "enableMissingLocationMigration": enableMissingLocationMigration,
-      "enableSearch": enableSearch,
     };
   }
 
@@ -157,17 +112,7 @@ class FeatureFlags {
   factory FeatureFlags.fromMap(Map<String, dynamic> json) {
     return FeatureFlags(
       disableCFWorker: json["disableCFWorker"] ?? FFDefault.disableCFWorker,
-      disableUrlSharing:
-          json["disableUrlSharing"] ?? FFDefault.disableUrlSharing,
       enableStripe: json["enableStripe"] ?? FFDefault.enableStripe,
-      enableMissingLocationMigration: json["enableMissingLocationMigration"] ??
-          FFDefault.enableMissingLocationMigration,
-      enableSearch: json["enableSearch"] ?? FFDefault.enableSearch,
     );
   }
-
-  @override
-  String toString() {
-    return toMap().toString();
-  }
 }

+ 6 - 4
lib/services/file_magic_service.dart

@@ -32,9 +32,9 @@ class FileMagicService {
       FileMagicService._privateConstructor();
 
   Future<void> changeVisibility(List<File> files, int visibility) async {
-    final Map<String, dynamic> update = {kMagicKeyVisibility: visibility};
+    final Map<String, dynamic> update = {magicKeyVisibility: visibility};
     await _updateMagicData(files, update);
-    if (visibility == kVisibilityVisible) {
+    if (visibility == visibilityVisible) {
       // Force reload home gallery to pull in the now unarchived files
       Bus.instance.fire(ForceReloadHomeGalleryEvent());
       Bus.instance
@@ -64,7 +64,8 @@ class FileMagicService {
         // read the existing magic metadata and apply new updates to existing data
         // current update is simple replace. This will be enhanced in the future,
         // as required.
-        final Map<String, dynamic> jsonToUpdate = jsonDecode(file.pubMmdEncodedJson);
+        final Map<String, dynamic> jsonToUpdate =
+            jsonDecode(file.pubMmdEncodedJson);
         newMetadataUpdate.forEach((key, value) {
           jsonToUpdate[key] = value;
         });
@@ -134,7 +135,8 @@ class FileMagicService {
         // read the existing magic metadata and apply new updates to existing data
         // current update is simple replace. This will be enhanced in the future,
         // as required.
-        final Map<String, dynamic> jsonToUpdate = jsonDecode(file.mMdEncodedJson);
+        final Map<String, dynamic> jsonToUpdate =
+            jsonDecode(file.mMdEncodedJson);
         newMetadataUpdate.forEach((key, value) {
           jsonToUpdate[key] = value;
         });

+ 37 - 0
lib/services/files_service.dart

@@ -0,0 +1,37 @@
+// ignore: import_of_legacy_library_into_null_safe
+import 'package:dio/dio.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/core/network.dart';
+
+class FilesService {
+  late Configuration _config;
+  late Dio _dio;
+  late Logger _logger;
+  FilesService._privateConstructor() {
+    _config = Configuration.instance;
+    _dio = Network.instance.getDio();
+    _logger = Logger("FilesService");
+  }
+  static final FilesService instance = FilesService._privateConstructor();
+
+  Future<int> getFileSize(int uploadedFileID) async {
+    try {
+      final response = await _dio.post(
+        Configuration.instance.getHttpEndpoint() + "/files/size",
+        options: Options(
+          headers: {
+            "X-Auth-Token": _config.getToken(),
+          },
+        ),
+        data: {
+          "fileIDs": [uploadedFileID],
+        },
+      );
+      return response.data["size"];
+    } catch (e) {
+      _logger.severe(e);
+      rethrow;
+    }
+  }
+}

+ 31 - 1
lib/services/ignored_files_service.dart

@@ -49,6 +49,36 @@ class IgnoredFilesService {
     return false;
   }
 
+  // removeIgnoredMappings is used to remove the ignore mapping for the given
+  // set of files so that they can be uploaded.
+  Future<void> removeIgnoredMappings(List<File> files) async {
+    final List<IgnoredFile> ignoredFiles = [];
+    final Set<String> idsToRemoveFromCache = {};
+    final Set<String> currentlyIgnoredIDs = await ignoredIDs;
+    for (final file in files) {
+      // check if upload is not skipped for file. If not, no need to remove
+      // any mapping
+      if (!shouldSkipUpload(currentlyIgnoredIDs, file)) {
+        continue;
+      }
+      final id = _getIgnoreID(file.localID, file.deviceFolder, file.title);
+      idsToRemoveFromCache.add(id);
+      ignoredFiles.add(
+        IgnoredFile(file.localID, file.title, file.deviceFolder, ""),
+      );
+    }
+
+    if (ignoredFiles.isNotEmpty) {
+      await _db.removeIgnoredEntries(ignoredFiles);
+      currentlyIgnoredIDs.removeAll(idsToRemoveFromCache);
+    }
+  }
+
+  Future<void> reset() async {
+    await _db.clearTable();
+    _ignoredIDs = null;
+  }
+
   Future<Set<String>> _loadExistingIDs() async {
     _logger.fine('loading existing IDs');
     final result = await _db.getAll();
@@ -66,7 +96,7 @@ class IgnoredFilesService {
     );
   }
 
-  // _computeIgnoreID will return null if don't have sufficient information
+  // _getIgnoreID will return null if don't have sufficient information
   // to ignore the file based on the platform. Uploads from web or files shared to
   // end usually don't have local id.
   // For Android: It returns deviceFolder-title as ID for Android.

+ 333 - 0
lib/services/local/local_sync_util.dart

@@ -0,0 +1,333 @@
+// @dart = 2.9
+import 'dart:math';
+
+import 'package:computer/computer.dart';
+import 'package:flutter/foundation.dart';
+import 'package:logging/logging.dart';
+import 'package:photo_manager/photo_manager.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/events/local_import_progress.dart';
+import 'package:photos/models/file.dart';
+import 'package:tuple/tuple.dart';
+
+final _logger = Logger("FileSyncUtil");
+const ignoreSizeConstraint = SizeConstraint(ignoreSize: true);
+const assetFetchPageSize = 2000;
+
+Future<Tuple2<List<LocalPathAsset>, List<File>>> getLocalPathAssetsAndFiles(
+  int fromTime,
+  int toTime,
+  Computer computer,
+) async {
+  final pathEntities = await _getGalleryList(
+    updateFromTime: fromTime,
+    updateToTime: toTime,
+  );
+  final List<LocalPathAsset> localPathAssets = [];
+
+  // alreadySeenLocalIDs is used to track and ignore file with particular
+  // localID if it's already present in another album. This only impacts iOS
+  // devices where a file can belong to multiple
+  final Set<String> alreadySeenLocalIDs = {};
+  final List<File> uniqueFiles = [];
+  for (AssetPathEntity pathEntity in pathEntities) {
+    final List<AssetEntity> assetsInPath = await _getAllAssetLists(pathEntity);
+    final Tuple2<Set<String>, List<File>> result = await computer.compute(
+      _getLocalIDsAndFilesFromAssets,
+      param: <String, dynamic>{
+        "pathEntity": pathEntity,
+        "fromTime": fromTime,
+        "alreadySeenLocalIDs": alreadySeenLocalIDs,
+        "assetList": assetsInPath,
+      },
+    );
+    alreadySeenLocalIDs.addAll(result.item1);
+    uniqueFiles.addAll(result.item2);
+    localPathAssets.add(
+      LocalPathAsset(
+        localIDs: result.item1,
+        pathName: pathEntity.name,
+        pathID: pathEntity.id,
+      ),
+    );
+  }
+  return Tuple2(localPathAssets, uniqueFiles);
+}
+
+// getDeviceFolderWithCountAndLatestFile returns a tuple of AssetPathEntity and
+// latest file's localID in the assetPath, along with modifiedPath time and
+// total count of assets in a Asset Path.
+// We use this result to update the latest thumbnail for deviceFolder and
+// identify (in future) which AssetPath needs to be re-synced again.
+Future<List<Tuple2<AssetPathEntity, String>>>
+    getDeviceFolderWithCountAndCoverID() async {
+  final List<Tuple2<AssetPathEntity, String>> result = [];
+  final pathEntities = await _getGalleryList(
+    needsTitle: false,
+    containsModifiedPath: true,
+    orderOption:
+        const OrderOption(type: OrderOptionType.createDate, asc: false),
+  );
+  for (AssetPathEntity pathEntity in pathEntities) {
+    //todo: test and handle empty album case
+    final latestEntity = await pathEntity.getAssetListPaged(
+      page: 0,
+      size: 1,
+    );
+    final String localCoverID = latestEntity.first.id;
+    result.add(Tuple2(pathEntity, localCoverID));
+  }
+  return result;
+}
+
+Future<List<LocalPathAsset>> getAllLocalAssets() async {
+  final filterOptionGroup = FilterOptionGroup();
+  filterOptionGroup.setOption(
+    AssetType.image,
+    const FilterOption(sizeConstraint: ignoreSizeConstraint),
+  );
+  filterOptionGroup.setOption(
+    AssetType.video,
+    const FilterOption(sizeConstraint: ignoreSizeConstraint),
+  );
+  filterOptionGroup.createTimeCond = DateTimeCond.def().copyWith(ignore: true);
+  final assetPaths = await PhotoManager.getAssetPathList(
+    hasAll: true,
+    type: RequestType.common,
+    filterOption: filterOptionGroup,
+  );
+  final List<LocalPathAsset> localPathAssets = [];
+  for (final assetPath in assetPaths) {
+    final Set<String> localIDs = <String>{};
+    for (final asset in await _getAllAssetLists(assetPath)) {
+      localIDs.add(asset.id);
+    }
+    localPathAssets.add(
+      LocalPathAsset(
+        localIDs: localIDs,
+        pathName: assetPath.name,
+        pathID: assetPath.id,
+      ),
+    );
+  }
+  return localPathAssets;
+}
+
+Future<LocalDiffResult> getDiffWithLocal(
+  List<LocalPathAsset> assets,
+  // current set of assets available on device
+  Set<String> existingIDs, // localIDs of files already imported in app
+  Map<String, Set<String>> pathToLocalIDs,
+  Set<String> invalidIDs,
+  Computer computer,
+) async {
+  final Map<String, dynamic> args = <String, dynamic>{};
+  args['assets'] = assets;
+  args['existingIDs'] = existingIDs;
+  args['invalidIDs'] = invalidIDs;
+  args['pathToLocalIDs'] = pathToLocalIDs;
+  final LocalDiffResult diffResult =
+      await computer.compute(_getLocalAssetsDiff, param: args);
+  diffResult.uniqueLocalFiles =
+      await _convertLocalAssetsToUniqueFiles(diffResult.localPathAssets);
+  return diffResult;
+}
+
+// _getLocalAssetsDiff compares local db with the file system and compute
+// the files which needs to be added or removed from device collection.
+LocalDiffResult _getLocalAssetsDiff(Map<String, dynamic> args) {
+  final List<LocalPathAsset> onDeviceLocalPathAsset = args['assets'];
+  final Set<String> existingIDs = args['existingIDs'];
+  final Set<String> invalidIDs = args['invalidIDs'];
+  final Map<String, Set<String>> pathToLocalIDs = args['pathToLocalIDs'];
+  final Map<String, Set<String>> newPathToLocalIDs = <String, Set<String>>{};
+  final Map<String, Set<String>> removedPathToLocalIDs =
+      <String, Set<String>>{};
+  final List<LocalPathAsset> unsyncedAssets = [];
+
+  for (final localPathAsset in onDeviceLocalPathAsset) {
+    final String pathID = localPathAsset.pathID;
+    // Start identifying pathID to localID mapping changes which needs to be
+    // synced
+    final Set<String> candidateLocalIDsForRemoval =
+        pathToLocalIDs[pathID] ?? <String>{};
+    final Set<String> missingLocalIDsInPath = <String>{};
+    for (final String localID in localPathAsset.localIDs) {
+      if (candidateLocalIDsForRemoval.contains(localID)) {
+        // remove the localID after checking. Any pending existing ID indicates
+        // the the local file was removed from the path.
+        candidateLocalIDsForRemoval.remove(localID);
+      } else {
+        missingLocalIDsInPath.add(localID);
+      }
+    }
+    if (candidateLocalIDsForRemoval.isNotEmpty) {
+      removedPathToLocalIDs[pathID] = candidateLocalIDsForRemoval;
+    }
+    if (missingLocalIDsInPath.isNotEmpty) {
+      newPathToLocalIDs[pathID] = missingLocalIDsInPath;
+    }
+    // End
+
+    localPathAsset.localIDs.removeAll(existingIDs);
+    localPathAsset.localIDs.removeAll(invalidIDs);
+    if (localPathAsset.localIDs.isNotEmpty) {
+      unsyncedAssets.add(localPathAsset);
+    }
+  }
+  return LocalDiffResult(
+    localPathAssets: unsyncedAssets,
+    newPathToLocalIDs: newPathToLocalIDs,
+    deletePathToLocalIDs: removedPathToLocalIDs,
+  );
+}
+
+Future<List<File>> _convertLocalAssetsToUniqueFiles(
+  List<LocalPathAsset> assets,
+) async {
+  final Set<String> alreadySeenLocalIDs = <String>{};
+  final List<File> files = [];
+  for (LocalPathAsset localPathAsset in assets) {
+    final String localPathName = localPathAsset.pathName;
+    for (final String localID in localPathAsset.localIDs) {
+      if (!alreadySeenLocalIDs.contains(localID)) {
+        final assetEntity = await AssetEntity.fromId(localID);
+        files.add(
+          await File.fromAsset(localPathName, assetEntity),
+        );
+        alreadySeenLocalIDs.add(localID);
+      }
+    }
+  }
+  return files;
+}
+
+/// returns a list of AssetPathEntity with relevant filter operations.
+/// [needTitle] impacts the performance for fetching the actual [AssetEntity]
+/// in iOS. Same is true for [containsModifiedPath]
+Future<List<AssetPathEntity>> _getGalleryList({
+  final int updateFromTime,
+  final int updateToTime,
+  final bool containsModifiedPath = false,
+  // in iOS fetching the AssetEntity title impacts performance
+  final bool needsTitle = true,
+  final OrderOption orderOption,
+}) async {
+  final filterOptionGroup = FilterOptionGroup();
+  filterOptionGroup.setOption(
+    AssetType.image,
+    FilterOption(needTitle: needsTitle, sizeConstraint: ignoreSizeConstraint),
+  );
+  filterOptionGroup.setOption(
+    AssetType.video,
+    FilterOption(needTitle: needsTitle, sizeConstraint: ignoreSizeConstraint),
+  );
+
+  if (orderOption != null) {
+    filterOptionGroup.addOrderOption(orderOption);
+  }
+
+  if (updateFromTime != null && updateToTime != null) {
+    filterOptionGroup.updateTimeCond = DateTimeCond(
+      min: DateTime.fromMicrosecondsSinceEpoch(updateFromTime),
+      max: DateTime.fromMicrosecondsSinceEpoch(updateToTime),
+    );
+  }
+  filterOptionGroup.containsPathModified = containsModifiedPath;
+  final galleryList = await PhotoManager.getAssetPathList(
+    hasAll: true,
+    type: RequestType.common,
+    filterOption: filterOptionGroup,
+  );
+  galleryList.sort((s1, s2) {
+    if (s1.isAll) {
+      return 1;
+    }
+    return 0;
+  });
+
+  return galleryList;
+}
+
+Future<List<AssetEntity>> _getAllAssetLists(AssetPathEntity pathEntity) async {
+  final List<AssetEntity> result = [];
+  int currentPage = 0;
+  List<AssetEntity> currentPageResult = [];
+  do {
+    currentPageResult = await pathEntity.getAssetListPaged(
+      page: currentPage,
+      size: assetFetchPageSize,
+    );
+    Bus.instance.fire(
+      LocalImportProgressEvent(pathEntity.name,
+          currentPage * assetFetchPageSize + currentPageResult.length),
+    );
+    result.addAll(currentPageResult);
+    currentPage = currentPage + 1;
+  } while (currentPageResult.length >= assetFetchPageSize);
+  return result;
+}
+
+// review: do we need to run this inside compute, after making File.FromAsset
+// sync. If yes, update the method documentation with reason.
+Future<Tuple2<Set<String>, List<File>>> _getLocalIDsAndFilesFromAssets(
+  Map<String, dynamic> args,
+) async {
+  final pathEntity = args["pathEntity"] as AssetPathEntity;
+  final assetList = args["assetList"];
+  final fromTime = args["fromTime"];
+  final alreadySeenLocalIDs = args["alreadySeenLocalIDs"] as Set<String>;
+  final List<File> files = [];
+  final Set<String> localIDs = {};
+  for (AssetEntity entity in assetList) {
+    localIDs.add(entity.id);
+    final bool assetCreatedOrUpdatedAfterGivenTime = max(
+          entity.createDateTime.microsecondsSinceEpoch,
+          entity.modifiedDateTime.microsecondsSinceEpoch,
+        ) >
+        fromTime;
+    if (!alreadySeenLocalIDs.contains(entity.id) &&
+        assetCreatedOrUpdatedAfterGivenTime) {
+      try {
+        final file = await File.fromAsset(pathEntity.name, entity);
+        files.add(file);
+      } catch (e) {
+        _logger.severe(e);
+      }
+    }
+  }
+  return Tuple2(localIDs, files);
+}
+
+class LocalPathAsset {
+  final Set<String> localIDs;
+  final String pathID;
+  final String pathName;
+
+  LocalPathAsset({
+    @required this.localIDs,
+    @required this.pathName,
+    @required this.pathID,
+  });
+}
+
+class LocalDiffResult {
+  // unique localPath Assets.
+  final List<LocalPathAsset> localPathAssets;
+
+  // set of File object created from localPathAssets
+  List<File> uniqueLocalFiles;
+
+  // newPathToLocalIDs represents new entries which needs to be synced to
+  // the local db
+  final Map<String, Set<String>> newPathToLocalIDs;
+
+  final Map<String, Set<String>> deletePathToLocalIDs;
+
+  LocalDiffResult({
+    this.uniqueLocalFiles,
+    this.localPathAssets,
+    this.newPathToLocalIDs,
+    this.deletePathToLocalIDs,
+  });
+}

+ 11 - 11
lib/services/local_file_update_service.dart

@@ -99,9 +99,9 @@ class LocalFileUpdateService {
     List<String> localIDsToProcess,
   ) async {
     _logger.info("files to process ${localIDsToProcess.length} for reupload");
-    List<ente.File> localFiles =
-        (await FilesDB.instance.getLocalFiles(localIDsToProcess));
-    Set<String> processedIDs = {};
+    final List<ente.File> localFiles =
+        await FilesDB.instance.getLocalFiles(localIDsToProcess);
+    final Set<String> processedIDs = {};
     for (ente.File file in localFiles) {
       if (processedIDs.contains(file.localID)) {
         continue;
@@ -114,10 +114,10 @@ class LocalFileUpdateService {
             file.hash != null &&
             (file.hash == uploadData.hashData.fileHash ||
                 file.hash == uploadData.hashData.zipHash)) {
-          _logger.info("Skip file update as hash matched ${file.tag()}");
+          _logger.info("Skip file update as hash matched ${file.tag}");
         } else {
           _logger.info(
-            "Marking for file update as hash did not match ${file.tag()}",
+            "Marking for file update as hash did not match ${file.tag}",
           );
           await FilesDB.instance.updateUploadedFile(
             file.localID,
@@ -159,14 +159,14 @@ class LocalFileUpdateService {
     }
     // migration only needs to run if Android API Level is 29 or higher
     final int version = int.parse(await PhotoManager.systemVersion());
-    bool isMigrationRequired = version >= 29;
+    final bool isMigrationRequired = version >= 29;
     if (isMigrationRequired) {
       await _importLocalFilesForMigration();
       final sTime = DateTime.now().microsecondsSinceEpoch;
       bool hasData = true;
       const int limitInBatch = 100;
       while (hasData) {
-        var localIDsToProcess =
+        final localIDsToProcess =
             await _fileUpdationDB.getLocalIDsForPotentialReUpload(
           limitInBatch,
           FileUpdationDB.missingLocation,
@@ -190,15 +190,15 @@ class LocalFileUpdateService {
     List<String> localIDsToProcess,
   ) async {
     _logger.info("files to process ${localIDsToProcess.length}");
-    var localIDsWithLocation = <String>[];
+    final localIDsWithLocation = <String>[];
     for (var localID in localIDsToProcess) {
       bool hasLocation = false;
       try {
-        var assetEntity = await AssetEntity.fromId(localID);
+        final assetEntity = await AssetEntity.fromId(localID);
         if (assetEntity == null) {
           continue;
         }
-        var latLng = await assetEntity.latlngAsync();
+        final latLng = await assetEntity.latlngAsync();
         if ((latLng.longitude ?? 0.0) != 0.0 ||
             (latLng.longitude ?? 0.0) != 0.0) {
           _logger.finest(
@@ -227,7 +227,7 @@ class LocalFileUpdateService {
     }
     final sTime = DateTime.now().microsecondsSinceEpoch;
     _logger.info('importing files without location info');
-    var fileLocalIDs = await _filesDB.getLocalFilesBackedUpWithoutLocation();
+    final fileLocalIDs = await _filesDB.getLocalFilesBackedUpWithoutLocation();
     await _fileUpdationDB.insertMultiple(
       fileLocalIDs,
       FileUpdationDB.missingLocation,

+ 177 - 61
lib/services/local_sync_service.dart

@@ -4,18 +4,24 @@ import 'dart:async';
 import 'dart:io';
 
 import 'package:computer/computer.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
+import 'package:photos/db/device_files_db.dart';
 import 'package:photos/db/file_updation_db.dart';
 import 'package:photos/db/files_db.dart';
+import 'package:photos/events/backup_folders_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/events/sync_status_update_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/services/app_lifecycle_service.dart';
-import 'package:photos/utils/file_sync_util.dart';
+import 'package:photos/services/local/local_sync_util.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:sqflite/sqflite.dart';
+import 'package:tuple/tuple.dart';
 
 class LocalSyncService {
   final _logger = Logger("LocalSyncService");
@@ -26,6 +32,7 @@ class LocalSyncService {
 
   static const kDbUpdationTimeKey = "db_updation_time";
   static const kHasCompletedFirstImportKey = "has_completed_firstImport";
+  static const hasImportedDeviceCollections = "has_imported_device_collections";
   static const kHasGrantedPermissionsKey = "has_granted_permissions";
   static const kPermissionStateKey = "permission_state";
   static const kEditedFileIDsKey = "edited_file_ids";
@@ -75,8 +82,8 @@ class LocalSyncService {
     _logger.info(
       existingLocalFileIDs.length.toString() + " localIDs were discovered",
     );
-    final editedFileIDs = getEditedFileIDs().toSet();
-    final downloadedFileIDs = getDownloadedFileIDs().toSet();
+    final editedFileIDs = _getEditedFileIDs().toSet();
+    final downloadedFileIDs = _getDownloadedFileIDs().toSet();
     final syncStartTime = DateTime.now().microsecondsSinceEpoch;
     final lastDBUpdationTime = _prefs.getInt(kDbUpdationTimeKey) ?? 0;
     final startTime = DateTime.now().microsecondsSinceEpoch;
@@ -117,6 +124,9 @@ class LocalSyncService {
     if (!_prefs.containsKey(kHasCompletedFirstImportKey) ||
         !_prefs.getBool(kHasCompletedFirstImportKey)) {
       await _prefs.setBool(kHasCompletedFirstImportKey, true);
+      // mark device collection has imported on first import
+      await _refreshDeviceFolderCountAndCover(isFirstSync: true);
+      await _prefs.setBool(hasImportedDeviceCollections, true);
       _logger.fine("first gallery import finished");
       Bus.instance
           .fire(SyncStatusUpdate(SyncStatus.completedFirstGalleryImport));
@@ -128,41 +138,114 @@ class LocalSyncService {
     _existingSync = null;
   }
 
+  Future<bool> _refreshDeviceFolderCountAndCover({
+    bool isFirstSync = false,
+  }) async {
+    final List<Tuple2<AssetPathEntity, String>> result =
+        await getDeviceFolderWithCountAndCoverID();
+    final bool hasUpdated = await _db.updateDeviceCoverWithCount(
+      result,
+      shouldBackup: Configuration.instance.hasSelectedAllFoldersForBackup(),
+    );
+    // do not fire UI update event during first sync. Otherwise the next screen
+    // to shop the backup folder is skipped
+    if (hasUpdated && !isFirstSync) {
+      Bus.instance.fire(BackupFoldersUpdatedEvent());
+    }
+    // migrate the backed up folder settings after first import is done remove
+    // after 6 months?
+    if (!_prefs.containsKey(hasImportedDeviceCollections) &&
+        _prefs.containsKey(kHasCompletedFirstImportKey)) {
+      await _migrateOldSettings(result);
+    }
+    return hasUpdated;
+  }
+
+  Future<void> _migrateOldSettings(
+    List<Tuple2<AssetPathEntity, String>> result,
+  ) async {
+    final pathsToBackUp = Configuration.instance.getPathsToBackUp();
+    final entriesToBackUp = Map.fromEntries(
+      result
+          .where((element) => pathsToBackUp.contains(element.item1.name))
+          .map((e) => MapEntry(e.item1.id, true)),
+    );
+    if (entriesToBackUp.isNotEmpty) {
+      await _db.updateDevicePathSyncStatus(entriesToBackUp);
+      Bus.instance.fire(BackupFoldersUpdatedEvent());
+    }
+    await Configuration.instance
+        .setHasSelectedAnyBackupFolder(pathsToBackUp.isNotEmpty);
+    await _prefs.setBool(hasImportedDeviceCollections, true);
+  }
+
+  bool isDeviceFileMigrationDone() {
+    return _prefs.containsKey(hasImportedDeviceCollections);
+  }
+
   Future<bool> syncAll() async {
-    final sTime = DateTime.now().microsecondsSinceEpoch;
+    final stopwatch = Stopwatch()..start();
     final localAssets = await getAllLocalAssets();
-    final eTime = DateTime.now().microsecondsSinceEpoch;
-    final d = Duration(microseconds: eTime - sTime);
     _logger.info(
-      "Loading from the beginning returned " +
-          localAssets.length.toString() +
-          " assets and took " +
-          d.inMilliseconds.toString() +
-          "ms",
+      "Loading allLocalAssets ${localAssets.length} took ${stopwatch.elapsedMilliseconds}ms ",
+    );
+    await _refreshDeviceFolderCountAndCover();
+    _logger.info(
+      "refreshDeviceFolderCountAndCover + allLocalAssets took ${stopwatch.elapsedMilliseconds}ms ",
     );
-    final existingIDs = await _db.getExistingLocalFileIDs();
-    final invalidIDs = getInvalidFileIDs().toSet();
-    final unsyncedFiles =
-        await getUnsyncedFiles(localAssets, existingIDs, invalidIDs, _computer);
-    if (unsyncedFiles.isNotEmpty) {
-      await _db.insertMultiple(unsyncedFiles);
+    final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
+    final Map<String, Set<String>> pathToLocalIDs =
+        await _db.getDevicePathIDToLocalIDMap();
+    final invalidIDs = _getInvalidFileIDs().toSet();
+    final localDiffResult = await getDiffWithLocal(
+      localAssets,
+      existingLocalFileIDs,
+      pathToLocalIDs,
+      invalidIDs,
+      _computer,
+    );
+    bool hasAnyMappingChanged = false;
+    if (localDiffResult.newPathToLocalIDs?.isNotEmpty ?? false) {
+      await _db.insertPathIDToLocalIDMapping(localDiffResult.newPathToLocalIDs);
+      hasAnyMappingChanged = true;
+    }
+    if (localDiffResult.deletePathToLocalIDs?.isNotEmpty ?? false) {
+      await _db
+          .deletePathIDToLocalIDMapping(localDiffResult.deletePathToLocalIDs);
+      hasAnyMappingChanged = true;
+    }
+    final bool hasUnsyncedFiles =
+        localDiffResult.uniqueLocalFiles?.isNotEmpty ?? false;
+    if (hasUnsyncedFiles) {
+      await _db.insertMultiple(
+        localDiffResult.uniqueLocalFiles,
+        conflictAlgorithm: ConflictAlgorithm.ignore,
+      );
       _logger.info(
-        "Inserted " + unsyncedFiles.length.toString() + " unsynced files.",
+        "Inserted ${localDiffResult.uniqueLocalFiles.length} "
+        "un-synced files",
+      );
+    }
+    debugPrint(
+      "syncAll: mappingChange : $hasAnyMappingChanged, "
+      "unSyncedFiles: $hasUnsyncedFiles",
+    );
+    if (hasAnyMappingChanged || hasUnsyncedFiles) {
+      Bus.instance.fire(
+        LocalPhotosUpdatedEvent(localDiffResult.uniqueLocalFiles),
       );
-      _updatePathsToBackup(unsyncedFiles);
-      Bus.instance.fire(LocalPhotosUpdatedEvent(unsyncedFiles));
-      return true;
     }
-    return false;
+    _logger.info("syncAll took ${stopwatch.elapsed.inMilliseconds}ms ");
+    return hasUnsyncedFiles;
   }
 
   Future<void> trackEditedFile(File file) async {
-    final editedIDs = getEditedFileIDs();
+    final editedIDs = _getEditedFileIDs();
     editedIDs.add(file.localID);
     await _prefs.setStringList(kEditedFileIDsKey, editedIDs);
   }
 
-  List<String> getEditedFileIDs() {
+  List<String> _getEditedFileIDs() {
     if (_prefs.containsKey(kEditedFileIDsKey)) {
       return _prefs.getStringList(kEditedFileIDsKey);
     } else {
@@ -172,32 +255,30 @@ class LocalSyncService {
   }
 
   Future<void> trackDownloadedFile(String localID) async {
-    final downloadedIDs = getDownloadedFileIDs();
+    final downloadedIDs = _getDownloadedFileIDs();
     downloadedIDs.add(localID);
     await _prefs.setStringList(kDownloadedFileIDsKey, downloadedIDs);
   }
 
-  List<String> getDownloadedFileIDs() {
+  List<String> _getDownloadedFileIDs() {
     if (_prefs.containsKey(kDownloadedFileIDsKey)) {
       return _prefs.getStringList(kDownloadedFileIDsKey);
     } else {
-      final List<String> downloadedIDs = [];
-      return downloadedIDs;
+      return <String>[];
     }
   }
 
   Future<void> trackInvalidFile(File file) async {
-    final invalidIDs = getInvalidFileIDs();
+    final invalidIDs = _getInvalidFileIDs();
     invalidIDs.add(file.localID);
     await _prefs.setStringList(kInvalidFileIDsKey, invalidIDs);
   }
 
-  List<String> getInvalidFileIDs() {
+  List<String> _getInvalidFileIDs() {
     if (_prefs.containsKey(kInvalidFileIDsKey)) {
       return _prefs.getStringList(kInvalidFileIDsKey);
     } else {
-      final List<String> invalidIDs = [];
-      return invalidIDs;
+      return <String>[];
     }
   }
 
@@ -213,6 +294,11 @@ class LocalSyncService {
   Future<void> onPermissionGranted(PermissionState state) async {
     await _prefs.setBool(kHasGrantedPermissionsKey, true);
     await _prefs.setString(kPermissionStateKey, state.toString());
+    if (state == PermissionState.limited) {
+      // when limited permission is granted, by default mark all folders for
+      // backup
+      await Configuration.instance.setSelectAllFoldersForBackup(true);
+    }
     _registerChangeCallback();
   }
 
@@ -220,6 +306,24 @@ class LocalSyncService {
     return _prefs.getBool(kHasCompletedFirstImportKey) ?? false;
   }
 
+  // Warning: resetLocalSync should only be used for testing imported related
+  // changes
+  Future<void> resetLocalSync() async {
+    assert(kDebugMode, "only available in debug mode");
+    await FilesDB.instance.deleteDB();
+    for (var element in [
+      kHasCompletedFirstImportKey,
+      hasImportedDeviceCollections,
+      kDbUpdationTimeKey,
+      kDownloadedFileIDsKey,
+      kEditedFileIDsKey,
+      "has_synced_edit_time",
+      "has_selected_all_folders_for_backup",
+    ]) {
+      await _prefs.remove(element);
+    }
+  }
+
   Future<void> _loadAndStorePhotos(
     int fromTime,
     int toTime,
@@ -233,46 +337,58 @@ class LocalSyncService {
           " to " +
           DateTime.fromMicrosecondsSinceEpoch(toTime).toString(),
     );
-    final files = await getDeviceFiles(fromTime, toTime, _computer);
+    final Tuple2<List<LocalPathAsset>, List<File>> result =
+        await getLocalPathAssetsAndFiles(fromTime, toTime, _computer);
+    await FilesDB.instance.insertLocalAssets(
+      result.item1,
+      shouldAutoBackup: Configuration.instance.hasSelectedAllFoldersForBackup(),
+    );
+    final List<File> files = result.item2;
     if (files.isNotEmpty) {
       _logger.info("Fetched " + files.length.toString() + " files.");
-      final updatedFiles = files
-          .where((file) => existingLocalFileIDs.contains(file.localID))
-          .toList();
-      updatedFiles.removeWhere((file) => editedFileIDs.contains(file.localID));
-      updatedFiles
-          .removeWhere((file) => downloadedFileIDs.contains(file.localID));
-      if (updatedFiles.isNotEmpty) {
-        _logger.info(
-          updatedFiles.length.toString() + " local files were updated.",
-        );
-      }
-
-      final List<String> updatedLocalIDs = [];
-      for (final file in updatedFiles) {
-        updatedLocalIDs.add(file.localID);
-      }
-      await FileUpdationDB.instance.insertMultiple(
-        updatedLocalIDs,
-        FileUpdationDB.modificationTimeUpdated,
+      await _trackUpdatedFiles(
+        files,
+        existingLocalFileIDs,
+        editedFileIDs,
+        downloadedFileIDs,
       );
       final List<File> allFiles = [];
       allFiles.addAll(files);
       files.removeWhere((file) => existingLocalFileIDs.contains(file.localID));
-      await _db.insertMultiple(files);
+      await _db.insertMultiple(
+        files,
+        conflictAlgorithm: ConflictAlgorithm.ignore,
+      );
       _logger.info("Inserted " + files.length.toString() + " files.");
-      _updatePathsToBackup(files);
       Bus.instance.fire(LocalPhotosUpdatedEvent(allFiles));
     }
     await _prefs.setInt(kDbUpdationTimeKey, toTime);
   }
 
-  void _updatePathsToBackup(List<File> files) {
-    if (Configuration.instance.hasSelectedAllFoldersForBackup()) {
-      final pathsToBackup = Configuration.instance.getPathsToBackUp();
-      final newFilePaths = files.map((file) => file.deviceFolder).toList();
-      pathsToBackup.addAll(newFilePaths);
-      Configuration.instance.setPathsToBackUp(pathsToBackup);
+  Future<void> _trackUpdatedFiles(
+    List<File> files,
+    Set<String> existingLocalFileIDs,
+    Set<String> editedFileIDs,
+    Set<String> downloadedFileIDs,
+  ) async {
+    final updatedFiles = files
+        .where((file) => existingLocalFileIDs.contains(file.localID))
+        .toList();
+    updatedFiles.removeWhere((file) => editedFileIDs.contains(file.localID));
+    updatedFiles
+        .removeWhere((file) => downloadedFileIDs.contains(file.localID));
+    if (updatedFiles.isNotEmpty) {
+      _logger.info(
+        updatedFiles.length.toString() + " local files were updated.",
+      );
+      final List<String> updatedLocalIDs = [];
+      for (final file in updatedFiles) {
+        updatedLocalIDs.add(file.localID);
+      }
+      await FileUpdationDB.instance.insertMultiple(
+        updatedLocalIDs,
+        FileUpdationDB.modificationTimeUpdated,
+      );
     }
   }
 
@@ -287,7 +403,7 @@ class LocalSyncService {
       if (hasGrantedLimitedPermissions()) {
         syncAll();
       } else {
-        sync();
+        sync().then((value) => _refreshDeviceFolderCountAndCover());
       }
     });
     PhotoManager.startChangeNotify();

+ 1 - 1
lib/services/memories_service.dart

@@ -33,7 +33,7 @@ class MemoriesService extends ChangeNotifier {
     // Intention of delay is to give more CPU cycles to other tasks
     Future.delayed(const Duration(seconds: 5), () {
       _memoriesDB.clearMemoriesSeenBeforeTime(
-        DateTime.now().microsecondsSinceEpoch - (7 * kMicroSecondsInDay),
+        DateTime.now().microsecondsSinceEpoch - (7 * microSecondsInDay),
       );
     });
   }

+ 1 - 3
lib/services/notification_service.dart

@@ -1,5 +1,3 @@
-// @dart=2.9
-
 import 'dart:io';
 
 import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@@ -28,7 +26,7 @@ class NotificationService {
     );
   }
 
-  Future selectNotification(String payload) async {}
+  Future selectNotification(String? payload) async {}
 
   Future<void> showNotification(String title, String message) async {
     if (!Platform.isAndroid) {

+ 1 - 2
lib/services/push_service.dart

@@ -15,8 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart';
 class PushService {
   static const kFCMPushToken = "fcm_push_token";
   static const kLastFCMTokenUpdationTime = "fcm_push_token_updation_time";
-  static const kFCMTokenUpdationIntervalInMicroSeconds =
-      30 * kMicroSecondsInDay;
+  static const kFCMTokenUpdationIntervalInMicroSeconds = 30 * microSecondsInDay;
   static const kPushAction = "action";
   static const kSync = "sync";
 

+ 197 - 46
lib/services/remote_sync_service.dart

@@ -4,24 +4,30 @@ import 'dart:async';
 import 'dart:io';
 import 'dart:math';
 
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/event_bus.dart';
+import 'package:photos/db/device_files_db.dart';
 import 'package:photos/db/file_updation_db.dart';
 import 'package:photos/db/files_db.dart';
+import 'package:photos/events/backup_folders_updated_event.dart';
 import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/force_reload_home_gallery_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/events/sync_status_update_event.dart';
+import 'package:photos/models/device_collection.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file_type.dart';
+import 'package:photos/models/upload_strategy.dart';
 import 'package:photos/services/app_lifecycle_service.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_file_update_service.dart';
-import 'package:photos/services/local_sync_service.dart';
+import 'package:photos/services/sync_service.dart';
 import 'package:photos/services/trash_sync_service.dart';
 import 'package:photos/utils/diff_fetcher.dart';
 import 'package:photos/utils/file_uploader.dart';
@@ -31,9 +37,10 @@ import 'package:shared_preferences/shared_preferences.dart';
 class RemoteSyncService {
   final _logger = Logger("RemoteSyncService");
   final _db = FilesDB.instance;
-  final _uploader = FileUploader.instance;
-  final _collectionsService = CollectionsService.instance;
-  final _diffFetcher = DiffFetcher();
+  final FileUploader _uploader = FileUploader.instance;
+  final Configuration _config = Configuration.instance;
+  final CollectionsService _collectionsService = CollectionsService.instance;
+  final DiffFetcher _diffFetcher = DiffFetcher();
   final LocalFileUpdateService _localFileUpdateService =
       LocalFileUpdateService.instance;
   int _completedUploads = 0;
@@ -42,6 +49,7 @@ class RemoteSyncService {
   bool _existingSyncSilent = false;
 
   static const kHasSyncedArchiveKey = "has_synced_archive";
+  final String _isFirstRemoteSyncDone = "isFirstRemoteSyncDone";
 
   // 28 Sept, 2021 9:03:20 AM IST
   static const kArchiveFeatureReleaseTime = 1632800000000000;
@@ -70,7 +78,7 @@ class RemoteSyncService {
   }
 
   Future<void> sync({bool silently = false}) async {
-    if (!Configuration.instance.hasConfiguredAccount()) {
+    if (!_config.hasConfiguredAccount()) {
       _logger.info("Skipping remote sync since account is not configured");
       return;
     }
@@ -87,6 +95,15 @@ class RemoteSyncService {
     _existingSyncSilent = silently;
 
     try {
+      // use flag to decide if we should start marking files for upload before
+      // remote-sync is done. This is done to avoid adding existing files to
+      // the same or different collection when user had already uploaded them
+      // before.
+      final bool hasSyncedBefore =
+          _prefs.containsKey(_isFirstRemoteSyncDone) ?? false;
+      if (hasSyncedBefore) {
+        await syncDeviceCollectionFilesForUpload();
+      }
       await _pullDiff();
       // sync trash but consume error during initial launch.
       // this is to ensure that we don't pause upload due to any error during
@@ -95,12 +112,17 @@ class RemoteSyncService {
       await TrashSyncService.instance
           .syncTrash()
           .onError((e, s) => _logger.severe('trash sync failed', e, s));
+      if (!hasSyncedBefore) {
+        await _prefs.setBool(_isFirstRemoteSyncDone, true);
+        await syncDeviceCollectionFilesForUpload();
+      }
       final filesToBeUploaded = await _getFilesToBeUploaded();
       final hasUploadedFiles = await _uploadFiles(filesToBeUploaded);
       if (hasUploadedFiles) {
         await _pullDiff();
         _existingSync.complete();
         _existingSync = null;
+        await syncDeviceCollectionFilesForUpload();
         final hasMoreFilesToBackup = (await _getFilesToBeUploaded()).isNotEmpty;
         if (hasMoreFilesToBackup && !_shouldThrottleSync()) {
           // Skipping a resync to ensure that files that were ignored in this
@@ -180,9 +202,8 @@ class RemoteSyncService {
         await _diffFetcher.getEncryptedFilesDiff(collectionID, sinceTime);
     if (diff.deletedFiles.isNotEmpty) {
       final fileIDs = diff.deletedFiles.map((f) => f.uploadedFileID).toList();
-      final deletedFiles =
-          (await FilesDB.instance.getFilesFromIDs(fileIDs)).values.toList();
-      await FilesDB.instance.deleteFilesFromCollection(collectionID, fileIDs);
+      final deletedFiles = (await _db.getFilesFromIDs(fileIDs)).values.toList();
+      await _db.deleteFilesFromCollection(collectionID, fileIDs);
       Bus.instance.fire(
         CollectionUpdatedEvent(
           collectionID,
@@ -224,17 +245,165 @@ class RemoteSyncService {
     }
   }
 
-  Future<List<File>> _getFilesToBeUploaded() async {
-    final foldersToBackUp = Configuration.instance.getPathsToBackUp();
-    List<File> filesToBeUploaded;
-    if (LocalSyncService.instance.hasGrantedLimitedPermissions() &&
-        foldersToBackUp.isEmpty) {
-      filesToBeUploaded = await _db.getAllLocalFiles();
-    } else {
-      filesToBeUploaded =
-          await _db.getFilesToBeUploadedWithinFolders(foldersToBackUp);
+  Future<void> syncDeviceCollectionFilesForUpload() async {
+    final int ownerID = _config.getUserID();
+    final deviceCollections = await _db.getDeviceCollections();
+    deviceCollections.removeWhere((element) => !element.shouldBackup);
+    // Sort by count to ensure that photos in iOS are first inserted in
+    // smallest album marked for backup. This is to ensure that photo is
+    // first attempted to upload in a non-recent album.
+    deviceCollections.sort((a, b) => a.count.compareTo(b.count));
+    await _createCollectionsForDevicePath(deviceCollections);
+    final Map<String, Set<String>> pathIdToLocalIDs =
+        await _db.getDevicePathIDToLocalIDMap();
+    for (final deviceCollection in deviceCollections) {
+      _logger.fine("processing ${deviceCollection.name}");
+      final Set<String> localIDsToSync =
+          pathIdToLocalIDs[deviceCollection.id] ?? {};
+      if (deviceCollection.uploadStrategy == UploadStrategy.ifMissing) {
+        final Set<String> alreadyClaimedLocalIDs =
+            await _db.getLocalIDsMarkedForOrAlreadyUploaded(ownerID);
+        localIDsToSync.removeAll(alreadyClaimedLocalIDs);
+      }
+
+      if (localIDsToSync.isEmpty || deviceCollection.collectionID == -1) {
+        continue;
+      }
+
+      await _db.setCollectionIDForUnMappedLocalFiles(
+        deviceCollection.collectionID,
+        localIDsToSync,
+      );
+
+      // mark IDs as already synced if corresponding entry is present in
+      // the collection. This can happen when a user has marked a folder
+      // for sync, then un-synced it and again tries to mark if for sync.
+      final Set<String> existingMapping =
+          await _db.getLocalFileIDsForCollection(deviceCollection.collectionID);
+      final Set<String> commonElements =
+          localIDsToSync.intersection(existingMapping);
+      if (commonElements.isNotEmpty) {
+        debugPrint(
+          "${commonElements.length} files already existing in "
+          "collection ${deviceCollection.collectionID} for ${deviceCollection.name}",
+        );
+        localIDsToSync.removeAll(commonElements);
+      }
+
+      // At this point, the remaining localIDsToSync will need to create
+      // new file entries, where we can store mapping for localID and
+      // corresponding collection ID
+      if (localIDsToSync.isNotEmpty) {
+        debugPrint(
+          'Adding new entries for ${localIDsToSync.length} files'
+          ' for ${deviceCollection.name}',
+        );
+        final filesWithCollectionID =
+            await _db.getLocalFiles(localIDsToSync.toList());
+        final List<File> newFilesToInsert = [];
+        final Set<String> fileFoundForLocalIDs = {};
+        for (var existingFile in filesWithCollectionID) {
+          final String localID = existingFile.localID;
+          if (!fileFoundForLocalIDs.contains(localID)) {
+            existingFile.generatedID = null;
+            existingFile.collectionID = deviceCollection.collectionID;
+            existingFile.uploadedFileID = null;
+            existingFile.ownerID = null;
+            newFilesToInsert.add(existingFile);
+            fileFoundForLocalIDs.add(localID);
+          }
+        }
+        await _db.insertMultiple(newFilesToInsert);
+        if (fileFoundForLocalIDs.length != localIDsToSync.length) {
+          _logger.warning(
+            "mismatch in num of filesToSync ${localIDsToSync.length} to "
+            "fileSynced ${fileFoundForLocalIDs.length}",
+          );
+        }
+      }
+    }
+  }
+
+  Future<void> updateDeviceFolderSyncStatus(
+    Map<String, bool> syncStatusUpdate,
+  ) async {
+    final Set<int> oldCollectionIDsForAutoSync =
+        await _db.getDeviceSyncCollectionIDs();
+    await _db.updateDevicePathSyncStatus(syncStatusUpdate);
+    final Set<int> newCollectionIDsForAutoSync =
+        await _db.getDeviceSyncCollectionIDs();
+    SyncService.instance.onDeviceCollectionSet(newCollectionIDsForAutoSync);
+    // remove all collectionIDs which are still marked for backup
+    oldCollectionIDsForAutoSync.removeAll(newCollectionIDsForAutoSync);
+    await removeFilesQueuedForUpload(oldCollectionIDsForAutoSync.toList());
+    Bus.instance.fire(LocalPhotosUpdatedEvent(<File>[]));
+    Bus.instance.fire(BackupFoldersUpdatedEvent());
+  }
+
+  Future<void> removeFilesQueuedForUpload(List<int> collectionIDs) async {
+    /*
+      For each collection, perform following action
+      1) Get List of all files not uploaded yet
+      2) Delete files who localIDs is also present in other collections.
+      3) For Remaining files, set the collectionID as -1
+     */
+    debugPrint("Removing files for collections $collectionIDs");
+    for (int collectionID in collectionIDs) {
+      final List<File> pendingUploads =
+          await _db.getPendingUploadForCollection(collectionID);
+      if (pendingUploads.isEmpty) {
+        continue;
+      }
+      final Set<String> localIDsInOtherFileEntries =
+          await _db.getLocalIDsPresentInEntries(
+        pendingUploads,
+        collectionID,
+      );
+      final List<File> entriesToUpdate = [];
+      final List<int> entriesToDelete = [];
+      for (File pendingUpload in pendingUploads) {
+        if (localIDsInOtherFileEntries.contains(pendingUpload.localID)) {
+          entriesToDelete.add(pendingUpload.generatedID);
+        } else {
+          pendingUpload.collectionID = null;
+          entriesToUpdate.add(pendingUpload);
+        }
+      }
+      await _db.deleteMultipleByGeneratedIDs(entriesToDelete);
+      await _db.insertMultiple(entriesToUpdate);
     }
-    if (!Configuration.instance.shouldBackupVideos() || _shouldThrottleSync()) {
+  }
+
+  Future<void> _createCollectionsForDevicePath(
+    List<DeviceCollection> deviceCollections,
+  ) async {
+    for (var deviceCollection in deviceCollections) {
+      int deviceCollectionID = deviceCollection.collectionID;
+      if (deviceCollectionID != -1) {
+        final collectionByID =
+            _collectionsService.getCollectionByID(deviceCollectionID);
+        if (collectionByID == null || collectionByID.isDeleted) {
+          _logger.info(
+            "Collection $deviceCollectionID either deleted or missing "
+            "for path ${deviceCollection.name}",
+          );
+          deviceCollectionID = -1;
+        }
+      }
+      if (deviceCollectionID == -1) {
+        final collection =
+            await _collectionsService.getOrCreateForPath(deviceCollection.name);
+        await _db.updateDeviceCollection(deviceCollection.id, collection.id);
+        deviceCollection.collectionID = collection.id;
+      }
+    }
+  }
+
+  Future<List<File>> _getFilesToBeUploaded() async {
+    final deviceCollections = await _db.getDeviceCollections();
+    deviceCollections.removeWhere((element) => !element.shouldBackup);
+    final List<File> filesToBeUploaded = await _db.getFilesPendingForUpload();
+    if (!_config.shouldBackupVideos() || _shouldThrottleSync()) {
       filesToBeUploaded
           .removeWhere((element) => element.fileType == FileType.video);
     }
@@ -252,11 +421,6 @@ class RemoteSyncService {
         );
       }
     }
-    if (filesToBeUploaded.isEmpty) {
-      // look for files which user manually tried to back up but they are not
-      // uploaded yet. These files should ignore video backup & ignored files filter
-      filesToBeUploaded = await _db.getPendingManualUploads();
-    }
     _sortByTimeAndType(filesToBeUploaded);
     _logger.info(
       filesToBeUploaded.length.toString() + " new files to be uploaded.",
@@ -265,16 +429,14 @@ class RemoteSyncService {
   }
 
   Future<bool> _uploadFiles(List<File> filesToBeUploaded) async {
-    final updatedFileIDs = await _db.getUploadedFileIDsToBeUpdated();
-    _logger.info(updatedFileIDs.length.toString() + " files updated.");
-
-    final editedFiles = await _db.getEditedRemoteFiles();
-    _logger.info(editedFiles.length.toString() + " files edited.");
+    final int ownerID = _config.getUserID();
+    final updatedFileIDs = await _db.getUploadedFileIDsToBeUpdated(ownerID);
+    if (updatedFileIDs.isNotEmpty) {
+      _logger.info("Identified ${updatedFileIDs.length} files for reupload");
+    }
 
     _completedUploads = 0;
-    final int toBeUploaded =
-        filesToBeUploaded.length + updatedFileIDs.length + editedFiles.length;
-
+    final int toBeUploaded = filesToBeUploaded.length + updatedFileIDs.length;
     if (toBeUploaded > 0) {
       Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload));
       // verify if files upload is allowed based on their subscription plan and
@@ -303,21 +465,10 @@ class RemoteSyncService {
       // prefer existing collection ID for manually uploaded files.
       // See https://github.com/ente-io/frame/pull/187
       final collectionID = file.collectionID ??
-          (await CollectionsService.instance
-                  .getOrCreateForPath(file.deviceFolder))
-              .id;
+          (await _collectionsService.getOrCreateForPath(file.deviceFolder)).id;
       _uploadFile(file, collectionID, futures);
     }
 
-    for (final file in editedFiles) {
-      if (_shouldThrottleSync() &&
-          futures.length >= kMaximumPermissibleUploadsInThrottledMode) {
-        _logger.info("Skipping some edited files as we are throttling uploads");
-        break;
-      }
-      _uploadFile(file, file.collectionID, futures);
-    }
-
     try {
       await Future.wait(futures);
     } on InvalidFileError {
@@ -348,8 +499,7 @@ class RemoteSyncService {
   Future<void> _onFileUploaded(File file) async {
     Bus.instance.fire(CollectionUpdatedEvent(file.collectionID, [file]));
     _completedUploads++;
-    final toBeUploadedInThisSession =
-        FileUploader.instance.getCurrentSessionUploadCount();
+    final toBeUploadedInThisSession = _uploader.getCurrentSessionUploadCount();
     if (toBeUploadedInThisSession == 0) {
       return;
     }
@@ -396,7 +546,7 @@ class RemoteSyncService {
         localUploadedFromDevice = 0,
         localButUpdatedOnDevice = 0,
         remoteNewFile = 0;
-    final int userID = Configuration.instance.getUserID();
+    final int userID = _config.getUserID();
     bool needsGalleryReload = false;
     // this is required when same file is uploaded twice in the same
     // collection. Without this check, if both remote files are part of same
@@ -507,6 +657,7 @@ class RemoteSyncService {
           " remoteFiles seen first time",
     );
     if (needsGalleryReload) {
+      _logger.fine('force reload home gallery');
       Bus.instance.fire(ForceReloadHomeGalleryEvent());
     }
   }

+ 204 - 55
lib/services/search_service.dart

@@ -13,15 +13,15 @@ import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/models/file.dart';
+import 'package:photos/models/file_type.dart';
 import 'package:photos/models/location.dart';
 import 'package:photos/models/search/album_search_result.dart';
-import 'package:photos/models/search/holiday_search_result.dart';
+import 'package:photos/models/search/generic_search_result.dart';
 import 'package:photos/models/search/location_api_response.dart';
-import 'package:photos/models/search/location_search_result.dart';
-import 'package:photos/models/search/month_search_result.dart';
-import 'package:photos/models/search/year_search_result.dart';
+import 'package:photos/models/search/search_result.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/utils/date_time_util.dart';
+import 'package:tuple/tuple.dart';
 
 class SearchService {
   Future<List<File>> _cachedFilesFuture;
@@ -32,6 +32,7 @@ class SearchService {
   static const _maximumResultsLimit = 20;
 
   SearchService._privateConstructor();
+
   static final SearchService instance = SearchService._privateConstructor();
 
   Future<void> init() async {
@@ -40,33 +41,33 @@ class SearchService {
       /* In case home screen loads before 5 seconds and user starts search,
        future will not be null.So here getAllFiles won't run again in that case. */
       if (_cachedFilesFuture == null) {
-        getAllFiles();
+        _getAllFiles();
       }
     });
 
     Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
       _cachedFilesFuture = null;
-      getAllFiles();
+      _getAllFiles();
     });
   }
 
-  Future<List<File>> getAllFiles() async {
+  Future<List<File>> _getAllFiles() async {
     if (_cachedFilesFuture != null) {
       return _cachedFilesFuture;
     }
+    _logger.fine("Reading all files from db");
     _cachedFilesFuture = FilesDB.instance.getAllFilesFromDB();
     return _cachedFilesFuture;
   }
 
   Future<List<File>> getFileSearchResults(String query) async {
     final List<File> fileSearchResults = [];
-    final List<File> files = await getAllFiles();
-    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
+    final List<File> files = await _getAllFiles();
     for (var file in files) {
       if (fileSearchResults.length >= _maximumResultsLimit) {
         break;
       }
-      if (file.title.contains(nonCaseSensitiveRegexForQuery)) {
+      if (file.title.toLowerCase().contains(query.toLowerCase())) {
         fileSearchResults.add(file);
       }
     }
@@ -77,12 +78,12 @@ class SearchService {
     _cachedFilesFuture = null;
   }
 
-  Future<List<LocationSearchResult>> getLocationSearchResults(
+  Future<List<GenericSearchResult>> getLocationSearchResults(
     String query,
   ) async {
-    final List<LocationSearchResult> locationSearchResults = [];
+    final List<GenericSearchResult> searchResults = [];
     try {
-      final List<File> allFiles = await SearchService.instance.getAllFiles();
+      final List<File> allFiles = await _getAllFiles();
 
       final response = await _dio.get(
         _config.getHttpEndpoint() + "/search/location",
@@ -108,15 +109,19 @@ class SearchService {
           (first, second) => second.creationTime.compareTo(first.creationTime),
         );
         if (filesInLocation.isNotEmpty) {
-          locationSearchResults.add(
-            LocationSearchResult(locationData.place, filesInLocation),
+          searchResults.add(
+            GenericSearchResult(
+              ResultType.location,
+              locationData.place,
+              filesInLocation,
+            ),
           );
         }
       }
     } catch (e) {
       _logger.severe(e);
     }
-    return locationSearchResults;
+    return searchResults;
   }
 
   // getFilteredCollectionsWithThumbnail removes deleted or archived or
@@ -124,8 +129,6 @@ class SearchService {
   Future<List<AlbumSearchResult>> getCollectionSearchResults(
     String query,
   ) async {
-    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
-
     /*latestCollectionFiles is to identify collections which have at least one file as we don't display
      empty collections and to get the file to pass for tumbnail */
     final List<File> latestCollectionFiles =
@@ -140,7 +143,7 @@ class SearchService {
       final Collection collection =
           CollectionsService.instance.getCollectionByID(file.collectionID);
       if (!collection.isArchived() &&
-          collection.name.contains(nonCaseSensitiveRegexForQuery)) {
+          collection.name.toLowerCase().contains(query.toLowerCase())) {
         collectionSearchResults
             .add(AlbumSearchResult(CollectionWithThumbnail(collection, file)));
       }
@@ -149,16 +152,17 @@ class SearchService {
     return collectionSearchResults;
   }
 
-  Future<List<YearSearchResult>> getYearSearchResults(
+  Future<List<GenericSearchResult>> getYearSearchResults(
     String yearFromQuery,
   ) async {
-    final List<YearSearchResult> yearSearchResults = [];
+    final List<GenericSearchResult> searchResults = [];
     for (var yearData in YearsData.instance.yearsData) {
       if (yearData.year.startsWith(yearFromQuery)) {
         final List<File> filesInYear = await _getFilesInYear(yearData.duration);
         if (filesInYear.isNotEmpty) {
-          yearSearchResults.add(
-            YearSearchResult(
+          searchResults.add(
+            GenericSearchResult(
+              ResultType.year,
               yearData.year,
               filesInYear,
             ),
@@ -166,58 +170,147 @@ class SearchService {
         }
       }
     }
-    return yearSearchResults;
+    return searchResults;
   }
 
-  Future<List<HolidaySearchResult>> getHolidaySearchResults(
+  Future<List<GenericSearchResult>> getHolidaySearchResults(
     String query,
   ) async {
-    final List<HolidaySearchResult> holidaySearchResults = [];
-
-    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
+    final List<GenericSearchResult> searchResults = [];
 
     for (var holiday in allHolidays) {
-      if (holiday.name.contains(nonCaseSensitiveRegexForQuery)) {
+      if (holiday.name.toLowerCase().contains(query.toLowerCase())) {
         final matchedFiles =
             await FilesDB.instance.getFilesCreatedWithinDurations(
-          _getDurationsOfHolidayInEveryYear(holiday.day, holiday.month),
+          _getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month),
           null,
           order: 'DESC',
         );
         if (matchedFiles.isNotEmpty) {
-          holidaySearchResults.add(
-            HolidaySearchResult(holiday.name, matchedFiles),
+          searchResults.add(
+            GenericSearchResult(ResultType.event, holiday.name, matchedFiles),
           );
         }
       }
     }
-    return holidaySearchResults;
+    return searchResults;
   }
 
-  Future<List<MonthSearchResult>> getMonthSearchResults(String query) async {
-    final List<MonthSearchResult> monthSearchResults = [];
-    final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
-
-    for (var month in allMonths) {
-      if (month.name.startsWith(nonCaseSensitiveRegexForQuery)) {
+  Future<List<GenericSearchResult>> getFileTypeResults(
+    String query,
+  ) async {
+    final List<GenericSearchResult> searchResults = [];
+    final List<File> allFiles = await _getAllFiles();
+    for (var fileType in FileType.values) {
+      final String fileTypeString = getHumanReadableString(fileType);
+      if (fileTypeString.toLowerCase().startsWith(query.toLowerCase())) {
         final matchedFiles =
-            await FilesDB.instance.getFilesCreatedWithinDurations(
-          _getDurationsOfMonthInEveryYear(month.monthNumber),
-          null,
-          order: 'DESC',
-        );
+            allFiles.where((e) => e.fileType == fileType).toList();
         if (matchedFiles.isNotEmpty) {
-          monthSearchResults.add(
-            MonthSearchResult(
-              month.name,
+          searchResults.add(
+            GenericSearchResult(
+              ResultType.fileType,
+              fileTypeString,
               matchedFiles,
             ),
           );
         }
       }
     }
+    return searchResults;
+  }
+
+  Future<List<GenericSearchResult>> getFileExtensionResults(
+    String query,
+  ) async {
+    final List<GenericSearchResult> searchResults = [];
+    if (!query.startsWith(".")) {
+      return searchResults;
+    }
 
-    return monthSearchResults;
+    final List<File> allFiles = await _getAllFiles();
+    final Map<String, List<File>> resultMap = <String, List<File>>{};
+
+    for (File eachFile in allFiles) {
+      final String fileName = eachFile.displayName;
+      if (fileName.contains(query)) {
+        final String exnType = fileName.split(".").last.toUpperCase();
+        if (!resultMap.containsKey(exnType)) {
+          resultMap[exnType] = <File>[];
+        }
+        resultMap[exnType].add(eachFile);
+      }
+    }
+    for (MapEntry<String, List<File>> entry in resultMap.entries) {
+      searchResults.add(
+        GenericSearchResult(
+          ResultType.fileExtension,
+          entry.key.toUpperCase(),
+          entry.value,
+        ),
+      );
+    }
+    return searchResults;
+  }
+
+  Future<List<GenericSearchResult>> getMonthSearchResults(String query) async {
+    final List<GenericSearchResult> searchResults = [];
+    for (var month in _getMatchingMonths(query)) {
+      final matchedFiles =
+          await FilesDB.instance.getFilesCreatedWithinDurations(
+        _getDurationsOfMonthInEveryYear(month.monthNumber),
+        null,
+        order: 'DESC',
+      );
+      if (matchedFiles.isNotEmpty) {
+        searchResults.add(
+          GenericSearchResult(
+            ResultType.month,
+            month.name,
+            matchedFiles,
+          ),
+        );
+      }
+    }
+    return searchResults;
+  }
+
+  Future<List<GenericSearchResult>> getDateResults(
+    String query,
+  ) async {
+    final List<GenericSearchResult> searchResults = [];
+    final potentialDates = _getPossibleEventDate(query);
+
+    for (var potentialDate in potentialDates) {
+      final int day = potentialDate.item1;
+      final int month = potentialDate.item2.monthNumber;
+      final int year = potentialDate.item3; // nullable
+      final matchedFiles =
+          await FilesDB.instance.getFilesCreatedWithinDurations(
+        _getDurationsForCalendarDateInEveryYear(day, month, year: year),
+        null,
+        order: 'DESC',
+      );
+      if (matchedFiles.isNotEmpty) {
+        searchResults.add(
+          GenericSearchResult(
+            ResultType.event,
+            '$day ${potentialDate.item2.name} ${year ?? ''}',
+            matchedFiles,
+          ),
+        );
+      }
+    }
+    return searchResults;
+  }
+
+  List<MonthData> _getMatchingMonths(String query) {
+    return allMonths
+        .where(
+          (monthData) =>
+              monthData.name.toLowerCase().startsWith(query.toLowerCase()),
+        )
+        .toList();
   }
 
   Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
@@ -228,20 +321,28 @@ class SearchService {
     );
   }
 
-  List<List<int>> _getDurationsOfHolidayInEveryYear(int day, int month) {
+  List<List<int>> _getDurationsForCalendarDateInEveryYear(
+    int day,
+    int month, {
+    int year,
+  }) {
     final List<List<int>> durationsOfHolidayInEveryYear = [];
-    for (var year = 1970; year <= currentYear; year++) {
-      durationsOfHolidayInEveryYear.add([
-        DateTime(year, month, day).microsecondsSinceEpoch,
-        DateTime(year, month, day + 1).microsecondsSinceEpoch,
-      ]);
+    final int startYear = year ?? searchStartYear;
+    final int endYear = year ?? currentYear;
+    for (var yr = startYear; yr <= endYear; yr++) {
+      if (isValidDate(day: day, month: month, year: yr)) {
+        durationsOfHolidayInEveryYear.add([
+          DateTime(yr, month, day).microsecondsSinceEpoch,
+          DateTime(yr, month, day + 1).microsecondsSinceEpoch,
+        ]);
+      }
     }
     return durationsOfHolidayInEveryYear;
   }
 
   List<List<int>> _getDurationsOfMonthInEveryYear(int month) {
     final List<List<int>> durationsOfMonthInEveryYear = [];
-    for (var year = 1970; year < currentYear; year++) {
+    for (var year = searchStartYear; year <= currentYear; year++) {
       durationsOfMonthInEveryYear.add([
         DateTime.utc(year, month, 1).microsecondsSinceEpoch,
         month == 12
@@ -270,4 +371,52 @@ class SearchService {
         location.longitude < locationData.bbox[2] &&
         location.latitude < locationData.bbox[3];
   }
+
+  List<Tuple3<int, MonthData, int>> _getPossibleEventDate(String query) {
+    final List<Tuple3<int, MonthData, int>> possibleEvents = [];
+    if (query.trim().isEmpty) {
+      return possibleEvents;
+    }
+    final result = query
+        .trim()
+        .split(RegExp('[ ,-/]+'))
+        .map((e) => e.trim())
+        .where((e) => e.isNotEmpty)
+        .toList();
+    final resultCount = result.length;
+    if (resultCount < 1 || resultCount > 4) {
+      return possibleEvents;
+    }
+
+    final int day = int.tryParse(result[0]);
+    if (day == null || day < 1 || day > 31) {
+      return possibleEvents;
+    }
+    final List<MonthData> potentialMonth =
+        resultCount > 1 ? _getMatchingMonths(result[1]) : allMonths;
+    final int parsedYear = resultCount >= 3 ? int.tryParse(result[2]) : null;
+    final List<int> matchingYears = [];
+    if (parsedYear != null) {
+      bool foundMatch = false;
+      for (int i = searchStartYear; i <= currentYear; i++) {
+        if (i.toString().startsWith(parsedYear.toString())) {
+          matchingYears.add(i);
+          foundMatch = foundMatch || (i == parsedYear);
+        }
+      }
+      if (!foundMatch && parsedYear > 1000 && parsedYear <= currentYear) {
+        matchingYears.add(parsedYear);
+      }
+    }
+    for (var element in potentialMonth) {
+      if (matchingYears.isEmpty) {
+        possibleEvents.add(Tuple3(day, element, null));
+      } else {
+        for (int yr in matchingYears) {
+          possibleEvents.add(Tuple3(day, element, yr));
+        }
+      }
+    }
+    return possibleEvents;
+  }
 }

+ 10 - 1
lib/services/sync_service.dart

@@ -179,6 +179,15 @@ class SyncService {
     );
   }
 
+  void onDeviceCollectionSet(Set<int> collectionIDs) {
+    _uploader.removeFromQueueWhere(
+      (file) {
+        return !collectionIDs.contains(file.collectionID);
+      },
+      UserCancelledUploadError(),
+    );
+  }
+
   void onVideoBackupPaused() {
     _uploader.removeFromQueueWhere(
       (file) {
@@ -243,7 +252,7 @@ class SyncService {
     final lastNotificationShownTime =
         _prefs.getInt(kLastStorageLimitExceededNotificationPushTime) ?? 0;
     final now = DateTime.now().microsecondsSinceEpoch;
-    if ((now - lastNotificationShownTime) > kMicroSecondsInDay) {
+    if ((now - lastNotificationShownTime) > microSecondsInDay) {
       await _prefs.setInt(kLastStorageLimitExceededNotificationPushTime, now);
       NotificationService.instance.showNotification(
         "storage limit exceeded",

+ 1 - 0
lib/services/trash_sync_service.dart

@@ -181,6 +181,7 @@ class TrashSyncService {
         data: params,
       );
       await _trashDB.clearTable();
+      unawaited(syncTrash());
       Bus.instance.fire(TrashUpdatedEvent());
       Bus.instance.fire(ForceReloadTrashPageEvent());
     } catch (e, s) {

+ 1 - 1
lib/services/update_service.dart

@@ -66,7 +66,7 @@ class UpdateService {
         _prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
     final now = DateTime.now().microsecondsSinceEpoch;
     final hasBeen3DaysSinceLastNotification =
-        (now - lastNotificationShownTime) > (3 * kMicroSecondsInDay);
+        (now - lastNotificationShownTime) > (3 * microSecondsInDay);
     if (shouldUpdate &&
         hasBeen3DaysSinceLastNotification &&
         _latestVersion.shouldNotify) {

+ 156 - 0
lib/theme/colors.dart

@@ -0,0 +1,156 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+
+class EnteColorScheme {
+  // Background Colors
+  final Color backgroundBase;
+  final Color backgroundElevated;
+  final Color backgroundElevated2;
+
+  // Backdrop Colors
+  final Color backdropBase;
+  final Color backdropBaseMute;
+
+  // Text Colors
+  final Color textBase;
+  final Color textMuted;
+  final Color textFaint;
+
+  // Fill Colors
+  final Color fillBase;
+  final Color fillMuted;
+  final Color fillFaint;
+
+  // Stroke Colors
+  final Color strokeBase;
+  final Color strokeMuted;
+  final Color strokeFaint;
+
+  // Fixed Colors
+  final Color primary700;
+  final Color primary500;
+  final Color primary400;
+  final Color primary300;
+
+  final Color warning700;
+  final Color warning500;
+  final Color warning400;
+  final Color caution500;
+
+  const EnteColorScheme(
+    this.backgroundBase,
+    this.backgroundElevated,
+    this.backgroundElevated2,
+    this.backdropBase,
+    this.backdropBaseMute,
+    this.textBase,
+    this.textMuted,
+    this.textFaint,
+    this.fillBase,
+    this.fillMuted,
+    this.fillFaint,
+    this.strokeBase,
+    this.strokeMuted,
+    this.strokeFaint, {
+    this.primary700 = _primary700,
+    this.primary500 = _primary500,
+    this.primary400 = _primary400,
+    this.primary300 = _primary300,
+    this.warning700 = _warning700,
+    this.warning500 = _warning500,
+    this.warning400 = _warning700,
+    this.caution500 = _caution500,
+  });
+}
+
+const EnteColorScheme lightScheme = EnteColorScheme(
+  backgroundBaseLight,
+  backgroundElevatedLight,
+  backgroundElevated2Light,
+  backdropBaseLight,
+  backdropBaseMuteLight,
+  textBaseLight,
+  textMutedLight,
+  textFaintLight,
+  fillBaseLight,
+  fillMutedLight,
+  fillFaintLight,
+  strokeBaseLight,
+  strokeMutedLight,
+  strokeFaintLight,
+);
+
+const EnteColorScheme darkScheme = EnteColorScheme(
+  backgroundBaseDark,
+  backgroundElevatedDark,
+  backgroundElevated2Dark,
+  backdropBaseDark,
+  backdropBaseMuteDark,
+  textBaseDark,
+  textMutedDark,
+  textFaintDark,
+  fillBaseDark,
+  fillMutedDark,
+  fillFaintDark,
+  strokeBaseDark,
+  strokeMutedDark,
+  strokeFaintDark,
+);
+
+// Background Colors
+const Color backgroundBaseLight = Color.fromRGBO(255, 255, 255, 1);
+const Color backgroundElevatedLight = Color.fromRGBO(255, 255, 255, 1);
+const Color backgroundElevated2Light = Color.fromRGBO(251, 251, 251, 1);
+
+const Color backgroundBaseDark = Color.fromRGBO(0, 0, 0, 1);
+const Color backgroundElevatedDark = Color.fromRGBO(27, 27, 27, 1);
+const Color backgroundElevated2Dark = Color.fromRGBO(37, 37, 37, 1);
+
+// Backdrop Colors
+const Color backdropBaseLight = Color.fromRGBO(255, 255, 255, 0.75);
+const Color backdropBaseMuteLight = Color.fromRGBO(255, 255, 255, 0.30);
+
+const Color backdropBaseDark = Color.fromRGBO(0, 0, 0, 0.65);
+const Color backdropBaseMuteDark = Color.fromRGBO(0, 0, 0, 0.20);
+
+// Text Colors
+const Color textBaseLight = Color.fromRGBO(0, 0, 0, 1);
+const Color textMutedLight = Color.fromRGBO(0, 0, 0, 0.6);
+const Color textFaintLight = Color.fromRGBO(0, 0, 0, 0.5);
+
+const Color textBaseDark = Color.fromRGBO(255, 255, 255, 1);
+const Color textMutedDark = Color.fromRGBO(255, 255, 255, 0.7);
+const Color textFaintDark = Color.fromRGBO(255, 255, 255, 0.5);
+
+// Fill Colors
+const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
+const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12);
+const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04);
+
+const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1);
+const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16);
+const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12);
+
+// Stroke Colors
+const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1);
+const Color strokeMutedLight = Color.fromRGBO(0, 0, 0, 0.24);
+const Color strokeFaintLight = Color.fromRGBO(0, 0, 0, 0.04);
+
+const Color strokeBaseDark = Color.fromRGBO(255, 255, 255, 1);
+const Color strokeMutedDark = Color.fromRGBO(255, 255, 255, 0.24);
+const Color strokeFaintDark = Color.fromRGBO(255, 255, 255, 0.16);
+
+// Fixed Colors
+
+const Color _primary700 = Color.fromRGBO(0, 179, 60, 1);
+const Color _primary500 = Color.fromRGBO(29, 185, 84, 1);
+const Color _primary400 = Color.fromRGBO(38, 203, 95, 1);
+const Color _primary300 = Color.fromRGBO(1, 222, 77, 1);
+
+const Color _warning700 = Color.fromRGBO(234, 63, 63, 1);
+const Color _warning500 = Color.fromRGBO(255, 101, 101, 1);
+const Color warning500 = Color.fromRGBO(255, 101, 101, 1);
+const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
+
+const Color _caution500 = Color.fromRGBO(255, 194, 71, 1);

+ 51 - 0
lib/theme/effects.dart

@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+
+const blurBase = 96;
+const blurMuted = 48;
+const blurFaint = 24;
+
+List<BoxShadow> shadowFloatLight = const [
+  BoxShadow(blurRadius: 10, color: Color.fromRGBO(0, 0, 0, 0.25)),
+];
+
+List<BoxShadow> shadowMenuLight = const [
+  BoxShadow(blurRadius: 6, color: Color.fromRGBO(0, 0, 0, 0.16)),
+  BoxShadow(
+    blurRadius: 6,
+    color: Color.fromRGBO(0, 0, 0, 0.12),
+    offset: Offset(0, 3),
+  ),
+];
+
+List<BoxShadow> shadowButtonLight = const [
+  BoxShadow(
+    blurRadius: 4,
+    color: Color.fromRGBO(0, 0, 0, 0.25),
+    offset: Offset(0, 4),
+  ),
+];
+
+List<BoxShadow> shadowFloatDark = const [
+  BoxShadow(
+    blurRadius: 12,
+    color: Color.fromRGBO(0, 0, 0, 0.75),
+    offset: Offset(0, 2),
+  ),
+];
+
+List<BoxShadow> shadowMenuDark = const [
+  BoxShadow(blurRadius: 6, color: Color.fromRGBO(0, 0, 0, 0.50)),
+  BoxShadow(
+    blurRadius: 6,
+    color: Color.fromRGBO(0, 0, 0, 0.25),
+    offset: Offset(0, 3),
+  ),
+];
+
+List<BoxShadow> shadowButtonDark = const [
+  BoxShadow(
+    blurRadius: 4,
+    color: Color.fromRGBO(0, 0, 0, 0.75),
+    offset: Offset(0, 4),
+  ),
+];

+ 36 - 0
lib/theme/ente_theme.dart

@@ -0,0 +1,36 @@
+import 'package:flutter/widgets.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/effects.dart';
+import 'package:photos/theme/text_style.dart';
+
+class EnteTheme {
+  final EnteTextTheme textTheme;
+  final EnteColorScheme colorScheme;
+  final List<BoxShadow> shadowFloat;
+  final List<BoxShadow> shadowMenu;
+  final List<BoxShadow> shadowButton;
+
+  const EnteTheme(
+    this.textTheme,
+    this.colorScheme, {
+    required this.shadowFloat,
+    required this.shadowMenu,
+    required this.shadowButton,
+  });
+}
+
+EnteTheme lightTheme = EnteTheme(
+  lightTextTheme,
+  lightScheme,
+  shadowFloat: shadowFloatLight,
+  shadowMenu: shadowMenuLight,
+  shadowButton: shadowButtonLight,
+);
+
+EnteTheme darkTheme = EnteTheme(
+  darkTextTheme,
+  darkScheme,
+  shadowFloat: shadowFloatDark,
+  shadowMenu: shadowMenuDark,
+  shadowButton: shadowButtonDark,
+);

+ 117 - 0
lib/theme/text_style.dart

@@ -0,0 +1,117 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+
+const FontWeight _regularWeight = FontWeight.w500;
+const FontWeight _boldWeight = FontWeight.w600;
+const String _fontFamily = 'Inter';
+
+const TextStyle h1 = TextStyle(
+  fontSize: 48,
+  height: 48 / 28,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+const TextStyle h2 = TextStyle(
+  fontSize: 32,
+  height: 39 / 32.0,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+const TextStyle h3 = TextStyle(
+  fontSize: 24,
+  height: 29 / 24.0,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+const TextStyle large = TextStyle(
+  fontSize: 18,
+  height: 22 / 18.0,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+const TextStyle body = TextStyle(
+  fontSize: 16,
+  height: 19.4 / 16.0,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+const TextStyle small = TextStyle(
+  fontSize: 14,
+  height: 17 / 14.0,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+const TextStyle mini = TextStyle(
+  fontSize: 12,
+  height: 15 / 12.0,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+const TextStyle tiny = TextStyle(
+  fontSize: 10,
+  height: 12 / 10.0,
+  fontWeight: _regularWeight,
+  fontFamily: _fontFamily,
+);
+
+class EnteTextTheme {
+  final TextStyle h1;
+  final TextStyle h1Bold;
+  final TextStyle h2;
+  final TextStyle h2Bold;
+  final TextStyle h3;
+  final TextStyle h3Bold;
+  final TextStyle large;
+  final TextStyle largeBold;
+  final TextStyle body;
+  final TextStyle bodyBold;
+  final TextStyle small;
+  final TextStyle smallBold;
+  final TextStyle mini;
+  final TextStyle miniBold;
+  final TextStyle tiny;
+  final TextStyle tinyBold;
+
+  const EnteTextTheme({
+    required this.h1,
+    required this.h1Bold,
+    required this.h2,
+    required this.h2Bold,
+    required this.h3,
+    required this.h3Bold,
+    required this.large,
+    required this.largeBold,
+    required this.body,
+    required this.bodyBold,
+    required this.small,
+    required this.smallBold,
+    required this.mini,
+    required this.miniBold,
+    required this.tiny,
+    required this.tinyBold,
+  });
+}
+
+EnteTextTheme lightTextTheme = _buildEnteTextStyle(textBaseLight);
+EnteTextTheme darkTextTheme = _buildEnteTextStyle(textBaseDark);
+
+EnteTextTheme _buildEnteTextStyle(Color color) {
+  return EnteTextTheme(
+    h1: h1.copyWith(color: color),
+    h1Bold: h1.copyWith(color: color, fontWeight: _boldWeight),
+    h2: h2.copyWith(color: color),
+    h2Bold: h2.copyWith(color: color, fontWeight: _boldWeight),
+    h3: h3.copyWith(color: color),
+    h3Bold: h3.copyWith(color: color, fontWeight: _boldWeight),
+    large: large.copyWith(color: color),
+    largeBold: large.copyWith(color: color, fontWeight: _boldWeight),
+    body: body.copyWith(color: color),
+    bodyBold: body.copyWith(color: color, fontWeight: _boldWeight),
+    small: small.copyWith(color: color),
+    smallBold: small.copyWith(color: color, fontWeight: _boldWeight),
+    mini: mini.copyWith(color: color),
+    miniBold: mini.copyWith(color: color, fontWeight: _boldWeight),
+    tiny: tiny.copyWith(color: color),
+    tinyBold: tiny.copyWith(color: color, fontWeight: _boldWeight),
+  );
+}

+ 0 - 1
lib/ui/account/delete_account_page.dart

@@ -75,7 +75,6 @@ class DeleteAccountPage extends StatelessWidget {
               ),
               GradientButton(
                 text: "Yes, send feedback",
-                paddingValue: 4,
                 iconData: Icons.check,
                 onTap: () async {
                   await sendEmail(

+ 2 - 2
lib/ui/account/recovery_key_page.dart

@@ -51,9 +51,9 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
   @override
   Widget build(BuildContext context) {
     final String recoveryKey = bip39.entropyToMnemonic(widget.recoveryKey);
-    if (recoveryKey.split(' ').length != kMnemonicKeyWordCount) {
+    if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
       throw AssertionError(
-        'recovery code should have $kMnemonicKeyWordCount words',
+        'recovery code should have $mnemonicKeyWordCount words',
       );
     }
     final double topPadding = widget.showAppBar

+ 8 - 5
lib/ui/account/verify_recovery_page.dart

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/event_bus.dart';
+import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/notification_event.dart';
 import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_remote_flag_service.dart';
@@ -118,6 +119,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
 
   @override
   Widget build(BuildContext context) {
+    final enteTheme = Theme.of(context).colorScheme.enteTheme;
     return Scaffold(
       appBar: AppBar(
         elevation: 0,
@@ -147,16 +149,17 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                         width: double.infinity,
                         child: Text(
                           'Verify recovery key',
-                          style: Theme.of(context).textTheme.headline5,
+                          style: enteTheme.textTheme.h3Bold,
                           textAlign: TextAlign.left,
                         ),
                       ),
-                      const SizedBox(height: 12),
+                      const SizedBox(height: 18),
                       Text(
                         "If you forget your password, your recovery key is the "
                         "only way to recover your photos.\n\nPlease verify that "
                         "you have safely backed up your 24 word recovery key by re-entering it.",
-                        style: Theme.of(context).textTheme.subtitle2,
+                        style: enteTheme.textTheme.small
+                            .copyWith(color: enteTheme.colorScheme.textMuted),
                       ),
                       const SizedBox(height: 12),
                       TextFormField(
@@ -186,7 +189,8 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                       const SizedBox(height: 12),
                       Text(
                         "If you saved the recovery key from older app versions, you might have a 64 character recovery code instead of 24 words. You can enter that too.",
-                        style: Theme.of(context).textTheme.caption,
+                        style: enteTheme.textTheme.mini
+                            .copyWith(color: enteTheme.colorScheme.textMuted),
                       ),
                       const SizedBox(height: 8),
                       Expanded(
@@ -201,7 +205,6 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
                               GradientButton(
                                 onTap: _verifyRecoveryKey,
                                 text: "Verify",
-                                paddingValue: 6,
                                 iconData: Icons.shield_outlined,
                               ),
                               const SizedBox(height: 8),

+ 80 - 59
lib/ui/backup_folder_selection_page.dart

@@ -3,15 +3,18 @@
 import 'dart:io';
 import 'dart:ui';
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart';
 import 'package:implicitly_animated_reorderable_list/transitions.dart';
+import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
-import 'package:photos/core/event_bus.dart';
+import 'package:photos/db/device_files_db.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/ente_theme_data.dart';
-import 'package:photos/events/backup_folders_updated_event.dart';
+import 'package:photos/models/device_collection.dart';
 import 'package:photos/models/file.dart';
+import 'package:photos/services/remote_sync_service.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
 
@@ -31,30 +34,35 @@ class BackupFolderSelectionPage extends StatefulWidget {
 }
 
 class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
-  final Set<String> _allFolders = <String>{};
-  Set<String> _selectedFolders = <String>{};
-  List<File> _latestFiles;
-  Map<String, int> _itemCount;
+  final Logger _logger = Logger((_BackupFolderSelectionPageState).toString());
+  final Set<String> _allDevicePathIDs = <String>{};
+  final Set<String> _selectedDevicePathIDs = <String>{};
+  List<DeviceCollection> _deviceCollections;
+  Map<String, int> _pathIDToItemCount;
 
   @override
   void initState() {
-    _selectedFolders = Configuration.instance.getPathsToBackUp();
-    FilesDB.instance.getLatestLocalFiles().then((files) async {
-      _itemCount = await FilesDB.instance.getFileCountInDeviceFolders();
+    FilesDB.instance
+        .getDeviceCollections(includeCoverThumbnail: true)
+        .then((files) async {
+      _pathIDToItemCount =
+          await FilesDB.instance.getDevicePathIDToImportedFileCount();
       setState(() {
-        _latestFiles = files;
-        _latestFiles.sort((first, second) {
-          return first.deviceFolder
-              .toLowerCase()
-              .compareTo(second.deviceFolder.toLowerCase());
+        _deviceCollections = files;
+        _deviceCollections.sort((first, second) {
+          return first.name.toLowerCase().compareTo(second.name.toLowerCase());
         });
-        for (final file in _latestFiles) {
-          _allFolders.add(file.deviceFolder);
+        for (final file in _deviceCollections) {
+          _allDevicePathIDs.add(file.id);
+          if (file.shouldBackup) {
+            _selectedDevicePathIDs.add(file.id);
+          }
         }
         if (widget.isOnboarding) {
-          _selectedFolders.addAll(_allFolders);
+          _selectedDevicePathIDs.addAll(_allDevicePathIDs);
         }
-        _selectedFolders.removeWhere((folder) => !_allFolders.contains(folder));
+        _selectedDevicePathIDs
+            .removeWhere((folder) => !_allDevicePathIDs.contains(folder));
       });
     });
     super.initState();
@@ -100,7 +108,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
           const Padding(
             padding: EdgeInsets.all(10),
           ),
-          _latestFiles == null
+          _deviceCollections == null
               ? Container()
               : GestureDetector(
                   behavior: HitTestBehavior.translucent,
@@ -109,7 +117,8 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                     child: Align(
                       alignment: Alignment.centerLeft,
                       child: Text(
-                        _selectedFolders.length == _allFolders.length
+                        _selectedDevicePathIDs.length ==
+                                _allDevicePathIDs.length
                             ? "Unselect all"
                             : "Select all",
                         textAlign: TextAlign.right,
@@ -121,18 +130,18 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                     ),
                   ),
                   onTap: () {
-                    final hasSelectedAll =
-                        _selectedFolders.length == _allFolders.length;
+                    final hasSelectedAll = _selectedDevicePathIDs.length ==
+                        _allDevicePathIDs.length;
                     // Flip selection
                     if (hasSelectedAll) {
-                      _selectedFolders.clear();
+                      _selectedDevicePathIDs.clear();
                     } else {
-                      _selectedFolders.addAll(_allFolders);
+                      _selectedDevicePathIDs.addAll(_allDevicePathIDs);
                     }
-                    _latestFiles.sort((first, second) {
-                      return first.deviceFolder
+                    _deviceCollections.sort((first, second) {
+                      return first.name
                           .toLowerCase()
-                          .compareTo(second.deviceFolder.toLowerCase());
+                          .compareTo(second.name.toLowerCase());
                     });
                     setState(() {});
                   },
@@ -163,12 +172,25 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                           bottom: Platform.isIOS ? 60 : 32,
                         ),
                   child: OutlinedButton(
-                    onPressed: _selectedFolders.isEmpty
+                    onPressed: _selectedDevicePathIDs.isEmpty
                         ? null
                         : () async {
+                            final Map<String, bool> syncStatus = {};
+                            for (String pathID in _allDevicePathIDs) {
+                              syncStatus[pathID] =
+                                  _selectedDevicePathIDs.contains(pathID);
+                            }
                             await Configuration.instance
-                                .setPathsToBackUp(_selectedFolders);
-                            Bus.instance.fire(BackupFoldersUpdatedEvent());
+                                .setHasSelectedAnyBackupFolder(
+                              _selectedDevicePathIDs.isNotEmpty,
+                            );
+                            await RemoteSyncService.instance
+                                .updateDeviceFolderSyncStatus(syncStatus);
+                            await Configuration.instance
+                                .setSelectAllFoldersForBackup(
+                              _allDevicePathIDs.length ==
+                                  _selectedDevicePathIDs.length,
+                            );
                             Navigator.of(context).pop();
                           },
                     child: Text(widget.buttonText),
@@ -202,7 +224,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
   }
 
   Widget _getFolders() {
-    if (_latestFiles == null) {
+    if (_deviceCollections == null) {
       return const EnteLoadingWidget();
     }
     _sortFiles();
@@ -214,14 +236,13 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
         thumbVisibility: true,
         child: Padding(
           padding: const EdgeInsets.only(right: 4),
-          child: ImplicitlyAnimatedReorderableList<File>(
+          child: ImplicitlyAnimatedReorderableList<DeviceCollection>(
             controller: scrollController,
-            items: _latestFiles,
-            areItemsTheSame: (oldItem, newItem) =>
-                oldItem.deviceFolder == newItem.deviceFolder,
+            items: _deviceCollections,
+            areItemsTheSame: (oldItem, newItem) => oldItem.id == newItem.id,
             onReorderFinished: (item, from, to, newItems) {
               setState(() {
-                _latestFiles
+                _deviceCollections
                   ..clear()
                   ..addAll(newItems);
               });
@@ -255,8 +276,11 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
     );
   }
 
-  Widget _getFileItem(File file) {
-    final isSelected = _selectedFolders.contains(file.deviceFolder);
+  Widget _getFileItem(DeviceCollection deviceCollection) {
+    final isSelected = _selectedDevicePathIDs.contains(deviceCollection.id);
+    final importedCount = _pathIDToItemCount != null
+        ? _pathIDToItemCount[deviceCollection.id] ?? 0
+        : -1;
     return Padding(
       padding: const EdgeInsets.only(bottom: 1, right: 1),
       child: Container(
@@ -294,9 +318,9 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                     value: isSelected,
                     onChanged: (value) {
                       if (value) {
-                        _selectedFolders.add(file.deviceFolder);
+                        _selectedDevicePathIDs.add(deviceCollection.id);
                       } else {
-                        _selectedFolders.remove(file.deviceFolder);
+                        _selectedDevicePathIDs.remove(deviceCollection.id);
                       }
                       setState(() {});
                     },
@@ -307,7 +331,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                       Container(
                         constraints: const BoxConstraints(maxWidth: 180),
                         child: Text(
-                          file.deviceFolder,
+                          deviceCollection.name,
                           textAlign: TextAlign.left,
                           style: TextStyle(
                             fontFamily: 'Inter-Medium',
@@ -326,9 +350,10 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                       ),
                       const Padding(padding: EdgeInsets.only(top: 2)),
                       Text(
-                        _itemCount[file.deviceFolder].toString() +
+                        (kDebugMode ? 'inApp: $importedCount : device ' : '') +
+                            (deviceCollection.count ?? 0).toString() +
                             " item" +
-                            (_itemCount[file.deviceFolder] == 1 ? "" : "s"),
+                            ((deviceCollection.count ?? 0) == 1 ? "" : "s"),
                         textAlign: TextAlign.left,
                         style: TextStyle(
                           fontSize: 12,
@@ -341,15 +366,15 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
                   ),
                 ],
               ),
-              _getThumbnail(file, isSelected),
+              _getThumbnail(deviceCollection.thumbnail, isSelected),
             ],
           ),
           onTap: () {
-            final value = !_selectedFolders.contains(file.deviceFolder);
+            final value = !_selectedDevicePathIDs.contains(deviceCollection.id);
             if (value) {
-              _selectedFolders.add(file.deviceFolder);
+              _selectedDevicePathIDs.add(deviceCollection.id);
             } else {
-              _selectedFolders.remove(file.deviceFolder);
+              _selectedDevicePathIDs.remove(deviceCollection.id);
             }
             setState(() {});
           },
@@ -359,20 +384,16 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
   }
 
   void _sortFiles() {
-    _latestFiles.sort((first, second) {
-      if (_selectedFolders.contains(first.deviceFolder) &&
-          _selectedFolders.contains(second.deviceFolder)) {
-        return first.deviceFolder
-            .toLowerCase()
-            .compareTo(second.deviceFolder.toLowerCase());
-      } else if (_selectedFolders.contains(first.deviceFolder)) {
+    _deviceCollections.sort((first, second) {
+      if (_selectedDevicePathIDs.contains(first.id) &&
+          _selectedDevicePathIDs.contains(second.id)) {
+        return first.name.toLowerCase().compareTo(second.name.toLowerCase());
+      } else if (_selectedDevicePathIDs.contains(first.id)) {
         return -1;
-      } else if (_selectedFolders.contains(second.deviceFolder)) {
+      } else if (_selectedDevicePathIDs.contains(second.id)) {
         return 1;
       }
-      return first.deviceFolder
-          .toLowerCase()
-          .compareTo(second.deviceFolder.toLowerCase());
+      return first.name.toLowerCase().compareTo(second.name.toLowerCase());
     });
   }
 
@@ -388,7 +409,7 @@ class _BackupFolderSelectionPageState extends State<BackupFolderSelectionPage> {
             ThumbnailWidget(
               file,
               shouldShowSyncStatus: false,
-              key: Key("backup_selection_widget" + file.tag()),
+              key: Key("backup_selection_widget" + file.tag),
             ),
             Padding(
               padding: const EdgeInsets.all(9),

+ 2 - 2
lib/ui/collections/collection_item_widget.dart

@@ -39,12 +39,12 @@ class CollectionItem extends StatelessWidget {
               height: sideOfThumbnail,
               width: sideOfThumbnail,
               child: Hero(
-                tag: "collection" + c.thumbnail.tag(),
+                tag: "collection" + c.thumbnail.tag,
                 child: ThumbnailWidget(
                   c.thumbnail,
                   shouldShowArchiveStatus: c.collection.isArchived(),
                   key: Key(
-                    "collection" + c.thumbnail.tag(),
+                    "collection" + c.thumbnail.tag,
                   ),
                 ),
               ),

+ 12 - 14
lib/ui/collections/device_folder_icon_widget.dart

@@ -1,15 +1,15 @@
 // @dart=2.9
 
 import 'package:flutter/material.dart';
-import 'package:photos/core/configuration.dart';
-import 'package:photos/models/device_folder.dart';
+import 'package:photos/models/device_collection.dart';
 import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
 import 'package:photos/ui/viewer/gallery/device_folder_page.dart';
 import 'package:photos/utils/navigation_util.dart';
 
 class DeviceFolderIcon extends StatelessWidget {
+  final DeviceCollection deviceCollection;
   const DeviceFolderIcon(
-    this.folder, {
+    this.deviceCollection, {
     Key key,
   }) : super(key: key);
 
@@ -38,12 +38,9 @@ class DeviceFolderIcon extends StatelessWidget {
     ),
   );
 
-  final DeviceFolder folder;
-
   @override
   Widget build(BuildContext context) {
-    final isBackedUp =
-        Configuration.instance.getPathsToBackUp().contains(folder.path);
+    final isBackedUp = deviceCollection.shouldBackup;
     return GestureDetector(
       child: Padding(
         padding: const EdgeInsets.symmetric(horizontal: 2),
@@ -58,17 +55,18 @@ class DeviceFolderIcon extends StatelessWidget {
                   height: 120,
                   width: 120,
                   child: Hero(
-                    tag:
-                        "device_folder:" + folder.path + folder.thumbnail.tag(),
+                    tag: "device_folder:" +
+                        deviceCollection.name +
+                        deviceCollection.thumbnail.tag,
                     child: Stack(
                       children: [
                         ThumbnailWidget(
-                          folder.thumbnail,
+                          deviceCollection.thumbnail,
                           shouldShowSyncStatus: false,
                           key: Key(
                             "device_folder:" +
-                                folder.path +
-                                folder.thumbnail.tag(),
+                                deviceCollection.name +
+                                deviceCollection.thumbnail.tag,
                           ),
                         ),
                         isBackedUp ? Container() : kUnsyncedIconOverlay,
@@ -80,7 +78,7 @@ class DeviceFolderIcon extends StatelessWidget {
               Padding(
                 padding: const EdgeInsets.only(top: 10),
                 child: Text(
-                  folder.name,
+                  deviceCollection.name,
                   style: Theme.of(context)
                       .textTheme
                       .subtitle1
@@ -93,7 +91,7 @@ class DeviceFolderIcon extends StatelessWidget {
         ),
       ),
       onTap: () {
-        routeToPage(context, DeviceFolderPage(folder));
+        routeToPage(context, DeviceFolderPage(deviceCollection));
       },
     );
   }

+ 73 - 20
lib/ui/collections/device_folders_grid_view_widget.dart

@@ -1,41 +1,94 @@
 // @dart=2.9
 
+import 'dart:async';
+
 import 'package:flutter/material.dart';
-import 'package:photos/models/device_folder.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/db/device_files_db.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/events/backup_folders_updated_event.dart';
+import 'package:photos/models/device_collection.dart';
+import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/ui/collections/device_folder_icon_widget.dart';
-import 'package:photos/ui/viewer/gallery/empte_state.dart';
-
-class DeviceFoldersGridViewWidget extends StatelessWidget {
-  final List<DeviceFolder> folders;
+import 'package:photos/ui/common/loading_widget.dart';
+import 'package:photos/ui/viewer/gallery/empty_state.dart';
 
-  const DeviceFoldersGridViewWidget(
-    this.folders, {
+class DeviceFoldersGridViewWidget extends StatefulWidget {
+  const DeviceFoldersGridViewWidget({
     Key key,
   }) : super(key: key);
 
+  @override
+  State<DeviceFoldersGridViewWidget> createState() =>
+      _DeviceFoldersGridViewWidgetState();
+}
+
+class _DeviceFoldersGridViewWidgetState
+    extends State<DeviceFoldersGridViewWidget> {
+  StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
+
+  @override
+  void initState() {
+    _backupFoldersUpdatedEvent =
+        Bus.instance.on<BackupFoldersUpdatedEvent>().listen((event) {
+      if (mounted) {
+        setState(() {});
+      }
+    });
+    super.initState();
+  }
+
   @override
   Widget build(BuildContext context) {
+    final bool isMigrationDone =
+        LocalSyncService.instance.isDeviceFileMigrationDone();
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 8),
       child: SizedBox(
         height: 170,
         child: Align(
           alignment: Alignment.centerLeft,
-          child: folders.isEmpty
-              ? const EmptyState()
-              : ListView.builder(
-                  shrinkWrap: true,
-                  scrollDirection: Axis.horizontal,
-                  padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
-                  physics: const ScrollPhysics(),
-                  // to disable GridView's scrolling
-                  itemBuilder: (context, index) {
-                    return DeviceFolderIcon(folders[index]);
-                  },
-                  itemCount: folders.length,
-                ),
+          child: FutureBuilder<List<DeviceCollection>>(
+            future: FilesDB.instance
+                .getDeviceCollections(includeCoverThumbnail: true),
+            builder: (context, snapshot) {
+              if (snapshot.hasData) {
+                return snapshot.data.isEmpty
+                    ? Padding(
+                        padding: const EdgeInsets.all(22),
+                        child: (isMigrationDone
+                            ? const EmptyState()
+                            : const EmptyState(
+                                text: "Importing....",
+                              )),
+                      )
+                    : ListView.builder(
+                        shrinkWrap: true,
+                        scrollDirection: Axis.horizontal,
+                        padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
+                        physics: const ScrollPhysics(),
+                        // to disable GridView's scrolling
+                        itemBuilder: (context, index) {
+                          final deviceCollection = snapshot.data[index];
+                          return DeviceFolderIcon(deviceCollection);
+                        },
+                        itemCount: snapshot.data.length,
+                      );
+              } else if (snapshot.hasError) {
+                return Text(snapshot.error.toString());
+              } else {
+                return const EnteLoadingWidget();
+              }
+            },
+          ),
         ),
       ),
     );
   }
+
+  @override
+  void dispose() {
+    _backupFoldersUpdatedEvent?.cancel();
+    super.dispose();
+  }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов