diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8e04b259b..bbc9cefe0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/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", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 92cb0a5a0..11fd2fa2e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -60,6 +60,8 @@ NSAllowsArbitraryLoadsInWebContent + ITSAppUsesNonExemptEncryption + NSFaceIDUsageDescription Please allow ente to lock itself with FaceID or TouchID NSPhotoLibraryUsageDescription diff --git a/lib/core/cache/image_cache.dart b/lib/core/cache/image_cache.dart index 98386efd3..2370cf9a2 100644 --- a/lib/core/cache/image_cache.dart +++ b/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 _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); } } diff --git a/lib/core/cache/lru_map.dart b/lib/core/cache/lru_map.dart index 69d790077..9e28c84c0 100644 --- a/lib/core/cache/lru_map.dart +++ b/lib/core/cache/lru_map.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'dart:collection'; typedef EvictionHandler = Function(K key, V value); @@ -7,12 +5,12 @@ typedef EvictionHandler = Function(K key, V value); class LRUMap { final LinkedHashMap _map = LinkedHashMap(); final int _maxSize; - final EvictionHandler _handler; + final EvictionHandler? _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 { _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); } } } diff --git a/lib/core/cache/thumbnail_cache.dart b/lib/core/cache/thumbnail_cache.dart index 4af10f215..08a05fda9 100644 --- a/lib/core/cache/thumbnail_cache.dart +++ b/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 _map = LRUMap(1000); + static final LRUMap _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(), ); } } diff --git a/lib/core/cache/video_cache_manager.dart b/lib/core/cache/video_cache_manager.dart index 601682eda..ada01c080 100644 --- a/lib/core/cache/video_cache_manager.dart +++ b/lib/core/cache/video_cache_manager.dart @@ -1,5 +1,3 @@ - - import 'package:flutter_cache_manager/flutter_cache_manager.dart'; class VideoCacheManager { diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index b604029fb..f4afbcac1 100644 --- a/lib/core/configuration.dart +++ b/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 logout() async { + Future 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 clearAutoLogoutFlag() { + return _preferences.remove("auto_logout"); } Future 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 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 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 getPathsToBackUp() { if (_preferences.containsKey(foldersToBackUpKey)) { - return _preferences.getStringList(foldersToBackUpKey).toSet(); + return _preferences.getStringList(foldersToBackUpKey)!.toSet(); } else { return {}; } } - Future setPathsToBackUp(Set 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 addPathToFoldersToBeBackedUp(String path) async { - final currentPaths = getPathsToBackUp(); - currentPaths.add(path); - return setPathsToBackUp(currentPaths); - } - Future 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 setKey(String key) async { + Future setKey(String? key) async { _key = key; if (key == null) { await _secureStorage.delete( @@ -455,7 +468,7 @@ class Configuration { } } - Future setSecretKey(String secretKey) async { + Future 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 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 setShouldHideFromRecents(bool value) { @@ -582,23 +588,23 @@ class Configuration { _volatilePassword = volatilePassword; } - String getVolatilePassword() { + String? getVolatilePassword() { return _volatilePassword; } - Future skipBackupFolderSelection() async { - await _preferences.setBool(keyHasSkippedBackupFolderSelection, true); + Future 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 _setSelectAllFoldersForBackup(bool value) async { + Future 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)!; } } diff --git a/lib/core/constants.dart b/lib/core/constants.dart index e028840a9..a51250130 100644 --- a/lib/core/constants.dart +++ b/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'; diff --git a/lib/core/error-reporting/super_logging.dart b/lib/core/error-reporting/super_logging.dart index cee59567f..10125cf73 100644 --- a/lib/core/error-reporting/super_logging.dart +++ b/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"); } diff --git a/lib/core/errors.dart b/lib/core/errors.dart index 3f3630382..f4f1bbde2 100644 --- a/lib/core/errors.dart +++ b/lib/core/errors.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - class InvalidFileError extends ArgumentError { InvalidFileError(String message) : super(message); } diff --git a/lib/core/network.dart b/lib/core/network.dart index aef529db3..bc2d82a1a 100644 --- a/lib/core/network.dart +++ b/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 init() async { await FkUserAgent.init(); diff --git a/lib/data/holidays.dart b/lib/data/holidays.dart index 831da3f1c..c8ed8f8b0 100644 --- a/lib/data/holidays.dart +++ b/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 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), ]; diff --git a/lib/data/months.dart b/lib/data/months.dart index d1eab1e96..530a869c1 100644 --- a/lib/data/months.dart +++ b/lib/data/months.dart @@ -1,7 +1,3 @@ -// @dart=2.9 - -import 'package:photos/models/search/month_search_result.dart'; - List allMonths = [ MonthData('January', 1), MonthData('February', 2), @@ -16,3 +12,10 @@ List allMonths = [ MonthData('November', 11), MonthData('December', 12), ]; + +class MonthData { + final String name; + final int monthNumber; + + MonthData(this.name, this.monthNumber); +} diff --git a/lib/data/years.dart b/lib/data/years.dart index b7acc3fb5..2b2e04d91 100644 --- a/lib/data/years.dart +++ b/lib/data/years.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'package:photos/utils/date_time_util.dart'; class YearsData { diff --git a/lib/db/collections_db.dart b/lib/db/collections_db.dart index 275a6f3fd..9fc9c2000 100644 --- a/lib/db/collections_db.dart +++ b/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 _dbFuture; + static Future? _dbFuture; Future get database async { _dbFuture ??= _initDatabase(); - return _dbFuture; + return _dbFuture!; } Future _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 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 deleteCollection(int collectionID) async { final db = await instance.database; return db.delete( @@ -206,7 +191,7 @@ class CollectionsDB { Map _getRowForCollection(Collection collection) { final row = {}; 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; } diff --git a/lib/db/device_files_db.dart b/lib/db/device_files_db.dart new file mode 100644 index 000000000..d23343db7 --- /dev/null +++ b/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 insertPathIDToLocalIDMapping( + Map> 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 deletePathIDToLocalIDMapping( + Map> 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> 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 = {}; + for (final row in rows) { + result[row['path_id']] = row["count"]; + } + return result; + } catch (e) { + _logger.severe("failed to getDevicePathIDToImportedFileCount", e); + rethrow; + } + } + + Future>> getDevicePathIDToLocalIDMap() async { + try { + final db = await database; + final rows = await db.rawQuery( + ''' SELECT id, path_id FROM device_files; ''', + ); + final result = >{}; + for (final row in rows) { + final String pathID = row['path_id']; + if (!result.containsKey(pathID)) { + result[pathID] = {}; + } + result[pathID].add(row['id']); + } + return result; + } catch (e) { + _logger.severe("failed to getDevicePathIDToLocalIDMap", e); + rethrow; + } + } + + Future> getDevicePathIDs() async { + final Database db = await database; + final rows = await db.rawQuery( + ''' + SELECT id FROM device_collections + ''', + ); + final Set result = {}; + for (final row in rows) { + result.add(row['id']); + } + return result; + } + + Future insertLocalAssets( + List localPathAssets, { + bool shouldAutoBackup = false, + }) async { + final Database db = await database; + final Map> pathIDToLocalIDsMap = {}; + try { + final batch = db.batch(); + final Set 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 updateDeviceCoverWithCount( + List> devicePathInfo, { + bool shouldBackup = false, + }) async { + bool hasUpdated = false; + try { + final Database db = await database; + final Set existingPathIds = await getDevicePathIDs(); + for (Tuple2 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> 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 result = {}; + for (final row in rows) { + result.add(row['collection_id']); + } + return result; + } + + Future updateDevicePathSyncStatus(Map 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 updateDeviceCollection( + String pathID, + int collectionID, + ) async { + final db = await database; + await db.update( + "device_collections", + {"collection_id": collectionID}, + where: 'id = ?', + whereArgs: [pathID], + ); + return; + } + + Future 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> getDeviceCollections({ + bool includeCoverThumbnail = false, + }) async { + debugPrint( + "Fetching DeviceCollections From DB with thumbnail = " + "$includeCoverThumbnail", + ); + try { + final db = await database; + final coverFiles = []; + 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 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; + } + } +} diff --git a/lib/db/file_updation_db.dart b/lib/db/file_updation_db.dart index 276dc8b46..c3d2bab4f 100644 --- a/lib/db/file_updation_db.dart +++ b/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 _dbFuture; + static Future? _dbFuture; Future 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> getLocalIDsForPotentialReUpload( + Future> 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 = []; + final result = []; for (final row in rows) { - result.add(row[columnLocalID]); + result.add(row[columnLocalID] as String?); } return result; } - Map _getRowForReUploadTable(String localID, String reason) { + Map _getRowForReUploadTable(String? localID, String reason) { assert(localID != null); final row = {}; row[columnLocalID] = localID; diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index c00649d66..dccdbf7b6 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -1,6 +1,6 @@ // @dart=2.9 -import 'dart:io'; +import 'dart:io' as io; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; @@ -29,7 +29,7 @@ class FilesDB { static final Logger _logger = Logger("FilesDB"); - static const table = 'files'; + static const filesTable = 'files'; static const tempTable = 'temp_files'; static const columnGeneratedID = '_id'; @@ -56,6 +56,7 @@ class FilesDB { static const columnFileDecryptionHeader = 'file_decryption_header'; static const columnThumbnailDecryptionHeader = 'thumbnail_decryption_header'; static const columnMetadataDecryptionHeader = 'metadata_decryption_header'; + static const columnFileSize = 'file_size'; // MMD -> Magic Metadata static const columnMMdEncodedJson = 'mmd_encoded_json'; @@ -69,7 +70,7 @@ class FilesDB { // we need to write query based on that field static const columnMMdVisibility = 'mmd_visibility'; - static final initializationScript = [...createTable(table)]; + static final initializationScript = [...createTable(filesTable)]; static final migrationScripts = [ ...alterDeviceFolderToAllowNULL(), ...alterTimestampColumnTypes(), @@ -77,7 +78,9 @@ class FilesDB { ...addMetadataColumns(), ...addMagicMetadataColumns(), ...addUniqueConstraintOnCollectionFiles(), - ...addPubMagicMetadataColumns() + ...addPubMagicMetadataColumns(), + ...createOnDeviceFilesAndPathCollection(), + ...addFileSizeColumn(), ]; final dbConfig = MigrationConfig( @@ -101,7 +104,7 @@ class FilesDB { // this opens the database (and creates it if it doesn't exist) Future _initDatabase() async { - final Directory documentsDirectory = + final io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); _logger.info("DB path " + path); @@ -141,16 +144,16 @@ class FilesDB { static List addIndices() { return [ ''' - CREATE INDEX IF NOT EXISTS collection_id_index ON $table($columnCollectionID); + CREATE INDEX IF NOT EXISTS collection_id_index ON $filesTable($columnCollectionID); ''', ''' - CREATE INDEX IF NOT EXISTS device_folder_index ON $table($columnDeviceFolder); + CREATE INDEX IF NOT EXISTS device_folder_index ON $filesTable($columnDeviceFolder); ''', ''' - CREATE INDEX IF NOT EXISTS creation_time_index ON $table($columnCreationTime); + CREATE INDEX IF NOT EXISTS creation_time_index ON $filesTable($columnCreationTime); ''', ''' - CREATE INDEX IF NOT EXISTS updation_time_index ON $table($columnUpdationTime); + CREATE INDEX IF NOT EXISTS updation_time_index ON $filesTable($columnUpdationTime); ''' ]; } @@ -161,12 +164,12 @@ class FilesDB { ''' INSERT INTO $tempTable SELECT * - FROM $table; + FROM $filesTable; - DROP TABLE $table; + DROP TABLE $filesTable; ALTER TABLE $tempTable - RENAME TO $table; + RENAME TO $filesTable; ''' ]; } @@ -220,14 +223,14 @@ class FilesDB { $columnMetadataDecryptionHeader, CAST($columnCreationTime AS INTEGER), CAST($columnUpdationTime AS INTEGER) - FROM $table; + FROM $filesTable; ''', ''' - DROP TABLE $table; + DROP TABLE $filesTable; ''', ''' ALTER TABLE $tempTable - RENAME TO $table; + RENAME TO $filesTable; ''', ]; } @@ -235,19 +238,19 @@ class FilesDB { static List addMetadataColumns() { return [ ''' - ALTER TABLE $table ADD COLUMN $columnFileSubType INTEGER; + ALTER TABLE $filesTable ADD COLUMN $columnFileSubType INTEGER; ''', ''' - ALTER TABLE $table ADD COLUMN $columnDuration INTEGER; + ALTER TABLE $filesTable ADD COLUMN $columnDuration INTEGER; ''', ''' - ALTER TABLE $table ADD COLUMN $columnExif TEXT; + ALTER TABLE $filesTable ADD COLUMN $columnExif TEXT; ''', ''' - ALTER TABLE $table ADD COLUMN $columnHash TEXT; + ALTER TABLE $filesTable ADD COLUMN $columnHash TEXT; ''', ''' - ALTER TABLE $table ADD COLUMN $columnMetadataVersion INTEGER; + ALTER TABLE $filesTable ADD COLUMN $columnMetadataVersion INTEGER; ''', ]; } @@ -255,13 +258,13 @@ class FilesDB { static List addMagicMetadataColumns() { return [ ''' - ALTER TABLE $table ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}'; + ALTER TABLE $filesTable ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}'; ''', ''' - ALTER TABLE $table ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0; + ALTER TABLE $filesTable ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0; ''', ''' - ALTER TABLE $table ADD COLUMN $columnMMdVisibility INTEGER DEFAULT $kVisibilityVisible; + ALTER TABLE $filesTable ADD COLUMN $columnMMdVisibility INTEGER DEFAULT $visibilityVisible; ''' ]; } @@ -269,20 +272,20 @@ class FilesDB { static List addUniqueConstraintOnCollectionFiles() { return [ ''' - DELETE from $table where $columnCollectionID || '-' || $columnUploadedFileID IN - (SELECT $columnCollectionID || '-' || $columnUploadedFileID from $table WHERE + DELETE from $filesTable where $columnCollectionID || '-' || $columnUploadedFileID IN + (SELECT $columnCollectionID || '-' || $columnUploadedFileID from $filesTable WHERE $columnCollectionID is not NULL AND $columnUploadedFileID is NOT NULL AND $columnCollectionID != -1 AND $columnUploadedFileID != -1 GROUP BY ($columnCollectionID || '-' || $columnUploadedFileID) HAVING count(*) > 1) AND ($columnCollectionID || '-' || $columnUploadedFileID || '-' || $columnGeneratedID) NOT IN (SELECT $columnCollectionID || '-' || $columnUploadedFileID || '-' || max($columnGeneratedID) - from $table WHERE + from $filesTable WHERE $columnCollectionID is not NULL AND $columnUploadedFileID is NOT NULL AND $columnCollectionID != -1 AND $columnUploadedFileID != -1 GROUP BY ($columnCollectionID || '-' || $columnUploadedFileID) HAVING count(*) > 1); ''', ''' - CREATE UNIQUE INDEX IF NOT EXISTS cid_uid ON $table ($columnCollectionID, $columnUploadedFileID) + CREATE UNIQUE INDEX IF NOT EXISTS cid_uid ON $filesTable ($columnCollectionID, $columnUploadedFileID) WHERE $columnCollectionID is not NULL AND $columnUploadedFileID is not NULL AND $columnCollectionID != -1 AND $columnUploadedFileID != -1; ''' @@ -292,20 +295,72 @@ class FilesDB { static List addPubMagicMetadataColumns() { return [ ''' - ALTER TABLE $table ADD COLUMN $columnPubMMdEncodedJson TEXT DEFAULT '{}'; + ALTER TABLE $filesTable ADD COLUMN $columnPubMMdEncodedJson TEXT DEFAULT '{}'; ''', ''' - ALTER TABLE $table ADD COLUMN $columnPubMMdVersion INTEGER DEFAULT 0; + ALTER TABLE $filesTable ADD COLUMN $columnPubMMdVersion INTEGER DEFAULT 0; ''' ]; } + static List createOnDeviceFilesAndPathCollection() { + return [ + ''' + CREATE TABLE IF NOT EXISTS device_files ( + id TEXT NOT NULL, + path_id TEXT NOT NULL, + UNIQUE(id, path_id) + ); + ''', + ''' + CREATE TABLE IF NOT EXISTS device_collections ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT, + modified_at INTEGER NOT NULL DEFAULT 0, + should_backup INTEGER NOT NULL DEFAULT 0, + count INTEGER NOT NULL DEFAULT 0, + collection_id INTEGER DEFAULT -1, + upload_strategy INTEGER DEFAULT 0, + cover_id TEXT + ); + ''', + ''' + CREATE INDEX IF NOT EXISTS df_id_idx ON device_files (id); + ''', + ''' + CREATE INDEX IF NOT EXISTS df_path_id_idx ON device_files (path_id); + ''', + ]; + } + + static List addFileSizeColumn() { + return [ + ''' + ALTER TABLE $filesTable ADD COLUMN $columnFileSize INTEGER; + ''', + ]; + } + Future clearTable() async { final db = await instance.database; - await db.delete(table); + await db.delete(filesTable); } - Future insertMultiple(List files) async { + Future deleteDB() async { + if (kDebugMode) { + debugPrint("Deleting files db"); + final io.Directory documentsDirectory = + await getApplicationDocumentsDirectory(); + final String path = join(documentsDirectory.path, _databaseName); + io.File(path).deleteSync(recursive: true); + _dbFuture = null; + } + } + + Future insertMultiple( + List files, { + ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace, + }) async { final startTime = DateTime.now(); final db = await instance.database; var batch = db.batch(); @@ -317,9 +372,9 @@ class FilesDB { batchCounter = 0; } batch.insert( - table, + filesTable, _getRowForFile(file), - conflictAlgorithm: ConflictAlgorithm.replace, + conflictAlgorithm: conflictAlgorithm, ); batchCounter++; } @@ -341,7 +396,7 @@ class FilesDB { Future insert(File file) async { final db = await instance.database; return db.insert( - table, + filesTable, _getRowForFile(file), conflictAlgorithm: ConflictAlgorithm.replace, ); @@ -350,20 +405,20 @@ class FilesDB { Future getFile(int generatedID) async { final db = await instance.database; final results = await db.query( - table, + filesTable, where: '$columnGeneratedID = ?', whereArgs: [generatedID], ); if (results.isEmpty) { return null; } - return _convertToFiles(results)[0]; + return convertToFiles(results)[0]; } Future getUploadedFile(int uploadedID, int collectionID) async { final db = await instance.database; final results = await db.query( - table, + filesTable, where: '$columnUploadedFileID = ? AND $columnCollectionID = ?', whereArgs: [ uploadedID, @@ -373,13 +428,13 @@ class FilesDB { if (results.isEmpty) { return null; } - return _convertToFiles(results)[0]; + return convertToFiles(results)[0]; } Future> getUploadedFileIDs(int collectionID) async { final db = await instance.database; final results = await db.query( - table, + filesTable, columns: [columnUploadedFileID], where: '$columnCollectionID = ?', whereArgs: [ @@ -396,7 +451,7 @@ class FilesDB { Future getBackedUpIDs() async { final db = await instance.database; final results = await db.query( - table, + filesTable, columns: [columnLocalID, columnUploadedFileID], where: '$columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', @@ -410,28 +465,28 @@ class FilesDB { return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList()); } - Future getAllUploadedFiles( + Future getAllPendingOrUploadedFiles( int startTime, int endTime, int ownerID, { int limit, bool asc, - int visibility = kVisibilityVisible, + int visibility = visibilityVisible, Set ignoredCollectionIDs, }) async { final db = await instance.database; final order = (asc ?? false ? 'ASC' : 'DESC'); final results = await db.query( - table, + filesTable, where: - '$columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnOwnerID = ? AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)' + '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1)' ' AND $columnMMdVisibility = ?', whereArgs: [startTime, endTime, ownerID, visibility], orderBy: '$columnCreationTime ' + order + ', $columnModificationTime ' + order, limit: limit, ); - final files = _convertToFiles(results); + final files = convertToFiles(results); final List deduplicatedFiles = _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); return FileLoadResult(deduplicatedFiles, files.length == limit); @@ -439,11 +494,11 @@ class FilesDB { Future> getCollectionIDsOfHiddenFiles( int ownerID, { - int visibility = kVisibilityArchive, + int visibility = visibilityArchive, }) async { final db = await instance.database; final results = await db.query( - table, + filesTable, where: '$columnOwnerID = ? AND $columnMMdVisibility = ? AND $columnCollectionID != -1', columns: [columnCollectionID], @@ -468,54 +523,22 @@ class FilesDB { final db = await instance.database; final order = (asc ?? false ? 'ASC' : 'DESC'); final results = await db.query( - table, + filesTable, where: '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)' ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))', - whereArgs: [startTime, endTime, ownerID, kVisibilityVisible], + whereArgs: [startTime, endTime, ownerID, visibilityVisible], orderBy: '$columnCreationTime ' + order + ', $columnModificationTime ' + order, limit: limit, ); - final files = _convertToFiles(results); + final files = convertToFiles(results); final List deduplicatedFiles = _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); return FileLoadResult(deduplicatedFiles, files.length == limit); } - Future getImportantFiles( - int startTime, - int endTime, - int ownerID, - List paths, { - int limit, - bool asc, - Set ignoredCollectionIDs, - }) async { - final db = await instance.database; - String inParam = ""; - for (final path in paths) { - inParam += "'" + path.replaceAll("'", "''") + "',"; - } - inParam = inParam.substring(0, inParam.length - 1); - final order = (asc ?? false ? 'ASC' : 'DESC'); - final results = await db.query( - table, - where: - '$columnCreationTime >= ? AND $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) AND ($columnMMdVisibility IS NULL OR $columnMMdVisibility = ?)' - 'AND (($columnLocalID IS NOT NULL AND $columnDeviceFolder IN ($inParam)) OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))', - whereArgs: [startTime, endTime, ownerID, kVisibilityVisible], - orderBy: - '$columnCreationTime ' + order + ', $columnModificationTime ' + order, - limit: limit, - ); - final files = _convertToFiles(results); - final List deduplicatedFiles = - _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); - return FileLoadResult(deduplicatedFiles, files.length == limit); - } - - List _deduplicateByLocalID(List files) { + List deduplicateByLocalID(List files) { final localIDs = {}; final List deduplicatedFiles = []; for (final file in files) { @@ -570,7 +593,7 @@ class FilesDB { int endTime, { int limit, bool asc, - int visibility = kVisibilityVisible, + int visibility = visibilityVisible, }) async { final db = await instance.database; final order = (asc ?? false ? 'ASC' : 'DESC'); @@ -587,94 +610,30 @@ class FilesDB { } final results = await db.query( - table, + filesTable, where: whereClause, whereArgs: whereArgs, orderBy: '$columnCreationTime ' + order + ', $columnModificationTime ' + order, limit: limit, ); - final files = _convertToFiles(results); + final files = convertToFiles(results); _logger.info("Fetched " + files.length.toString() + " files"); return FileLoadResult(files, files.length == limit); } - Future getFilesInPath( - String path, - int startTime, - int endTime, { - int limit, - bool asc, - }) async { - final db = await instance.database; - final order = (asc ?? false ? 'ASC' : 'DESC'); - final results = await db.query( - table, - where: - '$columnDeviceFolder = ? AND $columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnLocalID IS NOT NULL', - whereArgs: [path, startTime, endTime], - orderBy: - '$columnCreationTime ' + order + ', $columnModificationTime ' + order, - groupBy: columnLocalID, - limit: limit, - ); - final files = _convertToFiles(results); - return FileLoadResult(files, files.length == limit); - } - - Future getLocalDeviceFiles( - int startTime, - int endTime, { - int limit, - bool asc, - }) async { - final db = await instance.database; - final order = (asc ?? false ? 'ASC' : 'DESC'); - final results = await db.query( - table, - where: - '$columnCreationTime >= ? AND $columnCreationTime <= ? AND $columnLocalID IS NOT NULL', - whereArgs: [startTime, endTime], - orderBy: - '$columnCreationTime ' + order + ', $columnModificationTime ' + order, - limit: limit, - ); - final files = _convertToFiles(results); - final result = _deduplicateByLocalID(files); - return FileLoadResult(result, files.length == limit); - } - - Future> getAllVideos() async { - final db = await instance.database; - final results = await db.query( - table, - where: '$columnLocalID IS NOT NULL AND $columnFileType = 1', - orderBy: '$columnCreationTime DESC', - ); - return _convertToFiles(results); - } - - Future> getAllInPath(String path) async { - final db = await instance.database; - final results = await db.query( - table, - where: '$columnLocalID IS NOT NULL AND $columnDeviceFolder = ?', - whereArgs: [path], - orderBy: '$columnCreationTime DESC', - groupBy: columnLocalID, - ); - return _convertToFiles(results); - } - Future> getFilesCreatedWithinDurations( List> durations, Set ignoredCollectionIDs, { String order = 'ASC', }) async { + if (durations.isEmpty) { + return []; + } final db = await instance.database; String whereClause = "( "; for (int index = 0; index < durations.length; index++) { - whereClause += "($columnCreationTime > " + + whereClause += "($columnCreationTime >= " + durations[index][0].toString() + " AND $columnCreationTime < " + durations[index][1].toString() + @@ -683,43 +642,22 @@ class FilesDB { whereClause += " OR "; } } - whereClause += ") AND $columnMMdVisibility = $kVisibilityVisible"; + whereClause += ") AND $columnMMdVisibility = $visibilityVisible"; final results = await db.query( - table, + filesTable, where: whereClause, orderBy: '$columnCreationTime ' + order, ); - final files = _convertToFiles(results); + final files = convertToFiles(results); return _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); } - Future> getFilesToBeUploadedWithinFolders( - Set folders, - ) async { - if (folders.isEmpty) { - return []; - } - final db = await instance.database; - String inParam = ""; - for (final folder in folders) { - inParam += "'" + folder.replaceAll("'", "''") + "',"; - } - inParam = inParam.substring(0, inParam.length - 1); - final results = await db.query( - table, - where: - '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnDeviceFolder IN ($inParam)', - orderBy: '$columnCreationTime DESC', - groupBy: columnLocalID, - ); - return _convertToFiles(results); - } - - // Files which user added to a collection manually but they are not uploaded yet. - Future> getPendingManualUploads() async { + // Files which user added to a collection manually but they are not + // uploaded yet or files belonging to a collection which is marked for backup + Future> getFilesPendingForUpload() async { final db = await instance.database; final results = await db.query( - table, + filesTable, where: '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND ' '$columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 AND ' @@ -727,7 +665,7 @@ class FilesDB { orderBy: '$columnCreationTime DESC', groupBy: columnLocalID, ); - final files = _convertToFiles(results); + final files = convertToFiles(results); // future-safe filter just to ensure that the query doesn't end up returning files // which should not be backed up files.removeWhere( @@ -739,37 +677,28 @@ class FilesDB { return files; } - Future> getAllLocalFiles() async { + Future> getUnUploadedLocalFiles() async { final db = await instance.database; final results = await db.query( - table, + filesTable, where: '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL', orderBy: '$columnCreationTime DESC', groupBy: columnLocalID, ); - return _convertToFiles(results); + return convertToFiles(results); } - Future> getEditedRemoteFiles() async { - final db = await instance.database; - final results = await db.query( - table, - where: - '($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1) AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1)', - orderBy: '$columnCreationTime DESC', - groupBy: columnLocalID, - ); - return _convertToFiles(results); - } - - Future> getUploadedFileIDsToBeUpdated() async { + Future> getUploadedFileIDsToBeUpdated(int ownerID) async { final db = await instance.database; final rows = await db.query( - table, + filesTable, columns: [columnUploadedFileID], - where: - '($columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL)', + where: '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' + '($columnUploadedFileID ' + 'IS NOT ' + 'NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL)', + whereArgs: [ownerID], orderBy: '$columnCreationTime DESC', distinct: true, ); @@ -783,7 +712,7 @@ class FilesDB { Future getUploadedFileInAnyCollection(int uploadedFileID) async { final db = await instance.database; final results = await db.query( - table, + filesTable, where: '$columnUploadedFileID = ?', whereArgs: [ uploadedFileID, @@ -793,13 +722,13 @@ class FilesDB { if (results.isEmpty) { return null; } - return _convertToFiles(results)[0]; + return convertToFiles(results)[0]; } Future> getExistingLocalFileIDs() async { final db = await instance.database; final rows = await db.query( - table, + filesTable, columns: [columnLocalID], distinct: true, where: '$columnLocalID IS NOT NULL', @@ -811,10 +740,66 @@ class FilesDB { return result; } + Future> getLocalIDsMarkedForOrAlreadyUploaded(int ownerID) async { + final db = await instance.database; + final rows = await db.query( + filesTable, + columns: [columnLocalID], + distinct: true, + where: '$columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL ' + 'AND ' + '$columnCollectionID != -1) AND ($columnOwnerID = ? OR ' + '$columnOwnerID IS NULL)', + whereArgs: [ownerID], + ); + final result = {}; + for (final row in rows) { + result.add(row[columnLocalID]); + } + return result; + } + + Future> getLocalFileIDsForCollection(int collectionID) async { + final db = await instance.database; + final rows = await db.query( + filesTable, + columns: [columnLocalID], + where: '$columnLocalID IS NOT NULL AND $columnCollectionID = ?', + whereArgs: [collectionID], + ); + final result = {}; + for (final row in rows) { + result.add(row[columnLocalID]); + } + return result; + } + + // Sets the collectionID for the files with given LocalIDs if the + // corresponding file entries are not already mapped to some other collection + Future setCollectionIDForUnMappedLocalFiles( + int collectionID, + Set localIDs, + ) async { + final db = await instance.database; + String inParam = ""; + for (final localID in localIDs) { + inParam += "'" + localID + "',"; + } + inParam = inParam.substring(0, inParam.length - 1); + return await db.rawUpdate( + ''' + UPDATE $filesTable + SET $columnCollectionID = $collectionID + WHERE $columnLocalID IN ($inParam) AND ($columnCollectionID IS NULL OR + $columnCollectionID = -1); + ''', + ); + } + Future getNumberOfUploadedFiles() async { final db = await instance.database; final rows = await db.query( - table, + filesTable, columns: [columnUploadedFileID], where: '($columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NOT NULL)', @@ -833,7 +818,7 @@ class FilesDB { ) async { final db = await instance.database; return await db.update( - table, + filesTable, { columnTitle: title, columnLatitude: location.latitude, @@ -862,7 +847,7 @@ class FilesDB { // on iOS, match using localID and fileType. title can either match or // might be null based on how the file was imported String whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND - $columnLocalID = ? AND $columnFileType = ? AND + $columnLocalID = ? AND $columnFileType = ? AND ($columnTitle=? OR $columnTitle IS NULL) '''; List whereArgs = [ ownerID, @@ -870,7 +855,7 @@ class FilesDB { getInt(fileType), title, ]; - if (Platform.isAndroid) { + if (io.Platform.isAndroid) { whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND $columnLocalID = ? AND $columnFileType = ? AND $columnTitle=? AND $columnDeviceFolder= ? '''; @@ -884,12 +869,12 @@ class FilesDB { } final rows = await db.query( - table, + filesTable, where: whereClause, whereArgs: whereArgs, ); - return _convertToFiles(rows); + return convertToFiles(rows); } Future> getMatchingFiles( @@ -900,7 +885,7 @@ class FilesDB { ) async { final db = await instance.database; final rows = await db.query( - table, + filesTable, where: '''$columnTitle=? AND $columnDeviceFolder=?''', whereArgs: [ title, @@ -908,7 +893,7 @@ class FilesDB { ], ); if (rows.isNotEmpty) { - return _convertToFiles(rows); + return convertToFiles(rows); } else { return null; } @@ -923,10 +908,9 @@ class FilesDB { if (fileType == FileType.livePhoto && hashData.zipHash != null) { inParam += ",'${hashData.zipHash}'"; } - final db = await instance.database; final rows = await db.query( - table, + filesTable, where: '($columnUploadedFileID != NULL OR $columnUploadedFileID != -1) ' 'AND $columnOwnerID = ? AND $columnFileType =' ' ? ' @@ -936,13 +920,13 @@ class FilesDB { getInt(fileType), ], ); - return _convertToFiles(rows); + return convertToFiles(rows); } Future update(File file) async { final db = await instance.database; return await db.update( - table, + filesTable, _getRowForFile(file), where: '$columnGeneratedID = ?', whereArgs: [file.generatedID], @@ -952,7 +936,7 @@ class FilesDB { Future updateUploadedFileAcrossCollections(File file) async { final db = await instance.database; return await db.update( - table, + filesTable, _getRowForFileWithoutCollection(file), where: '$columnUploadedFileID = ?', whereArgs: [file.uploadedFileID], @@ -962,7 +946,7 @@ class FilesDB { Future updateLocalIDForUploaded(int uploadedID, String localID) async { final db = await instance.database; return await db.update( - table, + filesTable, {columnLocalID: localID}, where: '$columnUploadedFileID = ? AND $columnLocalID IS NULL', whereArgs: [uploadedID], @@ -972,7 +956,7 @@ class FilesDB { Future delete(int uploadedFileID) async { final db = await instance.database; return db.delete( - table, + filesTable, where: '$columnUploadedFileID =?', whereArgs: [uploadedFileID], ); @@ -981,7 +965,7 @@ class FilesDB { Future deleteByGeneratedID(int genID) async { final db = await instance.database; return db.delete( - table, + filesTable, where: '$columnGeneratedID =?', whereArgs: [genID], ); @@ -990,23 +974,34 @@ class FilesDB { Future deleteMultipleUploadedFiles(List uploadedFileIDs) async { final db = await instance.database; return await db.delete( - table, + filesTable, where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})', ); } + Future deleteMultipleByGeneratedIDs(List generatedIDs) async { + if (generatedIDs.isEmpty) { + return 0; + } + final db = await instance.database; + return await db.delete( + filesTable, + where: '$columnGeneratedID IN (${generatedIDs.join(', ')})', + ); + } + Future deleteLocalFile(File file) async { final db = await instance.database; if (file.localID != null) { // delete all files with same local ID return db.delete( - table, + filesTable, where: '$columnLocalID =?', whereArgs: [file.localID], ); } else { return db.delete( - table, + filesTable, where: '$columnGeneratedID =?', whereArgs: [file.generatedID], ); @@ -1022,7 +1017,7 @@ class FilesDB { final db = await instance.database; await db.rawQuery( ''' - UPDATE $table + UPDATE $filesTable SET $columnLocalID = NULL WHERE $columnLocalID IN ($inParam); ''', @@ -1037,10 +1032,10 @@ class FilesDB { inParam = inParam.substring(0, inParam.length - 1); final db = await instance.database; final results = await db.query( - table, + filesTable, where: '$columnLocalID IN ($inParam)', ); - return _convertToFiles(results); + return convertToFiles(results); } Future deleteUnSyncedLocalFiles(List localIDs) async { @@ -1051,7 +1046,7 @@ class FilesDB { inParam = inParam.substring(0, inParam.length - 1); final db = await instance.database; return db.delete( - table, + filesTable, where: '($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam)', ); @@ -1060,7 +1055,7 @@ class FilesDB { Future deleteFromCollection(int uploadedFileID, int collectionID) async { final db = await instance.database; return db.delete( - table, + filesTable, where: '$columnUploadedFileID = ? AND $columnCollectionID = ?', whereArgs: [uploadedFileID, collectionID], ); @@ -1072,7 +1067,7 @@ class FilesDB { ) async { final db = await instance.database; return db.delete( - table, + filesTable, where: '$columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')})', whereArgs: [collectionID], @@ -1083,7 +1078,7 @@ class FilesDB { final db = await instance.database; final count = Sqflite.firstIntValue( await db.rawQuery( - 'SELECT COUNT(*) FROM $table where $columnCollectionID = $collectionID', + 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = $collectionID', ), ); return count; @@ -1093,7 +1088,7 @@ class FilesDB { final db = await instance.database; final count = Sqflite.firstIntValue( await db.rawQuery( - 'SELECT COUNT(*) FROM $table where $columnMMdVisibility = $visibility AND $columnOwnerID = $ownerID', + 'SELECT COUNT(*) FROM $filesTable where $columnMMdVisibility = $visibility AND $columnOwnerID = $ownerID', ), ); return count; @@ -1102,7 +1097,7 @@ class FilesDB { Future deleteCollection(int collectionID) async { final db = await instance.database; return db.delete( - table, + filesTable, where: '$columnCollectionID = ?', whereArgs: [collectionID], ); @@ -1111,31 +1106,67 @@ class FilesDB { Future removeFromCollection(int collectionID, List fileIDs) async { final db = await instance.database; return db.delete( - table, + filesTable, where: '$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})', whereArgs: [collectionID], ); } + Future> getPendingUploadForCollection(int collectionID) async { + final db = await instance.database; + final results = await db.query( + filesTable, + where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NULL OR ' + '$columnUploadedFileID = -1)', + whereArgs: [collectionID], + ); + return convertToFiles(results); + } + + Future> getLocalIDsPresentInEntries( + List existingFiles, + int collectionID, + ) async { + String inParam = ""; + for (final existingFile in existingFiles) { + inParam += "'" + existingFile.localID + "',"; + } + inParam = inParam.substring(0, inParam.length - 1); + final db = await instance.database; + final rows = await db.rawQuery( + ''' + SELECT $columnLocalID + FROM $filesTable + WHERE $columnLocalID IN ($inParam) AND $columnCollectionID != + $collectionID AND $columnLocalID IS NOT NULL; + ''', + ); + final result = {}; + for (final row in rows) { + result.add(row[columnLocalID]); + } + return result; + } + Future> getLatestLocalFiles() async { final db = await instance.database; final rows = await db.rawQuery( ''' - SELECT $table.* - FROM $table + SELECT $filesTable.* + FROM $filesTable INNER JOIN ( SELECT $columnDeviceFolder, MAX($columnCreationTime) AS max_creation_time - FROM $table - WHERE $table.$columnLocalID IS NOT NULL + FROM $filesTable + WHERE $filesTable.$columnLocalID IS NOT NULL GROUP BY $columnDeviceFolder ) latest_files - ON $table.$columnDeviceFolder = latest_files.$columnDeviceFolder - AND $table.$columnCreationTime = latest_files.max_creation_time; + ON $filesTable.$columnDeviceFolder = latest_files.$columnDeviceFolder + AND $filesTable.$columnCreationTime = latest_files.max_creation_time; ''', ); - final files = _convertToFiles(rows); + final files = convertToFiles(rows); // TODO: Do this de-duplication within the SQL Query final folderMap = {}; for (final file in files) { @@ -1150,42 +1181,45 @@ class FilesDB { } Future> getLatestCollectionFiles() async { + debugPrint("Fetching latestCollectionFiles from db"); String query; if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) { query = ''' - SELECT $table.* - FROM $table + SELECT $filesTable.* + FROM $filesTable INNER JOIN ( SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time - FROM $table - WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 AND $columnMMdVisibility = $kVisibilityVisible) + FROM $filesTable + WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS + NOT -1 AND $columnMMdVisibility = $visibilityVisible AND + $columnUploadedFileID IS NOT -1) GROUP BY $columnCollectionID ) latest_files - ON $table.$columnCollectionID = latest_files.$columnCollectionID - AND $table.$columnCreationTime = latest_files.max_creation_time; + ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID + AND $filesTable.$columnCreationTime = latest_files.max_creation_time; '''; } else { query = ''' - SELECT $table.* - FROM $table + SELECT $filesTable.* + FROM $filesTable INNER JOIN ( SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time - FROM $table + FROM $filesTable WHERE ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1) GROUP BY $columnCollectionID ) latest_files - ON $table.$columnCollectionID = latest_files.$columnCollectionID - AND $table.$columnCreationTime = latest_files.max_creation_time; - '''; - } + ON $filesTable.$columnCollectionID = latest_files.$columnCollectionID + AND $filesTable.$columnCreationTime = latest_files.max_creation_time; + '''; + } final db = await instance.database; final rows = await db.rawQuery( query, ); - final files = _convertToFiles(rows); + final files = convertToFiles(rows); // TODO: Do this de-duplication within the SQL Query final collectionMap = {}; for (final file in files) { @@ -1204,7 +1238,7 @@ class FilesDB { final rows = await db.rawQuery( ''' SELECT COUNT(DISTINCT($columnLocalID)) as count, $columnDeviceFolder - FROM $table + FROM $filesTable WHERE $columnLocalID IS NOT NULL GROUP BY $columnDeviceFolder ''', @@ -1219,7 +1253,7 @@ class FilesDB { Future> getLocalFilesBackedUpWithoutLocation() async { final db = await instance.database; final rows = await db.query( - table, + filesTable, columns: [columnLocalID], distinct: true, where: @@ -1245,7 +1279,7 @@ class FilesDB { final db = await instance.database; await db.rawUpdate( ''' - UPDATE $table + UPDATE $filesTable SET $columnUpdationTime = NULL WHERE $columnLocalID IN ($inParam) AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0); @@ -1259,7 +1293,7 @@ class FilesDB { ) async { final db = await instance.database; final rows = await db.query( - table, + filesTable, where: '$columnUploadedFileID = ? AND $columnCollectionID = ?', whereArgs: [uploadedFileID, collectionID], limit: 1, @@ -1279,10 +1313,10 @@ class FilesDB { inParam = inParam.substring(0, inParam.length - 1); final db = await instance.database; final results = await db.query( - table, + filesTable, where: '$columnUploadedFileID IN ($inParam)', ); - final files = _convertToFiles(results); + final files = convertToFiles(results); for (final file in files) { result[file.uploadedFileID] = file; } @@ -1294,7 +1328,7 @@ class FilesDB { ) async { final db = await instance.database; final results = await db.query( - table, + filesTable, where: '$columnUploadedFileID = ? AND $columnCollectionID != -1', columns: [columnCollectionID], whereArgs: [uploadedFileID], @@ -1307,7 +1341,7 @@ class FilesDB { return collectionIDsOfFile; } - List _convertToFiles(List> results) { + List convertToFiles(List> results) { final List files = []; for (final result in results) { files.add(_getFileFromRow(result)); @@ -1317,8 +1351,8 @@ class FilesDB { Future> getAllFilesFromDB() async { final db = await instance.database; - final List> result = await db.query(table); - final List files = _convertToFiles(result); + final List> result = await db.query(filesTable); + final List files = convertToFiles(result); final List deduplicatedFiles = _deduplicatedAndFilterIgnoredFiles(files, null); return deduplicatedFiles; @@ -1353,10 +1387,11 @@ class FilesDB { row[columnExif] = file.exif; row[columnHash] = file.hash; row[columnMetadataVersion] = file.metadataVersion; + row[columnFileSize] = file.fileSize; row[columnMMdVersion] = file.mMdVersion ?? 0; row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}'; row[columnMMdVisibility] = - file.magicMetadata?.visibility ?? kVisibilityVisible; + file.magicMetadata?.visibility ?? visibilityVisible; row[columnPubMMdVersion] = file.pubMmdVersion ?? 0; row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}'; if (file.pubMagicMetadata != null && @@ -1395,7 +1430,7 @@ class FilesDB { row[columnMMdVersion] = file.mMdVersion ?? 0; row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}'; row[columnMMdVisibility] = - file.magicMetadata?.visibility ?? kVisibilityVisible; + file.magicMetadata?.visibility ?? visibilityVisible; row[columnPubMMdVersion] = file.pubMmdVersion ?? 0; row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}'; @@ -1436,6 +1471,7 @@ class FilesDB { file.exif = row[columnExif]; file.hash = row[columnHash]; file.metadataVersion = row[columnMetadataVersion] ?? 0; + file.fileSize = row[columnFileSize]; file.mMdVersion = row[columnMMdVersion] ?? 0; file.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}'; diff --git a/lib/db/ignored_files_db.dart b/lib/db/ignored_files_db.dart index 26fbb2fd0..1a20b2f67 100644 --- a/lib/db/ignored_files_db.dart +++ b/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 _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 removeIgnoredEntries(List 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 row) { return IgnoredFile( row[columnLocalID], diff --git a/lib/db/public_keys_db.dart b/lib/db/public_keys_db.dart index 0070106de..03ab61036 100644 --- a/lib/db/public_keys_db.dart +++ b/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 _dbFuture; + static Future? _dbFuture; Future get database async { _dbFuture ??= _initDatabase(); - return _dbFuture; + return _dbFuture!; } Future _initDatabase() async { - final Directory documentsDirectory = await getApplicationDocumentsDirectory(); + final Directory documentsDirectory = + await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); return await openDatabase( path, diff --git a/lib/db/trash_db.dart b/lib/db/trash_db.dart index 5cc303037..b4e7ef52b 100644 --- a/lib/db/trash_db.dart +++ b/lib/db/trash_db.dart @@ -87,7 +87,8 @@ class TrashDB { // this opens the database (and creates it if it doesn't exist) Future _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 ?? '{}'; diff --git a/lib/db/upload_locks_db.dart b/lib/db/upload_locks_db.dart index 2648ea4df..48eb2e810 100644 --- a/lib/db/upload_locks_db.dart +++ b/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 _dbFuture; + static Future? _dbFuture; Future get database async { _dbFuture ??= _initDatabase(); - return _dbFuture; + return _dbFuture!; } Future _initDatabase() async { - final Directory documentsDirectory = await getApplicationDocumentsDirectory(); + final Directory documentsDirectory = + await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); return await openDatabase( path, diff --git a/lib/ente_theme_data.dart b/lib/ente_theme_data.dart index 425f52517..6dfcacada 100644 --- a/lib/ente_theme_data.dart +++ b/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; - - Color get defaultTextColor => - brightness == Brightness.light ? Colors.black : Colors.white; - - Color get inverseTextColor => - brightness == Brightness.light ? Colors.white : Colors.black; - - Color get inverseIconColor => - brightness == Brightness.light ? Colors.white : Colors.black; + brightness == Brightness.light ? backgroundBaseLight : backgroundBaseDark; Color get inverseBackgroundColor => - brightness == Brightness.light ? Colors.black : Colors.white; + brightness != Brightness.light ? backgroundBaseLight : backgroundBaseDark; + + Color get defaultTextColor => + brightness == Brightness.light ? textBaseLight : textBaseDark; + + Color get inverseTextColor => + 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 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( diff --git a/lib/events/local_import_progress.dart b/lib/events/local_import_progress.dart new file mode 100644 index 000000000..a046e540e --- /dev/null +++ b/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); +} diff --git a/lib/events/sync_status_update_event.dart b/lib/events/sync_status_update_event.dart index 5f9e213b7..5566881a9 100644 --- a/lib/events/sync_status_update_event.dart +++ b/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 { diff --git a/lib/events/tab_changed_event.dart b/lib/events/tab_changed_event.dart index 6b4fb4e8f..9ecd71811 100644 --- a/lib/events/tab_changed_event.dart +++ b/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 { diff --git a/lib/main.dart b/lib/main.dart index 3a78a1f7f..a981b903f 100644 --- a/lib/main.dart +++ b/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, ), diff --git a/lib/models/backup_status.dart b/lib/models/backup_status.dart index aad1de06c..af2ec9331 100644 --- a/lib/models/backup_status.dart +++ b/lib/models/backup_status.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - class BackupStatus { final List localIDs; final int size; diff --git a/lib/models/billing_plan.dart b/lib/models/billing_plan.dart index f69628179..c875f0946 100644 --- a/lib/models/billing_plan.dart +++ b/lib/models/billing_plan.dart @@ -1,36 +1,22 @@ -// @dart=2.9 - import 'dart:convert'; -import 'package:flutter/foundation.dart'; - class BillingPlans { final List plans; final FreePlan freePlan; BillingPlans({ - this.plans, - this.freePlan, + required this.plans, + required this.freePlan, }); - BillingPlans copyWith({ - List plans, - FreePlan freePlan, - }) { - return BillingPlans( - plans: plans ?? this.plans, - freePlan: freePlan ?? this.freePlan, - ); - } - Map 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 map) { + static fromMap(Map? 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 toMap() { return { 'storage': storage, @@ -92,7 +49,7 @@ class FreePlan { }; } - factory FreePlan.fromMap(Map map) { + static fromMap(Map? 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 toMap() { return { 'id': id, @@ -176,7 +91,7 @@ class BillingPlan { }; } - factory BillingPlan.fromMap(Map map) { + static fromMap(Map? 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; - } } diff --git a/lib/models/collection.dart b/lib/models/collection.dart index 213d9c5b5..5f220aeb7 100644 --- a/lib/models/collection.dart +++ b/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 sharees; - final List publicURLs; + final List? sharees; + final List? 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 sharees, - List 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? sharees, + List? 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 map) { + static fromMap(Map? map) { if (map == null) return null; final sharees = (map['sharees'] == null || map['sharees'].length == 0) ? [] @@ -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 toMap() { final map = {}; if (encryptedPath != null) { @@ -242,7 +180,7 @@ class CollectionAttributes { return map; } - factory CollectionAttributes.fromMap(Map map) { + static fromMap(Map? 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 toMap() { return { 'id': id, @@ -307,7 +210,7 @@ class User { }; } - factory User.fromMap(Map map) { + static fromMap(Map? 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 toMap() { @@ -360,7 +250,7 @@ class PublicURL { }; } - factory PublicURL.fromMap(Map map) { + static fromMap(Map? 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; } diff --git a/lib/models/collection_file_item.dart b/lib/models/collection_file_item.dart index 80ca196c1..514808cb9 100644 --- a/lib/models/collection_file_item.dart +++ b/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 map) { + static fromMap(Map? map) { if (map == null) return null; return CollectionFileItem( diff --git a/lib/models/collection_items.dart b/lib/models/collection_items.dart index e9f3abf56..dce457a77 100644 --- a/lib/models/collection_items.dart +++ b/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 folders; - final List collections; - - CollectionItems(this.folders, this.collections); -} - class CollectionWithThumbnail { final Collection collection; - final File thumbnail; + final File? thumbnail; CollectionWithThumbnail( this.collection, diff --git a/lib/models/delete_account.dart b/lib/models/delete_account.dart index 49ce9e955..71ca04e9c 100644 --- a/lib/models/delete_account.dart +++ b/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, }); } diff --git a/lib/models/derived_key_result.dart b/lib/models/derived_key_result.dart index c67e4af96..a071fb1f8 100644 --- a/lib/models/derived_key_result.dart +++ b/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); } diff --git a/lib/models/device_collection.dart b/lib/models/device_collection.dart new file mode 100644 index 000000000..98991e069 --- /dev/null +++ b/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, + }); +} diff --git a/lib/models/device_folder.dart b/lib/models/device_folder.dart deleted file mode 100644 index e7e8dd604..000000000 --- a/lib/models/device_folder.dart +++ /dev/null @@ -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, - ); -} diff --git a/lib/models/duplicate_files.dart b/lib/models/duplicate_files.dart index 3d74dee85..2834ba9be 100644 --- a/lib/models/duplicate_files.dart +++ b/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); }); } diff --git a/lib/models/encryption_result.dart b/lib/models/encryption_result.dart index 29c11b98c..9da16c573 100644 --- a/lib/models/encryption_result.dart +++ b/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, + }); } diff --git a/lib/models/ente_file.dart b/lib/models/ente_file.dart index 4758bb4eb..5025bc4d6 100644 --- a/lib/models/ente_file.dart +++ b/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(); } diff --git a/lib/models/file.dart b/lib/models/file.dart index 7c693229a..230cb3171 100644 --- a/lib/models/file.dart +++ b/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 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 getAsset() { + Future get getAsset { if (localID == null) { return Future.value(null); } - return AssetEntity.fromId(localID); + return AssetEntity.fromId(localID!); } void applyMetadata(Map metadata) { @@ -152,7 +157,7 @@ class File extends EnteFile { Future> 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 getMetadata() { + Map get metadata { final metadata = {}; - 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(); } } diff --git a/lib/models/file_load_result.dart b/lib/models/file_load_result.dart index 64b9cb916..5c0d55486 100644 --- a/lib/models/file_load_result.dart +++ b/lib/models/file_load_result.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'package:photos/models/file.dart'; class FileLoadResult { diff --git a/lib/models/file_type.dart b/lib/models/file_type.dart index e4d592bda..98079ca8f 100644 --- a/lib/models/file_type.dart +++ b/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(); + } +} diff --git a/lib/models/filters/gallery_items_filter.dart b/lib/models/filters/gallery_items_filter.dart index 1a8bd7e5b..11b992a8c 100644 --- a/lib/models/filters/gallery_items_filter.dart +++ b/lib/models/filters/gallery_items_filter.dart @@ -1,5 +1,4 @@ // @dart=2.9 - import 'package:photos/models/file.dart'; class GalleryItemsFilter { diff --git a/lib/models/gallery_type.dart b/lib/models/gallery_type.dart index 0d5dbfbfc..1b90656c8 100644 --- a/lib/models/gallery_type.dart +++ b/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, diff --git a/lib/models/ignored_file.dart b/lib/models/ignored_file.dart index 4437d0953..7116e433f 100644 --- a/lib/models/ignored_file.dart +++ b/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}'; - } } diff --git a/lib/models/key_attributes.dart b/lib/models/key_attributes.dart index 3acc51396..8b8ec3990 100644 --- a/lib/models/key_attributes.dart +++ b/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, diff --git a/lib/models/key_gen_result.dart b/lib/models/key_gen_result.dart index c074f0447..1f8b9851e 100644 --- a/lib/models/key_gen_result.dart +++ b/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'; diff --git a/lib/models/location.dart b/lib/models/location.dart index 5b87828fc..e81964322 100644 --- a/lib/models/location.dart +++ b/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); diff --git a/lib/models/magic_metadata.dart b/lib/models/magic_metadata.dart index fedc1a843..9edab547e 100644 --- a/lib/models/magic_metadata.dart +++ b/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 toJson() { - final map = {}; - map[kMagicKeyVisibility] = visibility; - return map; - } - - factory MagicMetadata.fromMap(Map map) { + static fromMap(Map? 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 toJson() { - final map = {}; - map[kPubMagicKeyEditedTime] = editedTime; - map[kPubMagicKeyEditedName] = editedName; - return map; - } - - factory PubMagicMetadata.fromMap(Map map) { + static fromMap(Map? 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 toJson() { - final map = {}; - map[kMagicKeyVisibility] = visibility; - return map; - } - - factory CollectionMagicMetadata.fromMap(Map map) { + static fromMap(Map? map) { if (map == null) return null; return CollectionMagicMetadata( - visibility: map[kMagicKeyVisibility] ?? kVisibilityVisible, + visibility: map[magicKeyVisibility] ?? visibilityVisible, ); } } diff --git a/lib/models/memory.dart b/lib/models/memory.dart index 3a8bc97f7..21e503ab9 100644 --- a/lib/models/memory.dart +++ b/lib/models/memory.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'package:photos/models/file.dart'; class Memory { diff --git a/lib/models/private_key_attributes.dart b/lib/models/private_key_attributes.dart index b770ada62..c92f017fc 100644 --- a/lib/models/private_key_attributes.dart +++ b/lib/models/private_key_attributes.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - class PrivateKeyAttributes { final String key; final String recoveryKey; diff --git a/lib/models/public_key.dart b/lib/models/public_key.dart index 9908bbcf3..0d14a4a55 100644 --- a/lib/models/public_key.dart +++ b/lib/models/public_key.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - class PublicKey { final String email; final String publicKey; diff --git a/lib/models/search/album_search_result.dart b/lib/models/search/album_search_result.dart index 48bf64251..89b22bfb4 100644 --- a/lib/models/search/album_search_result.dart +++ b/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 resultFiles() { + // for album search result, we should open the album page directly + throw UnimplementedError(); + } } diff --git a/lib/models/search/file_search_result.dart b/lib/models/search/file_search_result.dart index ab276535a..076e9eacf 100644 --- a/lib/models/search/file_search_result.dart +++ b/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 resultFiles() { + // for fileSearchResult, the file detailed page view will be opened + throw UnimplementedError(); + } } diff --git a/lib/models/search/generic_search_result.dart b/lib/models/search/generic_search_result.dart new file mode 100644 index 000000000..4710633b6 --- /dev/null +++ b/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 _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 resultFiles() { + return _files; + } +} diff --git a/lib/models/search/holiday_search_result.dart b/lib/models/search/holiday_search_result.dart deleted file mode 100644 index c10c8e09f..000000000 --- a/lib/models/search/holiday_search_result.dart +++ /dev/null @@ -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 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); -} diff --git a/lib/models/search/location_api_response.dart b/lib/models/search/location_api_response.dart index 12c1dfdf6..e8be37497 100644 --- a/lib/models/search/location_api_response.dart +++ b/lib/models/search/location_api_response.dart @@ -1,26 +1,27 @@ -// @dart=2.9 - class LocationApiResponse { final List results; LocationApiResponse({ - this.results, + required this.results, }); LocationApiResponse copyWith({ - List results, + required List results, }) { return LocationApiResponse( - results: results ?? this.results, + results: results, ); } factory LocationApiResponse.fromMap(Map map) { return LocationApiResponse( - results: List.from( - (map['results']).map( - (x) => LocationDataFromResponse.fromMap(x as Map), - ), - ), + results: (map['results']) == null + ? [] + : List.from( + (map['results']).map( + (x) => + LocationDataFromResponse.fromMap(x as Map), + ), + ), ); } } @@ -29,20 +30,10 @@ class LocationDataFromResponse { final String place; final List bbox; LocationDataFromResponse({ - this.place, - this.bbox, + required this.place, + required this.bbox, }); - LocationDataFromResponse copyWith({ - String place, - List bbox, - }) { - return LocationDataFromResponse( - place: place ?? this.place, - bbox: bbox ?? this.bbox, - ); - } - factory LocationDataFromResponse.fromMap(Map map) { return LocationDataFromResponse( place: map['place'] as String, diff --git a/lib/models/search/location_search_result.dart b/lib/models/search/location_search_result.dart deleted file mode 100644 index b5ae689f1..000000000 --- a/lib/models/search/location_search_result.dart +++ /dev/null @@ -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 files; - - LocationSearchResult(this.location, this.files); -} diff --git a/lib/models/search/month_search_result.dart b/lib/models/search/month_search_result.dart deleted file mode 100644 index 965a20c8a..000000000 --- a/lib/models/search/month_search_result.dart +++ /dev/null @@ -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 files; - MonthSearchResult(this.month, this.files); -} - -class MonthData { - final String name; - final int monthNumber; - MonthData(this.name, this.monthNumber); -} diff --git a/lib/models/search/search_result.dart b/lib/models/search/search_result.dart new file mode 100644 index 000000000..c03473957 --- /dev/null +++ b/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 resultFiles(); +} + +enum ResultType { + collection, + file, + location, + month, + year, + fileType, + fileExtension, + event +} diff --git a/lib/models/search/search_results.dart b/lib/models/search/search_results.dart deleted file mode 100644 index 83b7f0a40..000000000 --- a/lib/models/search/search_results.dart +++ /dev/null @@ -1,3 +0,0 @@ - - -class SearchResult {} diff --git a/lib/models/search/year_search_result.dart b/lib/models/search/year_search_result.dart deleted file mode 100644 index 9bfcbaa43..000000000 --- a/lib/models/search/year_search_result.dart +++ /dev/null @@ -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 files; - - YearSearchResult(this.year, this.files); -} diff --git a/lib/models/selected_files.dart b/lib/models/selected_files.dart index ded8d8365..7b79a538f 100644 --- a/lib/models/selected_files.dart +++ b/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; } diff --git a/lib/models/sessions.dart b/lib/models/sessions.dart index f67c64af2..56ba8a31a 100644 --- a/lib/models/sessions.dart +++ b/lib/models/sessions.dart @@ -1,9 +1,3 @@ -// @dart=2.9 - -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; - class Sessions { final List sessions; @@ -11,43 +5,14 @@ class Sessions { this.sessions, ); - Sessions copyWith({ - List sessions, - }) { - return Sessions( - sessions ?? this.sessions, - ); - } - - Map toMap() { - return { - 'sessions': sessions?.map((x) => x.toMap())?.toList(), - }; - } - factory Sessions.fromMap(Map map) { + if (map["sessions"] == null) { + throw Exception('\'map["sessions"]\' must not be null'); + } return Sessions( List.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 toMap() { - return { - 'token': token, - 'creationTime': creationTime, - 'ip': ip, - 'ua': ua, - 'prettyUA': prettyUA, - 'lastUsedTime': lastUsedTime, - }; - } - factory Session.fromMap(Map 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; - } } diff --git a/lib/models/set_keys_request.dart b/lib/models/set_keys_request.dart index 520245b28..e782319f5 100644 --- a/lib/models/set_keys_request.dart +++ b/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 toMap() { @@ -26,19 +22,4 @@ class SetKeysRequest { 'opsLimit': opsLimit, }; } - - factory SetKeysRequest.fromMap(Map 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)); } diff --git a/lib/models/set_recovery_key_request.dart b/lib/models/set_recovery_key_request.dart index 9fa91567a..1a03e4e36 100644 --- a/lib/models/set_recovery_key_request.dart +++ b/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 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)); } diff --git a/lib/models/subscription.dart b/lib/models/subscription.dart index 7ca545f23..3804f9a2e 100644 --- a/lib/models/subscription.dart +++ b/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 toMap() { - final map = { - '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 map) { + static fromMap(Map? 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 toJson() { - final map = {}; - map["isCancelled"] = isCancelled; - map["customerID"] = customerID; - return map; - } - - @override - String toString() { - return 'Attributes{isCancelled: $isCancelled, customerID: $customerID}'; - } } diff --git a/lib/models/trash_file.dart b/lib/models/trash_file.dart index 4e0b89414..6dcc89d07 100644 --- a/lib/models/trash_file.dart +++ b/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; } diff --git a/lib/models/trash_item_request.dart b/lib/models/trash_item_request.dart index 8a522dd82..b4169f738 100644 --- a/lib/models/trash_item_request.dart +++ b/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 json) { return TrashRequest(json['fileID'], json['collectionID']); diff --git a/lib/models/upload_strategy.dart b/lib/models/upload_strategy.dart new file mode 100644 index 000000000..6fcf43baf --- /dev/null +++ b/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; + } +} diff --git a/lib/models/upload_url.dart b/lib/models/upload_url.dart index ba6714b7f..49aa3b6ad 100644 --- a/lib/models/upload_url.dart +++ b/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 map) { - if (map == null) return null; - return UploadURL( map['url'], map['objectKey'], diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index c662e3f7d..10c7129bf 100644 --- a/lib/models/user_details.dart +++ b/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 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 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 toMap() { - return {'email': email, 'usage': usage, 'id': id, 'isAdmin': isAdmin}; - } } class FamilyData { - final List members; + final List? 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 map) { - if (map == null) { - return null; - } + static fromMap(Map? map) { + if (map == null) return null; assert(map['members'] != null && map['members'].length >= 0); final members = List.from( map['members'].map((x) => FamilyMember.fromMap(x)), @@ -129,12 +106,4 @@ class FamilyData { map['expiryTime'] as int, ); } - - Map toMap() { - return { - 'members': members.map((x) => x?.toMap())?.toList(), - 'storage': storage, - 'expiryTime': expiryTime - }; - } } diff --git a/lib/services/app_lifecycle_service.dart b/lib/services/app_lifecycle_service.dart index adc20cb66..55c6bf959 100644 --- a/lib/services/app_lifecycle_service.dart +++ b/lib/services/app_lifecycle_service.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'package:logging/logging.dart'; class AppLifecycleService { diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index d3516e301..5c5e7f2bb 100644 --- a/lib/services/collections_service.dart +++ b/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> _cachedLatestFiles; final _dio = Network.instance.getDio(); - final _localCollections = {}; + final _localPathToCollectionID = {}; final _collectionIDToCollections = {}; final _cachedKeys = {}; @@ -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 getActiveCollections() { return _collectionIDToCollections.values @@ -241,6 +240,40 @@ class CollectionsService { RemoteSyncService.instance.sync(silently: true); } + Future trashCollection(Collection collection) async { + try { + final deviceCollections = await _filesDB.getDeviceCollections(); + final Map 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 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; diff --git a/lib/services/feature_flag_service.dart b/lib/services/feature_flag_service.dart index 1e6668296..502c272e2 100644 --- a/lib/services/feature_flag_service.dart +++ b/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 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 toMap() { return { "disableCFWorker": disableCFWorker, - "disableUrlSharing": disableUrlSharing, "enableStripe": enableStripe, - "enableMissingLocationMigration": enableMissingLocationMigration, - "enableSearch": enableSearch, }; } @@ -157,17 +112,7 @@ class FeatureFlags { factory FeatureFlags.fromMap(Map 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(); - } } diff --git a/lib/services/file_magic_service.dart b/lib/services/file_magic_service.dart index bd2a63b1c..07d3f230b 100644 --- a/lib/services/file_magic_service.dart +++ b/lib/services/file_magic_service.dart @@ -32,9 +32,9 @@ class FileMagicService { FileMagicService._privateConstructor(); Future changeVisibility(List files, int visibility) async { - final Map update = {kMagicKeyVisibility: visibility}; + final Map 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 jsonToUpdate = jsonDecode(file.pubMmdEncodedJson); + final Map 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 jsonToUpdate = jsonDecode(file.mMdEncodedJson); + final Map jsonToUpdate = + jsonDecode(file.mMdEncodedJson); newMetadataUpdate.forEach((key, value) { jsonToUpdate[key] = value; }); diff --git a/lib/services/files_service.dart b/lib/services/files_service.dart new file mode 100644 index 000000000..16718b336 --- /dev/null +++ b/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 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; + } + } +} diff --git a/lib/services/ignored_files_service.dart b/lib/services/ignored_files_service.dart index cf6a1f838..3d2f5f787 100644 --- a/lib/services/ignored_files_service.dart +++ b/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 removeIgnoredMappings(List files) async { + final List ignoredFiles = []; + final Set idsToRemoveFromCache = {}; + final Set 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 reset() async { + await _db.clearTable(); + _ignoredIDs = null; + } + Future> _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. diff --git a/lib/services/local/local_sync_util.dart b/lib/services/local/local_sync_util.dart new file mode 100644 index 000000000..96970adaf --- /dev/null +++ b/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, List>> getLocalPathAssetsAndFiles( + int fromTime, + int toTime, + Computer computer, +) async { + final pathEntities = await _getGalleryList( + updateFromTime: fromTime, + updateToTime: toTime, + ); + final List 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 alreadySeenLocalIDs = {}; + final List uniqueFiles = []; + for (AssetPathEntity pathEntity in pathEntities) { + final List assetsInPath = await _getAllAssetLists(pathEntity); + final Tuple2, List> result = await computer.compute( + _getLocalIDsAndFilesFromAssets, + param: { + "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>> + getDeviceFolderWithCountAndCoverID() async { + final List> 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> 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 localPathAssets = []; + for (final assetPath in assetPaths) { + final Set localIDs = {}; + 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 getDiffWithLocal( + List assets, + // current set of assets available on device + Set existingIDs, // localIDs of files already imported in app + Map> pathToLocalIDs, + Set invalidIDs, + Computer computer, +) async { + final Map args = {}; + 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 args) { + final List onDeviceLocalPathAsset = args['assets']; + final Set existingIDs = args['existingIDs']; + final Set invalidIDs = args['invalidIDs']; + final Map> pathToLocalIDs = args['pathToLocalIDs']; + final Map> newPathToLocalIDs = >{}; + final Map> removedPathToLocalIDs = + >{}; + final List 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 candidateLocalIDsForRemoval = + pathToLocalIDs[pathID] ?? {}; + final Set missingLocalIDsInPath = {}; + 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> _convertLocalAssetsToUniqueFiles( + List assets, +) async { + final Set alreadySeenLocalIDs = {}; + final List 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> _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> _getAllAssetLists(AssetPathEntity pathEntity) async { + final List result = []; + int currentPage = 0; + List 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, List>> _getLocalIDsAndFilesFromAssets( + Map args, +) async { + final pathEntity = args["pathEntity"] as AssetPathEntity; + final assetList = args["assetList"]; + final fromTime = args["fromTime"]; + final alreadySeenLocalIDs = args["alreadySeenLocalIDs"] as Set; + final List files = []; + final Set 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 localIDs; + final String pathID; + final String pathName; + + LocalPathAsset({ + @required this.localIDs, + @required this.pathName, + @required this.pathID, + }); +} + +class LocalDiffResult { + // unique localPath Assets. + final List localPathAssets; + + // set of File object created from localPathAssets + List uniqueLocalFiles; + + // newPathToLocalIDs represents new entries which needs to be synced to + // the local db + final Map> newPathToLocalIDs; + + final Map> deletePathToLocalIDs; + + LocalDiffResult({ + this.uniqueLocalFiles, + this.localPathAssets, + this.newPathToLocalIDs, + this.deletePathToLocalIDs, + }); +} diff --git a/lib/services/local_file_update_service.dart b/lib/services/local_file_update_service.dart index 636870a72..25af68197 100644 --- a/lib/services/local_file_update_service.dart +++ b/lib/services/local_file_update_service.dart @@ -99,9 +99,9 @@ class LocalFileUpdateService { List localIDsToProcess, ) async { _logger.info("files to process ${localIDsToProcess.length} for reupload"); - List localFiles = - (await FilesDB.instance.getLocalFiles(localIDsToProcess)); - Set processedIDs = {}; + final List localFiles = + await FilesDB.instance.getLocalFiles(localIDsToProcess); + final Set 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 localIDsToProcess, ) async { _logger.info("files to process ${localIDsToProcess.length}"); - var localIDsWithLocation = []; + final localIDsWithLocation = []; 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, diff --git a/lib/services/local_sync_service.dart b/lib/services/local_sync_service.dart index 7401c0be6..97f77949b 100644 --- a/lib/services/local_sync_service.dart +++ b/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 syncAll() async { - final sTime = DateTime.now().microsecondsSinceEpoch; - 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", + Future _refreshDeviceFolderCountAndCover({ + bool isFirstSync = false, + }) async { + final List> result = + await getDeviceFolderWithCountAndCoverID(); + final bool hasUpdated = await _db.updateDeviceCoverWithCount( + result, + shouldBackup: Configuration.instance.hasSelectedAllFoldersForBackup(), ); - final existingIDs = await _db.getExistingLocalFileIDs(); - final invalidIDs = getInvalidFileIDs().toSet(); - final unsyncedFiles = - await getUnsyncedFiles(localAssets, existingIDs, invalidIDs, _computer); - if (unsyncedFiles.isNotEmpty) { - await _db.insertMultiple(unsyncedFiles); - _logger.info( - "Inserted " + unsyncedFiles.length.toString() + " unsynced files.", - ); - _updatePathsToBackup(unsyncedFiles); - Bus.instance.fire(LocalPhotosUpdatedEvent(unsyncedFiles)); - return true; + // 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()); } - return false; + // 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 _migrateOldSettings( + List> 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 syncAll() async { + final stopwatch = Stopwatch()..start(); + final localAssets = await getAllLocalAssets(); + _logger.info( + "Loading allLocalAssets ${localAssets.length} took ${stopwatch.elapsedMilliseconds}ms ", + ); + await _refreshDeviceFolderCountAndCover(); + _logger.info( + "refreshDeviceFolderCountAndCover + allLocalAssets took ${stopwatch.elapsedMilliseconds}ms ", + ); + final existingLocalFileIDs = await _db.getExistingLocalFileIDs(); + final Map> 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 ${localDiffResult.uniqueLocalFiles.length} " + "un-synced files", + ); + } + debugPrint( + "syncAll: mappingChange : $hasAnyMappingChanged, " + "unSyncedFiles: $hasUnsyncedFiles", + ); + if (hasAnyMappingChanged || hasUnsyncedFiles) { + Bus.instance.fire( + LocalPhotosUpdatedEvent(localDiffResult.uniqueLocalFiles), + ); + } + _logger.info("syncAll took ${stopwatch.elapsed.inMilliseconds}ms "); + return hasUnsyncedFiles; } Future trackEditedFile(File file) async { - final editedIDs = getEditedFileIDs(); + final editedIDs = _getEditedFileIDs(); editedIDs.add(file.localID); await _prefs.setStringList(kEditedFileIDsKey, editedIDs); } - List getEditedFileIDs() { + List _getEditedFileIDs() { if (_prefs.containsKey(kEditedFileIDsKey)) { return _prefs.getStringList(kEditedFileIDsKey); } else { @@ -172,32 +255,30 @@ class LocalSyncService { } Future trackDownloadedFile(String localID) async { - final downloadedIDs = getDownloadedFileIDs(); + final downloadedIDs = _getDownloadedFileIDs(); downloadedIDs.add(localID); await _prefs.setStringList(kDownloadedFileIDsKey, downloadedIDs); } - List getDownloadedFileIDs() { + List _getDownloadedFileIDs() { if (_prefs.containsKey(kDownloadedFileIDsKey)) { return _prefs.getStringList(kDownloadedFileIDsKey); } else { - final List downloadedIDs = []; - return downloadedIDs; + return []; } } Future trackInvalidFile(File file) async { - final invalidIDs = getInvalidFileIDs(); + final invalidIDs = _getInvalidFileIDs(); invalidIDs.add(file.localID); await _prefs.setStringList(kInvalidFileIDsKey, invalidIDs); } - List getInvalidFileIDs() { + List _getInvalidFileIDs() { if (_prefs.containsKey(kInvalidFileIDsKey)) { return _prefs.getStringList(kInvalidFileIDsKey); } else { - final List invalidIDs = []; - return invalidIDs; + return []; } } @@ -213,6 +294,11 @@ class LocalSyncService { Future 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 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 _loadAndStorePhotos( int fromTime, int toTime, @@ -233,21 +337,50 @@ class LocalSyncService { " to " + DateTime.fromMicrosecondsSinceEpoch(toTime).toString(), ); - final files = await getDeviceFiles(fromTime, toTime, _computer); + final Tuple2, List> result = + await getLocalPathAssetsAndFiles(fromTime, toTime, _computer); + await FilesDB.instance.insertLocalAssets( + result.item1, + shouldAutoBackup: Configuration.instance.hasSelectedAllFoldersForBackup(), + ); + final List 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.", - ); - } + await _trackUpdatedFiles( + files, + existingLocalFileIDs, + editedFileIDs, + downloadedFileIDs, + ); + final List allFiles = []; + allFiles.addAll(files); + files.removeWhere((file) => existingLocalFileIDs.contains(file.localID)); + await _db.insertMultiple( + files, + conflictAlgorithm: ConflictAlgorithm.ignore, + ); + _logger.info("Inserted " + files.length.toString() + " files."); + Bus.instance.fire(LocalPhotosUpdatedEvent(allFiles)); + } + await _prefs.setInt(kDbUpdationTimeKey, toTime); + } + Future _trackUpdatedFiles( + List files, + Set existingLocalFileIDs, + Set editedFileIDs, + Set 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 updatedLocalIDs = []; for (final file in updatedFiles) { updatedLocalIDs.add(file.localID); @@ -256,23 +389,6 @@ class LocalSyncService { updatedLocalIDs, FileUpdationDB.modificationTimeUpdated, ); - final List allFiles = []; - allFiles.addAll(files); - files.removeWhere((file) => existingLocalFileIDs.contains(file.localID)); - await _db.insertMultiple(files); - _logger.info("Inserted " + files.length.toString() + " files."); - _updatePathsToBackup(files); - Bus.instance.fire(LocalPhotosUpdatedEvent(allFiles)); - } - await _prefs.setInt(kDbUpdationTimeKey, toTime); - } - - void _updatePathsToBackup(List 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); } } @@ -287,7 +403,7 @@ class LocalSyncService { if (hasGrantedLimitedPermissions()) { syncAll(); } else { - sync(); + sync().then((value) => _refreshDeviceFolderCountAndCover()); } }); PhotoManager.startChangeNotify(); diff --git a/lib/services/memories_service.dart b/lib/services/memories_service.dart index f678aec4d..8e07974c5 100644 --- a/lib/services/memories_service.dart +++ b/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), ); }); } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 1ff160970..5ad7cd5bb 100644 --- a/lib/services/notification_service.dart +++ b/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 showNotification(String title, String message) async { if (!Platform.isAndroid) { diff --git a/lib/services/push_service.dart b/lib/services/push_service.dart index d3e701552..56bdaa5b8 100644 --- a/lib/services/push_service.dart +++ b/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"; diff --git a/lib/services/remote_sync_service.dart b/lib/services/remote_sync_service.dart index 7cc4ef733..6bc5f20b1 100644 --- a/lib/services/remote_sync_service.dart +++ b/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 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> _getFilesToBeUploaded() async { - final foldersToBackUp = Configuration.instance.getPathsToBackUp(); - List filesToBeUploaded; - if (LocalSyncService.instance.hasGrantedLimitedPermissions() && - foldersToBackUp.isEmpty) { - filesToBeUploaded = await _db.getAllLocalFiles(); - } else { - filesToBeUploaded = - await _db.getFilesToBeUploadedWithinFolders(foldersToBackUp); + Future 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> pathIdToLocalIDs = + await _db.getDevicePathIDToLocalIDMap(); + for (final deviceCollection in deviceCollections) { + _logger.fine("processing ${deviceCollection.name}"); + final Set localIDsToSync = + pathIdToLocalIDs[deviceCollection.id] ?? {}; + if (deviceCollection.uploadStrategy == UploadStrategy.ifMissing) { + final Set 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 existingMapping = + await _db.getLocalFileIDsForCollection(deviceCollection.collectionID); + final Set 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 newFilesToInsert = []; + final Set 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}", + ); + } + } } - if (!Configuration.instance.shouldBackupVideos() || _shouldThrottleSync()) { + } + + Future updateDeviceFolderSyncStatus( + Map syncStatusUpdate, + ) async { + final Set oldCollectionIDsForAutoSync = + await _db.getDeviceSyncCollectionIDs(); + await _db.updateDevicePathSyncStatus(syncStatusUpdate); + final Set 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([])); + Bus.instance.fire(BackupFoldersUpdatedEvent()); + } + + Future removeFilesQueuedForUpload(List 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 pendingUploads = + await _db.getPendingUploadForCollection(collectionID); + if (pendingUploads.isEmpty) { + continue; + } + final Set localIDsInOtherFileEntries = + await _db.getLocalIDsPresentInEntries( + pendingUploads, + collectionID, + ); + final List entriesToUpdate = []; + final List 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); + } + } + + Future _createCollectionsForDevicePath( + List 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> _getFilesToBeUploaded() async { + final deviceCollections = await _db.getDeviceCollections(); + deviceCollections.removeWhere((element) => !element.shouldBackup); + final List 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 _uploadFiles(List 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 _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()); } } diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 961ea0045..df8bec920 100644 --- a/lib/services/search_service.dart +++ b/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> _cachedFilesFuture; @@ -32,6 +32,7 @@ class SearchService { static const _maximumResultsLimit = 20; SearchService._privateConstructor(); + static final SearchService instance = SearchService._privateConstructor(); Future 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().listen((event) { _cachedFilesFuture = null; - getAllFiles(); + _getAllFiles(); }); } - Future> getAllFiles() async { + Future> _getAllFiles() async { if (_cachedFilesFuture != null) { return _cachedFilesFuture; } + _logger.fine("Reading all files from db"); _cachedFilesFuture = FilesDB.instance.getAllFilesFromDB(); return _cachedFilesFuture; } Future> getFileSearchResults(String query) async { final List fileSearchResults = []; - final List files = await getAllFiles(); - final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false); + final List 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> getLocationSearchResults( + Future> getLocationSearchResults( String query, ) async { - final List locationSearchResults = []; + final List searchResults = []; try { - final List allFiles = await SearchService.instance.getAllFiles(); + final List 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> 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 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> getYearSearchResults( + Future> getYearSearchResults( String yearFromQuery, ) async { - final List yearSearchResults = []; + final List searchResults = []; for (var yearData in YearsData.instance.yearsData) { if (yearData.year.startsWith(yearFromQuery)) { final List 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> getHolidaySearchResults( + Future> getHolidaySearchResults( String query, ) async { - final List holidaySearchResults = []; - - final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false); + final List 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> getMonthSearchResults(String query) async { - final List monthSearchResults = []; - final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false); - - for (var month in allMonths) { - if (month.name.startsWith(nonCaseSensitiveRegexForQuery)) { + Future> getFileTypeResults( + String query, + ) async { + final List searchResults = []; + final List 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; + } - return monthSearchResults; + Future> getFileExtensionResults( + String query, + ) async { + final List searchResults = []; + if (!query.startsWith(".")) { + return searchResults; + } + + final List allFiles = await _getAllFiles(); + final Map> resultMap = >{}; + + 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] = []; + } + resultMap[exnType].add(eachFile); + } + } + for (MapEntry> entry in resultMap.entries) { + searchResults.add( + GenericSearchResult( + ResultType.fileExtension, + entry.key.toUpperCase(), + entry.value, + ), + ); + } + return searchResults; + } + + Future> getMonthSearchResults(String query) async { + final List 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> getDateResults( + String query, + ) async { + final List 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 _getMatchingMonths(String query) { + return allMonths + .where( + (monthData) => + monthData.name.toLowerCase().startsWith(query.toLowerCase()), + ) + .toList(); } Future> _getFilesInYear(List durationOfYear) async { @@ -228,20 +321,28 @@ class SearchService { ); } - List> _getDurationsOfHolidayInEveryYear(int day, int month) { + List> _getDurationsForCalendarDateInEveryYear( + int day, + int month, { + int year, + }) { final List> 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> _getDurationsOfMonthInEveryYear(int month) { final List> 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> _getPossibleEventDate(String query) { + final List> 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 potentialMonth = + resultCount > 1 ? _getMatchingMonths(result[1]) : allMonths; + final int parsedYear = resultCount >= 3 ? int.tryParse(result[2]) : null; + final List 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; + } } diff --git a/lib/services/sync_service.dart b/lib/services/sync_service.dart index aa99b1d74..b38305c1b 100644 --- a/lib/services/sync_service.dart +++ b/lib/services/sync_service.dart @@ -179,6 +179,15 @@ class SyncService { ); } + void onDeviceCollectionSet(Set 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", diff --git a/lib/services/trash_sync_service.dart b/lib/services/trash_sync_service.dart index a96131057..fdac9b5b6 100644 --- a/lib/services/trash_sync_service.dart +++ b/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) { diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index 0917b6204..973bc545b 100644 --- a/lib/services/update_service.dart +++ b/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) { diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart new file mode 100644 index 000000000..a0c6f3e20 --- /dev/null +++ b/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); diff --git a/lib/theme/effects.dart b/lib/theme/effects.dart new file mode 100644 index 000000000..e13558517 --- /dev/null +++ b/lib/theme/effects.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +const blurBase = 96; +const blurMuted = 48; +const blurFaint = 24; + +List shadowFloatLight = const [ + BoxShadow(blurRadius: 10, color: Color.fromRGBO(0, 0, 0, 0.25)), +]; + +List 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 shadowButtonLight = const [ + BoxShadow( + blurRadius: 4, + color: Color.fromRGBO(0, 0, 0, 0.25), + offset: Offset(0, 4), + ), +]; + +List shadowFloatDark = const [ + BoxShadow( + blurRadius: 12, + color: Color.fromRGBO(0, 0, 0, 0.75), + offset: Offset(0, 2), + ), +]; + +List 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 shadowButtonDark = const [ + BoxShadow( + blurRadius: 4, + color: Color.fromRGBO(0, 0, 0, 0.75), + offset: Offset(0, 4), + ), +]; diff --git a/lib/theme/ente_theme.dart b/lib/theme/ente_theme.dart new file mode 100644 index 000000000..62406b0f7 --- /dev/null +++ b/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 shadowFloat; + final List shadowMenu; + final List 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, +); diff --git a/lib/theme/text_style.dart b/lib/theme/text_style.dart new file mode 100644 index 000000000..8b36c73ed --- /dev/null +++ b/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), + ); +} diff --git a/lib/ui/account/delete_account_page.dart b/lib/ui/account/delete_account_page.dart index 50f7452d8..b454aca9a 100644 --- a/lib/ui/account/delete_account_page.dart +++ b/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( diff --git a/lib/ui/account/recovery_key_page.dart b/lib/ui/account/recovery_key_page.dart index 9314c86b2..e5b561a24 100644 --- a/lib/ui/account/recovery_key_page.dart +++ b/lib/ui/account/recovery_key_page.dart @@ -51,9 +51,9 @@ class _RecoveryKeyPageState extends State { @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 diff --git a/lib/ui/account/verify_recovery_page.dart b/lib/ui/account/verify_recovery_page.dart index 2e94fb873..ebe059b50 100644 --- a/lib/ui/account/verify_recovery_page.dart +++ b/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 { @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 { 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 { 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 { GradientButton( onTap: _verifyRecoveryKey, text: "Verify", - paddingValue: 6, iconData: Icons.shield_outlined, ), const SizedBox(height: 8), diff --git a/lib/ui/backup_folder_selection_page.dart b/lib/ui/backup_folder_selection_page.dart index 031c020a2..13685a057 100644 --- a/lib/ui/backup_folder_selection_page.dart +++ b/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 { - final Set _allFolders = {}; - Set _selectedFolders = {}; - List _latestFiles; - Map _itemCount; + final Logger _logger = Logger((_BackupFolderSelectionPageState).toString()); + final Set _allDevicePathIDs = {}; + final Set _selectedDevicePathIDs = {}; + List _deviceCollections; + Map _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 { const Padding( padding: EdgeInsets.all(10), ), - _latestFiles == null + _deviceCollections == null ? Container() : GestureDetector( behavior: HitTestBehavior.translucent, @@ -109,7 +117,8 @@ class _BackupFolderSelectionPageState extends State { 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 { ), ), 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 { bottom: Platform.isIOS ? 60 : 32, ), child: OutlinedButton( - onPressed: _selectedFolders.isEmpty + onPressed: _selectedDevicePathIDs.isEmpty ? null : () async { + final Map 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 { } Widget _getFolders() { - if (_latestFiles == null) { + if (_deviceCollections == null) { return const EnteLoadingWidget(); } _sortFiles(); @@ -214,14 +236,13 @@ class _BackupFolderSelectionPageState extends State { thumbVisibility: true, child: Padding( padding: const EdgeInsets.only(right: 4), - child: ImplicitlyAnimatedReorderableList( + child: ImplicitlyAnimatedReorderableList( 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 { ); } - 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 { 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 { 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 { ), 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 { ), ], ), - _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 { } 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 { ThumbnailWidget( file, shouldShowSyncStatus: false, - key: Key("backup_selection_widget" + file.tag()), + key: Key("backup_selection_widget" + file.tag), ), Padding( padding: const EdgeInsets.all(9), diff --git a/lib/ui/collections/collection_item_widget.dart b/lib/ui/collections/collection_item_widget.dart index 41f74e015..7c93e5b3a 100644 --- a/lib/ui/collections/collection_item_widget.dart +++ b/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, ), ), ), diff --git a/lib/ui/collections/device_folder_icon_widget.dart b/lib/ui/collections/device_folder_icon_widget.dart index 153424bce..a5fbf9d8e 100644 --- a/lib/ui/collections/device_folder_icon_widget.dart +++ b/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)); }, ); } diff --git a/lib/ui/collections/device_folders_grid_view_widget.dart b/lib/ui/collections/device_folders_grid_view_widget.dart index efcf46976..5a3474cae 100644 --- a/lib/ui/collections/device_folders_grid_view_widget.dart +++ b/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'; +import 'package:photos/ui/common/loading_widget.dart'; +import 'package:photos/ui/viewer/gallery/empty_state.dart'; -class DeviceFoldersGridViewWidget extends StatelessWidget { - final List folders; - - const DeviceFoldersGridViewWidget( - this.folders, { +class DeviceFoldersGridViewWidget extends StatefulWidget { + const DeviceFoldersGridViewWidget({ Key key, }) : super(key: key); + @override + State createState() => + _DeviceFoldersGridViewWidgetState(); +} + +class _DeviceFoldersGridViewWidgetState + extends State { + StreamSubscription _backupFoldersUpdatedEvent; + + @override + void initState() { + _backupFoldersUpdatedEvent = + Bus.instance.on().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>( + 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(); + } } diff --git a/lib/ui/collections/hidden_collections_button_widget.dart b/lib/ui/collections/hidden_collections_button_widget.dart index bb2e67ebe..a236db176 100644 --- a/lib/ui/collections/hidden_collections_button_widget.dart +++ b/lib/ui/collections/hidden_collections_button_widget.dart @@ -46,7 +46,7 @@ class HiddenCollectionsButtonWidget extends StatelessWidget { const Padding(padding: EdgeInsets.all(6)), FutureBuilder( future: FilesDB.instance.fileCountWithVisibility( - kVisibilityArchive, + visibilityArchive, Configuration.instance.getUserID(), ), builder: (context, snapshot) { diff --git a/lib/ui/collections_gallery_widget.dart b/lib/ui/collections_gallery_widget.dart index c1dea4fff..450253605 100644 --- a/lib/ui/collections_gallery_widget.dart +++ b/lib/ui/collections_gallery_widget.dart @@ -1,20 +1,17 @@ // @dart=2.9 import 'dart:async'; -import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.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/local_photos_updated_event.dart'; import 'package:photos/events/user_logged_out_event.dart'; +import 'package:photos/models/collection.dart'; import 'package:photos/models/collection_items.dart'; -import 'package:photos/models/device_folder.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/ui/collections/device_folders_grid_view_widget.dart'; import 'package:photos/ui/collections/ente_section_title.dart'; @@ -23,10 +20,8 @@ import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart'; import 'package:photos/ui/collections/section_title.dart'; import 'package:photos/ui/collections/trash_button_widget.dart'; import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/viewer/gallery/device_all_page.dart'; -import 'package:photos/ui/viewer/gallery/empte_state.dart'; +import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/utils/local_settings.dart'; -import 'package:photos/utils/navigation_util.dart'; class CollectionsGalleryWidget extends StatefulWidget { const CollectionsGalleryWidget({Key key}) : super(key: key); @@ -41,7 +36,6 @@ class _CollectionsGalleryWidgetState extends State final _logger = Logger("CollectionsGallery"); StreamSubscription _localFilesSubscription; StreamSubscription _collectionUpdatesSubscription; - StreamSubscription _backupFoldersUpdatedEvent; StreamSubscription _loggedOutEvent; AlbumSortKey sortKey; String _loadReason = "init"; @@ -62,11 +56,6 @@ class _CollectionsGalleryWidgetState extends State _loadReason = (UserLoggedOutEvent).toString(); setState(() {}); }); - _backupFoldersUpdatedEvent = - Bus.instance.on().listen((event) { - _loadReason = (BackupFoldersUpdatedEvent).toString(); - setState(() {}); - }); sortKey = LocalSettings.instance.albumSortKey(); super.initState(); } @@ -75,7 +64,7 @@ class _CollectionsGalleryWidgetState extends State Widget build(BuildContext context) { super.build(context); _logger.info("Building, trigger: $_loadReason"); - return FutureBuilder( + return FutureBuilder>( future: _getCollections(), builder: (context, snapshot) { if (snapshot.hasData) { @@ -89,20 +78,9 @@ class _CollectionsGalleryWidgetState extends State ); } - Future _getCollections() async { - final filesDB = FilesDB.instance; + Future> _getCollections() async { final collectionsService = CollectionsService.instance; final userID = Configuration.instance.getUserID(); - final List folders = []; - final latestLocalFiles = await filesDB.getLatestLocalFiles(); - for (final file in latestLocalFiles) { - folders.add(DeviceFolder(file.deviceFolder, file.deviceFolder, file)); - } - folders.sort( - (first, second) => - second.thumbnail.creationTime.compareTo(first.thumbnail.creationTime), - ); - final List collectionsWithThumbnail = []; final latestCollectionFiles = await collectionsService.getLatestCollectionFiles(); @@ -114,6 +92,13 @@ class _CollectionsGalleryWidgetState extends State } collectionsWithThumbnail.sort( (first, second) { + if (second.collection.type == CollectionType.favorites && + first.collection.type != CollectionType.favorites) { + return 1; + } else if (first.collection.type == CollectionType.favorites && + second.collection.type != CollectionType.favorites) { + return 0; + } if (sortKey == AlbumSortKey.albumName) { return compareAsciiLowerCaseNatural( first.collection.name, @@ -128,10 +113,12 @@ class _CollectionsGalleryWidgetState extends State } }, ); - return CollectionItems(folders, collectionsWithThumbnail); + return collectionsWithThumbnail; } - Widget _getCollectionsGalleryWidget(CollectionItems items) { + Widget _getCollectionsGalleryWidget( + List collections, + ) { final TextStyle trashAndHiddenTextStyle = Theme.of(context) .textTheme .subtitle1 @@ -145,29 +132,9 @@ class _CollectionsGalleryWidgetState extends State child: Column( children: [ const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const SectionTitle("On device"), - Platform.isAndroid - ? const SizedBox.shrink() - : GestureDetector( - child: const Padding( - padding: EdgeInsets.only(right: 12.0), - child: Text("View all"), - ), - onTap: () => routeToPage(context, DeviceAllPage()), - ), - ], - ), + const SectionTitle("On device"), const SizedBox(height: 12), - items.folders.isEmpty - ? const Padding( - padding: EdgeInsets.all(22), - child: EmptyState(), - ) - : DeviceFoldersGridViewWidget(items.folders), + const DeviceFoldersGridViewWidget(), const Padding(padding: EdgeInsets.all(4)), const Divider(), Row( @@ -180,7 +147,7 @@ class _CollectionsGalleryWidgetState extends State ), const SizedBox(height: 12), Configuration.instance.hasConfiguredAccount() - ? RemoteCollectionsGridViewWidget(items.collections) + ? RemoteCollectionsGridViewWidget(collections) : const EmptyState(), const SizedBox(height: 10), const Divider(), @@ -275,7 +242,6 @@ class _CollectionsGalleryWidgetState extends State _localFilesSubscription.cancel(); _collectionUpdatesSubscription.cancel(); _loggedOutEvent.cancel(); - _backupFoldersUpdatedEvent.cancel(); super.dispose(); } diff --git a/lib/ui/common/gradient_button.dart b/lib/ui/common/gradient_button.dart index 061b2ccae..f34c39c12 100644 --- a/lib/ui/common/gradient_button.dart +++ b/lib/ui/common/gradient_button.dart @@ -5,23 +5,24 @@ import 'package:flutter/material.dart'; class GradientButton extends StatelessWidget { final List linearGradientColors; final Function onTap; - final Widget child; + // text is ignored if child is specified final String text; + // nullable final IconData iconData; + // padding between the text and icon final double paddingValue; const GradientButton({ Key key, - this.child, this.linearGradientColors = const [ Color(0xFF2CD267), Color(0xFF1DB954), ], this.onTap, - this.text, + this.text = '', this.iconData, this.paddingValue = 0.0, }) : super(key: key); @@ -29,9 +30,7 @@ class GradientButton extends StatelessWidget { @override Widget build(BuildContext context) { Widget buttonContent; - if (child != null) { - buttonContent = child; - } else if (iconData == null) { + if (iconData == null) { buttonContent = Text( text, style: const TextStyle( @@ -48,9 +47,10 @@ class GradientButton extends StatelessWidget { children: [ Icon( iconData, + size: 20, color: Colors.white, ), - Padding(padding: EdgeInsets.all(paddingValue)), + const Padding(padding: EdgeInsets.symmetric(horizontal: 6)), Text( text, style: const TextStyle( diff --git a/lib/ui/common/loading_widget.dart b/lib/ui/common/loading_widget.dart index 80d940947..8c84dae5e 100644 --- a/lib/ui/common/loading_widget.dart +++ b/lib/ui/common/loading_widget.dart @@ -1,9 +1,7 @@ -// @dart=2.9 - import 'package:flutter/cupertino.dart'; class EnteLoadingWidget extends StatelessWidget { - const EnteLoadingWidget({Key key}) : super(key: key); + const EnteLoadingWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/ui/components/notification_warning_widget.dart b/lib/ui/components/notification_warning_widget.dart index 69e59db51..7763e233e 100644 --- a/lib/ui/components/notification_warning_widget.dart +++ b/lib/ui/components/notification_warning_widget.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:photos/ente_theme_data.dart'; +import 'package:photos/theme/colors.dart'; +import 'package:photos/theme/text_style.dart'; class NotificationWarningWidget extends StatelessWidget { final IconData warningIcon; @@ -21,14 +23,14 @@ class NotificationWarningWidget extends StatelessWidget { child: GestureDetector( onTap: onTap, child: Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all( Radius.circular(8), ), - boxShadow: Theme.of(context).colorScheme.shadowMenu, - color: Theme.of(context).colorScheme.warning500, + boxShadow: Theme.of(context).colorScheme.enteTheme.shadowMenu, + color: warning500, ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), @@ -39,18 +41,18 @@ class NotificationWarningWidget extends StatelessWidget { size: 36, color: Colors.white, ), - const SizedBox(width: 10), + const SizedBox(width: 12), Flexible( child: Text( text, - style: const TextStyle(height: 1.4, color: Colors.white), + style: darkTextTheme.bodyBold, textAlign: TextAlign.left, ), ), - const SizedBox(width: 10), + const SizedBox(width: 12), ClipOval( child: Material( - color: Theme.of(context).colorScheme.fillFaint, + color: fillFaintDark, child: InkWell( splashColor: Colors.red, // Splash color onTap: onTap, diff --git a/lib/ui/create_collection_page.dart b/lib/ui/create_collection_page.dart index 1e8f48031..76b39a9b2 100644 --- a/lib/ui/create_collection_page.dart +++ b/lib/ui/create_collection_page.dart @@ -11,6 +11,7 @@ import 'package:photos/models/collection_items.dart'; import 'package:photos/models/file.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/collections_service.dart'; +import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/services/remote_sync_service.dart'; import 'package:photos/ui/common/gradient_button.dart'; import 'package:photos/ui/common/loading_widget.dart'; @@ -94,7 +95,6 @@ class _CreateCollectionPageState extends State { _showNameAlbumDialog(); }, iconData: Icons.create_new_folder_outlined, - paddingValue: 6, text: "To a new album", ), ), @@ -156,7 +156,7 @@ class _CreateCollectionPageState extends State { child: SizedBox( height: 64, width: 64, - key: Key("collection_item:" + item.thumbnail.tag()), + key: Key("collection_item:" + item.thumbnail.tag), child: ThumbnailWidget(item.thumbnail), ), ), @@ -290,7 +290,7 @@ class _CreateCollectionPageState extends State { await dialog.show(); try { final int fromCollectionID = - widget.selectedFiles.files?.first?.collectionID; + widget.selectedFiles.files.first?.collectionID; await CollectionsService.instance.move( toCollectionID, fromCollectionID, @@ -360,6 +360,10 @@ class _CreateCollectionPageState extends State { } } if (filesPendingUpload.isNotEmpty) { + // filesPendingUpload might be getting ignored during auto-upload + // because the user deleted these files from ente in the past. + await IgnoredFilesService.instance + .removeIgnoredMappings(filesPendingUpload); await FilesDB.instance.insertMultiple(filesPendingUpload); } if (files.isNotEmpty) { diff --git a/lib/ui/home_widget.dart b/lib/ui/home_widget.dart index be6e412b5..fd0d3a954 100644 --- a/lib/ui/home_widget.dart +++ b/lib/ui/home_widget.dart @@ -297,7 +297,7 @@ class _HomeWidgetState extends State { } final bool showBackupFolderHook = - Configuration.instance.getPathsToBackUp().isEmpty && + !Configuration.instance.hasSelectedAnyBackupFolder() && !LocalSyncService.instance.hasGrantedLimitedPermissions() && CollectionsService.instance.getActiveCollections().isEmpty; return Stack( @@ -392,42 +392,32 @@ class _HomeWidgetState extends State { } final gallery = Gallery( asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async { - final importantPaths = Configuration.instance.getPathsToBackUp(); final ownerID = Configuration.instance.getUserID(); + final hasSelectedAllForBackup = + Configuration.instance.hasSelectedAllFoldersForBackup(); final archivedCollectionIds = CollectionsService.instance.getArchivedCollections(); FileLoadResult result; - if (importantPaths.isNotEmpty) { - result = await FilesDB.instance.getImportantFiles( + if (hasSelectedAllForBackup) { + result = await FilesDB.instance.getAllLocalAndUploadedFiles( creationStartTime, creationEndTime, ownerID, - importantPaths.toList(), limit: limit, asc: asc, ignoredCollectionIDs: archivedCollectionIds, ); } else { - if (LocalSyncService.instance.hasGrantedLimitedPermissions()) { - result = await FilesDB.instance.getAllLocalAndUploadedFiles( - creationStartTime, - creationEndTime, - ownerID, - limit: limit, - asc: asc, - ignoredCollectionIDs: archivedCollectionIds, - ); - } else { - result = await FilesDB.instance.getAllUploadedFiles( - creationStartTime, - creationEndTime, - ownerID, - limit: limit, - asc: asc, - ignoredCollectionIDs: archivedCollectionIds, - ); - } + result = await FilesDB.instance.getAllPendingOrUploadedFiles( + creationStartTime, + creationEndTime, + ownerID, + limit: limit, + asc: asc, + ignoredCollectionIDs: archivedCollectionIds, + ); } + // hide ignored files from home page UI final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs; result.files.removeWhere( diff --git a/lib/ui/huge_listview/lazy_loading_gallery.dart b/lib/ui/huge_listview/lazy_loading_gallery.dart index 4b1608e57..573cc0806 100644 --- a/lib/ui/huge_listview/lazy_loading_gallery.dart +++ b/lib/ui/huge_listview/lazy_loading_gallery.dart @@ -103,7 +103,7 @@ class _LazyLoadingGalleryState extends State { DateTime(galleryDate.year, galleryDate.month, galleryDate.day); final result = await widget.asyncLoader( dayStartTime.microsecondsSinceEpoch, - dayStartTime.microsecondsSinceEpoch + kMicroSecondsInDay - 1, + dayStartTime.microsecondsSinceEpoch + microSecondsInDay - 1, ); if (mounted) { setState(() { @@ -321,7 +321,7 @@ class _LazyLoadingGridViewState extends State { child: Stack( children: [ Hero( - tag: widget.tag + file.tag(), + tag: widget.tag + file.tag, child: ColorFiltered( colorFilter: ColorFilter.mode( Colors.black.withOpacity( @@ -331,10 +331,10 @@ class _LazyLoadingGridViewState extends State { ), child: ThumbnailWidget( file, - diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration, - serverLoadDeferDuration: kThumbnailServerLoadDeferDuration, + diskLoadDeferDuration: thumbnailDiskLoadDeferDuration, + serverLoadDeferDuration: thumbnailServerLoadDeferDuration, shouldShowLivePhotoOverlay: true, - key: Key(widget.tag + file.tag()), + key: Key(widget.tag + file.tag), ), ), ), diff --git a/lib/ui/landing_page_widget.dart b/lib/ui/landing_page_widget.dart index e54de1695..17fd35862 100644 --- a/lib/ui/landing_page_widget.dart +++ b/lib/ui/landing_page_widget.dart @@ -10,6 +10,7 @@ import 'package:photos/ui/account/email_entry_page.dart'; import 'package:photos/ui/account/login_page.dart'; import 'package:photos/ui/account/password_entry_page.dart'; import 'package:photos/ui/account/password_reentry_page.dart'; +import 'package:photos/ui/common/dialogs.dart'; import 'package:photos/ui/common/gradient_button.dart'; import 'package:photos/ui/payment/subscription.dart'; @@ -23,6 +24,12 @@ class LandingPageWidget extends StatefulWidget { class _LandingPageWidgetState extends State { double _featureIndex = 0; + @override + void initState() { + super.initState(); + Future(_showAutoLogoutDialogIfRequired); + } + @override Widget build(BuildContext context) { return Scaffold(body: _getBody(), resizeToAvoidBottomInset: false); @@ -195,6 +202,25 @@ class _LandingPageWidgetState extends State { ), ); } + + Future _showAutoLogoutDialogIfRequired() async { + final bool autoLogout = Configuration.instance.showAutoLogoutDialog(); + if (autoLogout) { + final result = await showChoiceDialog( + context, + "Please login again", + '''Unfortunately, the ente app had to log you out because of some technical issues. Sorry!\n\nPlease login again.''', + firstAction: "Cancel", + secondAction: "Login", + ); + if (result != null) { + await Configuration.instance.clearAutoLogoutFlag(); + } + if (result == DialogUserChoice.secondChoice) { + _navigateToSignInPage(); + } + } + } } class FeatureItemWidget extends StatelessWidget { diff --git a/lib/ui/loading_photos_widget.dart b/lib/ui/loading_photos_widget.dart index 4a4f4c69c..51ae17ef9 100644 --- a/lib/ui/loading_photos_widget.dart +++ b/lib/ui/loading_photos_widget.dart @@ -1,11 +1,13 @@ // @dart=2.9 import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/ente_theme_data.dart'; +import 'package:photos/events/local_import_progress.dart'; import 'package:photos/events/sync_status_update_event.dart'; import 'package:photos/services/local_sync_service.dart'; import 'package:photos/ui/backup_folder_selection_page.dart'; @@ -21,7 +23,9 @@ class LoadingPhotosWidget extends StatefulWidget { class _LoadingPhotosWidgetState extends State { StreamSubscription _firstImportEvent; + StreamSubscription _imprortProgressEvent; int _currentPage = 0; + String _loadingMessage = "Loading your photos..."; final PageController _pageController = PageController( initialPage: 0, ); @@ -56,6 +60,15 @@ class _LoadingPhotosWidgetState extends State { } } }); + _imprortProgressEvent = + Bus.instance.on().listen((event) { + if (Platform.isAndroid) { + _loadingMessage = 'Processing ${event.folderName}...'; + if (mounted) { + setState(() {}); + } + } + }); Timer.periodic(const Duration(seconds: 5), (Timer timer) { if (!mounted) { return; @@ -77,6 +90,7 @@ class _LoadingPhotosWidgetState extends State { @override void dispose() { _firstImportEvent.cancel(); + _imprortProgressEvent.cancel(); super.dispose(); } @@ -118,7 +132,7 @@ class _LoadingPhotosWidgetState extends State { ], ), Text( - "Loading your photos...", + _loadingMessage, style: TextStyle( color: Theme.of(context).colorScheme.subTextColor, ), diff --git a/lib/ui/memories_widget.dart b/lib/ui/memories_widget.dart index 33da0a95f..39ef03f4b 100644 --- a/lib/ui/memories_widget.dart +++ b/lib/ui/memories_widget.dart @@ -150,11 +150,11 @@ class _MemoryWidgetState extends State { width: isSeen ? 60 : 56, height: isSeen ? 60 : 56, child: Hero( - tag: "memories" + memory.file.tag(), + tag: "memories" + memory.file.tag, child: ThumbnailWidget( memory.file, shouldShowSyncStatus: false, - key: Key("memories" + memory.file.tag()), + key: Key("memories" + memory.file.tag), ), ), ), diff --git a/lib/ui/payment/payment_web_page.dart b/lib/ui/payment/payment_web_page.dart index 8c7859d13..06bf8541a 100644 --- a/lib/ui/payment/payment_web_page.dart +++ b/lib/ui/payment/payment_web_page.dart @@ -218,7 +218,7 @@ class _PaymentWebPageState extends State { final response = await billingService.verifySubscription( widget.planId, checkoutSessionID, - paymentProvider: kStripe, + paymentProvider: stripe, ); await _dialog.hide(); if (response != null) { diff --git a/lib/ui/payment/skip_subscription_widget.dart b/lib/ui/payment/skip_subscription_widget.dart index b9a144d47..ae72e0d77 100644 --- a/lib/ui/payment/skip_subscription_widget.dart +++ b/lib/ui/payment/skip_subscription_widget.dart @@ -42,7 +42,7 @@ class SkipSubscriptionWidget extends StatelessWidget { (route) => false, ); BillingService.instance - .verifySubscription(kFreeProductID, "", paymentProvider: "ente"); + .verifySubscription(freeProductID, "", paymentProvider: "ente"); }, child: const Text("Continue on free plan"), ), diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index fcd161e04..b1a23610c 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -67,7 +67,7 @@ class _StripeSubscriptionPageState extends State { _currentSubscription = userDetails.subscription; _showYearlyPlan = _currentSubscription.isYearlyPlan(); _hasActiveSubscription = _currentSubscription.isValid(); - _isStripeSubscriber = _currentSubscription.paymentProvider == kStripe; + _isStripeSubscriber = _currentSubscription.paymentProvider == stripe; return _filterStripeForUI().then((value) { _hasLoadedData = true; setState(() {}); @@ -103,7 +103,7 @@ class _StripeSubscriptionPageState extends State { if (widget.isOnboarding && _currentSubscription != null && _currentSubscription.isValid() && - _currentSubscription.productID != kFreeProductID) { + _currentSubscription.productID != freeProductID) { Navigator.of(context).popUntil((route) => route.isFirst); } } @@ -204,7 +204,7 @@ class _StripeSubscriptionPageState extends State { widgets.add(ValidityWidget(currentSubscription: _currentSubscription)); } - if (_currentSubscription.productID == kFreeProductID) { + if (_currentSubscription.productID == freeProductID) { if (widget.isOnboarding) { widgets.add(SkipSubscriptionWidget(freePlan: _freePlan)); } @@ -216,7 +216,7 @@ class _StripeSubscriptionPageState extends State { widgets.add(_stripeRenewOrCancelButton()); } - if (_currentSubscription.productID != kFreeProductID) { + if (_currentSubscription.productID != freeProductID) { widgets.addAll([ Align( alignment: Alignment.center, @@ -225,17 +225,17 @@ class _StripeSubscriptionPageState extends State { final String paymentProvider = _currentSubscription.paymentProvider; switch (_currentSubscription.paymentProvider) { - case kStripe: + case stripe: await _launchStripePortal(); break; - case kPlayStore: + case playStore: launchUrlString( "https://play.google.com/store/account/subscriptions?sku=" + _currentSubscription.productID + "&package=io.ente.photos", ); break; - case kAppStore: + case appStore: launchUrlString("https://apps.apple.com/account/billing"); break; default: @@ -328,7 +328,7 @@ class _StripeSubscriptionPageState extends State { } Future _launchFamilyPortal() async { - if (_userDetails.subscription.productID == kFreeProductID) { + if (_userDetails.subscription.productID == freeProductID) { await showErrorDialog( context, "Now you can share your storage plan with your family members!", @@ -441,7 +441,7 @@ class _StripeSubscriptionPageState extends State { // payment providers if (!_isStripeSubscriber && _hasActiveSubscription && - _currentSubscription.productID != kFreeProductID) { + _currentSubscription.productID != freeProductID) { showErrorDialog( context, "Sorry", @@ -544,7 +544,7 @@ class _StripeSubscriptionPageState extends State { // don't add current plan if it's monthly plan but UI is showing yearly plans // and vice versa. if (_showYearlyPlan != _currentSubscription.isYearlyPlan() && - _currentSubscription.productID != kFreeProductID) { + _currentSubscription.productID != freeProductID) { return; } int activePlanIndex = 0; diff --git a/lib/ui/payment/subscription_common_widgets.dart b/lib/ui/payment/subscription_common_widgets.dart index 3638782e7..c5903e698 100644 --- a/lib/ui/payment/subscription_common_widgets.dart +++ b/lib/ui/payment/subscription_common_widgets.dart @@ -87,13 +87,13 @@ class ValidityWidget extends StatelessWidget { @override Widget build(BuildContext context) { if (currentSubscription == null) { - return Container(); + return const SizedBox.shrink(); } final endDate = getDateAndMonthAndYear( DateTime.fromMicrosecondsSinceEpoch(currentSubscription.expiryTime), ); var message = "Renews on $endDate"; - if (currentSubscription.productID == kFreeProductID) { + if (currentSubscription.productID == freeProductID) { message = "Free plan valid till $endDate"; } else if (currentSubscription.attributes?.isCancelled ?? false) { message = "Your subscription will be cancelled on $endDate"; diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart index d62ee50b7..06dae0802 100644 --- a/lib/ui/payment/subscription_page.dart +++ b/lib/ui/payment/subscription_page.dart @@ -160,7 +160,7 @@ class _SubscriptionPageState extends State { _hasActiveSubscription = _currentSubscription.isValid(); final billingPlans = await _billingService.getBillingPlans(); _isActiveStripeSubscriber = - _currentSubscription.paymentProvider == kStripe && + _currentSubscription.paymentProvider == stripe && _currentSubscription.isValid(); _plans = billingPlans.plans.where((plan) { final productID = _isActiveStripeSubscriber @@ -210,7 +210,7 @@ class _SubscriptionPageState extends State { widgets.add(ValidityWidget(currentSubscription: _currentSubscription)); } - if (_currentSubscription.productID == kFreeProductID) { + if (_currentSubscription.productID == freeProductID) { if (widget.isOnboarding) { widgets.add(SkipSubscriptionWidget(freePlan: _freePlan)); } @@ -218,7 +218,7 @@ class _SubscriptionPageState extends State { } if (_hasActiveSubscription && - _currentSubscription.productID != kFreeProductID) { + _currentSubscription.productID != freeProductID) { widgets.addAll([ Align( alignment: Alignment.center, @@ -226,15 +226,15 @@ class _SubscriptionPageState extends State { onTap: () { final String paymentProvider = _currentSubscription.paymentProvider; - if (paymentProvider == kAppStore && !Platform.isAndroid) { + if (paymentProvider == appStore && !Platform.isAndroid) { launchUrlString("https://apps.apple.com/account/billing"); - } else if (paymentProvider == kPlayStore && Platform.isAndroid) { + } else if (paymentProvider == playStore && Platform.isAndroid) { launchUrlString( "https://play.google.com/store/account/subscriptions?sku=" + _currentSubscription.productID + "&package=io.ente.photos", ); - } else if (paymentProvider == kStripe) { + } else if (paymentProvider == stripe) { showErrorDialog( context, "Sorry", @@ -360,7 +360,7 @@ class _SubscriptionPageState extends State { bool foundActivePlan = false; final List planWidgets = []; if (_hasActiveSubscription && - _currentSubscription.productID == kFreeProductID) { + _currentSubscription.productID == freeProductID) { foundActivePlan = true; planWidgets.add( SubscriptionPlanWidget( @@ -407,7 +407,7 @@ class _SubscriptionPageState extends State { } final isCrossGradingOnAndroid = Platform.isAndroid && _hasActiveSubscription && - _currentSubscription.productID != kFreeProductID && + _currentSubscription.productID != freeProductID && _currentSubscription.productID != plan.androidID; if (isCrossGradingOnAndroid) { final existingProductDetailsResponse = @@ -485,7 +485,7 @@ class _SubscriptionPageState extends State { // todo: refactor manage family in common widget Future _launchFamilyPortal() async { - if (_userDetails.subscription.productID == kFreeProductID) { + if (_userDetails.subscription.productID == freeProductID) { await showErrorDialog( context, "Share your storage plan with your family members!", diff --git a/lib/ui/settings/debug_section_widget.dart b/lib/ui/settings/debug_section_widget.dart index 0743cf60c..ba093d826 100644 --- a/lib/ui/settings/debug_section_widget.dart +++ b/lib/ui/settings/debug_section_widget.dart @@ -4,9 +4,13 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:photos/core/configuration.dart'; +import 'package:photos/services/ignored_files_service.dart'; +import 'package:photos/services/local_sync_service.dart'; +import 'package:photos/services/sync_service.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/ui/settings/settings_section_title.dart'; import 'package:photos/ui/settings/settings_text_item.dart'; +import 'package:photos/utils/toast_util.dart'; class DebugSectionWidget extends StatelessWidget { const DebugSectionWidget({Key key}) : super(key: key); @@ -33,6 +37,29 @@ class DebugSectionWidget extends StatelessWidget { icon: Icons.navigate_next, ), ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () async { + await LocalSyncService.instance.resetLocalSync(); + showToast(context, "Done"); + }, + child: const SettingsTextItem( + text: "Delete Local Import DB", + icon: Icons.navigate_next, + ), + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () async { + await IgnoredFilesService.instance.reset(); + SyncService.instance.sync(); + showToast(context, "Done"); + }, + child: const SettingsTextItem( + text: "Allow auto-upload for ignored files", + icon: Icons.navigate_next, + ), + ), ], ); } diff --git a/lib/ui/settings/support_section_widget.dart b/lib/ui/settings/support_section_widget.dart index b7b543c71..845fc657b 100644 --- a/lib/ui/settings/support_section_widget.dart +++ b/lib/ui/settings/support_section_widget.dart @@ -33,7 +33,7 @@ class SupportSectionWidget extends StatelessWidget { GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { - await sendEmail(context, to: kSupportEmail); + await sendEmail(context, to: supportEmail); }, child: const SettingsTextItem(text: "Email", icon: Icons.navigate_next), @@ -50,7 +50,7 @@ class SupportSectionWidget extends StatelessWidget { final isLoggedIn = Configuration.instance.getToken() != null; final url = isLoggedIn ? endpoint + "?token=" + Configuration.instance.getToken() - : kRoadmapURL; + : roadmapURL; return WebPage("Roadmap", url); }, ), diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index e50498dc5..5c0811e61 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:photos/core/configuration.dart'; +import 'package:photos/services/feature_flag_service.dart'; import 'package:photos/ui/settings/account_section_widget.dart'; import 'package:photos/ui/settings/app_version_widget.dart'; import 'package:photos/ui/settings/backup_section_widget.dart'; @@ -104,7 +105,8 @@ class SettingsPage extends StatelessWidget { ]); } - if (kDebugMode && hasLoggedIn) { + if (FeatureFlagService.instance.isInternalUserOrDebugBuild() && + hasLoggedIn) { contents.addAll([sectionDivider, const DebugSectionWidget()]); } contents.add(const AppVersionWidget()); diff --git a/lib/ui/shared_collections_gallery.dart b/lib/ui/shared_collections_gallery.dart index 55cbb28cc..817db0403 100644 --- a/lib/ui/shared_collections_gallery.dart +++ b/lib/ui/shared_collections_gallery.dart @@ -191,7 +191,6 @@ class _SharedCollectionGalleryState extends State shareText("Check out https://ente.io"); }, iconData: Icons.outgoing_mail, - paddingValue: 2, text: "Invite", ), ), @@ -227,7 +226,6 @@ class _SharedCollectionGalleryState extends State ); }, iconData: Icons.person_add, - paddingValue: 2, text: "Share", ), ), @@ -294,10 +292,10 @@ class OutgoingCollectionItem extends StatelessWidget { height: 60, width: 60, child: Hero( - tag: "outgoing_collection" + c.thumbnail.tag(), + tag: "outgoing_collection" + c.thumbnail.tag, child: ThumbnailWidget( c.thumbnail, - key: Key("outgoing_collection" + c.thumbnail.tag()), + key: Key("outgoing_collection" + c.thumbnail.tag), ), ), ), @@ -385,10 +383,10 @@ class IncomingCollectionItem extends StatelessWidget { child: Stack( children: [ Hero( - tag: "shared_collection" + c.thumbnail.tag(), + tag: "shared_collection" + c.thumbnail.tag, child: ThumbnailWidget( c.thumbnail, - key: Key("shared_collection" + c.thumbnail.tag()), + key: Key("shared_collection" + c.thumbnail.tag), ), ), Align( diff --git a/lib/ui/sharing/share_collection_widget.dart b/lib/ui/sharing/share_collection_widget.dart index b675739c5..4bbbbe45a 100644 --- a/lib/ui/sharing/share_collection_widget.dart +++ b/lib/ui/sharing/share_collection_widget.dart @@ -9,14 +9,11 @@ import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:fluttercontactpicker/fluttercontactpicker.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; -import 'package:photos/core/event_bus.dart'; import 'package:photos/db/public_keys_db.dart'; import 'package:photos/ente_theme_data.dart'; -import 'package:photos/events/backup_folders_updated_event.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/models/public_key.dart'; import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/feature_flag_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/common/dialogs.dart'; import 'package:photos/ui/common/gradient_button.dart'; @@ -92,86 +89,70 @@ class _SharingDialogState extends State { ); } - if (!FeatureFlagService.instance.disableUrlSharing()) { - final bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false; - children.addAll([ - const Padding(padding: EdgeInsets.all(16)), - const Divider(height: 1), - const Padding(padding: EdgeInsets.all(12)), - SizedBox( - height: 36, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Public link"), - Switch( - value: hasUrl, - onChanged: (enable) async { - // confirm if user wants to disable the url - if (!enable) { - final choice = await showChoiceDialog( - context, - 'Disable link', - 'Are you sure that you want to disable the album link?', - firstAction: 'Yes, disable', - secondAction: 'No', - actionType: ActionType.critical, - ); - if (choice != DialogUserChoice.firstChoice) { - return; - } - } else { - // Add local folder in backup patch before creating - // sharable link - if (widget.collection.type == CollectionType.folder) { - final path = CollectionsService.instance - .decryptCollectionPath(widget.collection); - if (!Configuration.instance - .getPathsToBackUp() - .contains(path)) { - await Configuration.instance - .addPathToFoldersToBeBackedUp(path); - Bus.instance.fire(BackupFoldersUpdatedEvent()); - } - } - } - final dialog = createProgressDialog( + final bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false; + children.addAll([ + const Padding(padding: EdgeInsets.all(16)), + const Divider(height: 1), + const Padding(padding: EdgeInsets.all(12)), + SizedBox( + height: 36, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Public link"), + Switch( + value: hasUrl, + onChanged: (enable) async { + // confirm if user wants to disable the url + if (!enable) { + final choice = await showChoiceDialog( context, - enable ? "Creating link..." : "Disabling link...", + 'Disable link', + 'Are you sure that you want to disable the album link?', + firstAction: 'Yes, disable', + secondAction: 'No', + actionType: ActionType.critical, ); - try { - await dialog.show(); - enable - ? await CollectionsService.instance - .createShareUrl(widget.collection) - : await CollectionsService.instance - .disableShareUrl(widget.collection); - dialog.hide(); - setState(() {}); - } catch (e) { - dialog.hide(); - if (e is SharingNotPermittedForFreeAccountsError) { - _showUnSupportedAlert(); - } else { - _logger.severe("failed to share collection", e); - showGenericErrorDialog(context); - } + if (choice != DialogUserChoice.firstChoice) { + return; } - }, - ), - ], - ), + } + final dialog = createProgressDialog( + context, + enable ? "Creating link..." : "Disabling link...", + ); + try { + await dialog.show(); + enable + ? await CollectionsService.instance + .createShareUrl(widget.collection) + : await CollectionsService.instance + .disableShareUrl(widget.collection); + dialog.hide(); + setState(() {}); + } catch (e) { + dialog.hide(); + if (e is SharingNotPermittedForFreeAccountsError) { + _showUnSupportedAlert(); + } else { + _logger.severe("failed to share collection", e); + showGenericErrorDialog(context); + } + } + }, + ), + ], ), - const Padding(padding: EdgeInsets.all(8)), - ]); - if (widget.collection.publicURLs?.isNotEmpty ?? false) { - children.add( - const Padding( - padding: EdgeInsets.all(2), - ), - ); - children.add(_getShareableUrlWidget(context)); - } + ), + const Padding(padding: EdgeInsets.all(8)), + ]); + if (widget.collection.publicURLs?.isNotEmpty ?? false) { + children.add( + const Padding( + padding: EdgeInsets.all(2), + ), + ); + children.add(_getShareableUrlWidget(context)); } return AlertDialog( @@ -251,7 +232,8 @@ class _SharingDialogState extends State { final String collectionKey = Base58Encode( CollectionsService.instance.getCollectionKey(widget.collection.id), ); - final String url = "${widget.collection.publicURLs.first.url}#$collectionKey"; + final String url = + "${widget.collection.publicURLs.first.url}#$collectionKey"; return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -408,15 +390,7 @@ class _SharingDialogState extends State { } else { final dialog = createProgressDialog(context, "Sharing..."); await dialog.show(); - final collection = widget.collection; try { - if (collection.type == CollectionType.folder) { - final path = - CollectionsService.instance.decryptCollectionPath(collection); - if (!Configuration.instance.getPathsToBackUp().contains(path)) { - await Configuration.instance.addPathToFoldersToBeBackedUp(path); - } - } await CollectionsService.instance .share(widget.collection.id, email, publicKey); await dialog.hide(); diff --git a/lib/ui/status_bar_widget.dart b/lib/ui/status_bar_widget.dart index db3bd0c43..72c3290ec 100644 --- a/lib/ui/status_bar_widget.dart +++ b/lib/ui/status_bar_widget.dart @@ -7,7 +7,6 @@ import 'package:photos/core/event_bus.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/events/notification_event.dart'; import 'package:photos/events/sync_status_update_event.dart'; -import 'package:photos/services/feature_flag_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/user_remote_flag_service.dart'; import 'package:photos/ui/account/verify_recovery_page.dart'; @@ -97,14 +96,11 @@ class _StatusBarWidgetState extends State { Positioned( right: 0, top: 0, - child: FeatureFlagService.instance.enableSearch() - ? Container( - color: - Theme.of(context).colorScheme.defaultBackgroundColor, - height: kContainerHeight, - child: const SearchIconWidget(), - ) - : const SizedBox(height: 36, width: 48), + child: Container( + color: Theme.of(context).colorScheme.defaultBackgroundColor, + height: kContainerHeight, + child: const SearchIconWidget(), + ), ), ], ), @@ -117,7 +113,7 @@ class _StatusBarWidgetState extends State { ? NotificationWarningWidget( warningIcon: Icons.gpp_maybe, actionIcon: Icons.arrow_forward, - text: "Please ensure that you have your 24 word recovery key", + text: "Please ensure you have your 24 word recovery key", onTap: () async => { await routeToPage( context, diff --git a/lib/ui/tools/deduplicate_page.dart b/lib/ui/tools/deduplicate_page.dart index 8b788a380..1537d8a52 100644 --- a/lib/ui/tools/deduplicate_page.dart +++ b/lib/ui/tools/deduplicate_page.dart @@ -12,7 +12,7 @@ import 'package:photos/services/collections_service.dart'; import 'package:photos/services/deduplication_service.dart'; import 'package:photos/ui/viewer/file/detail_page.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; -import 'package:photos/ui/viewer/gallery/empte_state.dart'; +import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/utils/data_util.dart'; import 'package:photos/utils/delete_file_util.dart'; import 'package:photos/utils/navigation_util.dart'; @@ -445,16 +445,15 @@ class _DeduplicatePageState extends State { child: Stack( children: [ Hero( - tag: "deduplicate_" + file.tag(), + tag: "deduplicate_" + file.tag, child: ClipRRect( borderRadius: BorderRadius.circular(4), child: ThumbnailWidget( file, - diskLoadDeferDuration: kThumbnailDiskLoadDeferDuration, - serverLoadDeferDuration: - kThumbnailServerLoadDeferDuration, + diskLoadDeferDuration: thumbnailDiskLoadDeferDuration, + serverLoadDeferDuration: thumbnailServerLoadDeferDuration, shouldShowLivePhotoOverlay: true, - key: Key("deduplicate_" + file.tag()), + key: Key("deduplicate_" + file.tag), ), ), ), @@ -472,7 +471,8 @@ class _DeduplicatePageState extends State { padding: const EdgeInsets.only(right: 2), child: Text( CollectionsService.instance - .getCollectionNameByID(file.collectionID), + .getCollectionByID(file.collectionID) + .name, style: Theme.of(context).textTheme.caption.copyWith(fontSize: 12), overflow: TextOverflow.ellipsis, ), diff --git a/lib/ui/tools/editor/image_editor_page.dart b/lib/ui/tools/editor/image_editor_page.dart index 196c5a475..135869625 100644 --- a/lib/ui/tools/editor/image_editor_page.dart +++ b/lib/ui/tools/editor/image_editor_page.dart @@ -115,7 +115,7 @@ class _ImageEditorPageState extends State { Widget _buildImage() { return Hero( - tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag(), + tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag, child: ExtendedImage( image: widget.imageProvider, extendedImageEditorKey: editorKey, @@ -350,8 +350,8 @@ class _ImageEditorPageState extends State { newFile.creationTime = widget.originalFile.creationTime; newFile.collectionID = widget.originalFile.collectionID; newFile.location = widget.originalFile.location; - if (!newFile.hasLocation() && widget.originalFile.localID != null) { - final assetEntity = await widget.originalFile.getAsset(); + if (!newFile.hasLocation && widget.originalFile.localID != null) { + final assetEntity = await widget.originalFile.getAsset; if (assetEntity != null) { final latLong = await assetEntity.latlngAsync(); newFile.location = Location(latLong.latitude, latLong.longitude); diff --git a/lib/ui/tools/lock_screen.dart b/lib/ui/tools/lock_screen.dart index 4f38cd0eb..82fe53bec 100644 --- a/lib/ui/tools/lock_screen.dart +++ b/lib/ui/tools/lock_screen.dart @@ -42,7 +42,6 @@ class _LockScreenState extends State { child: GradientButton( text: "Unlock", iconData: Icons.lock_open_outlined, - paddingValue: 6, onTap: () async { _showLockScreen(); }, diff --git a/lib/ui/viewer/file/detail_page.dart b/lib/ui/viewer/file/detail_page.dart index 97f3c194e..616bd9890 100644 --- a/lib/ui/viewer/file/detail_page.dart +++ b/lib/ui/viewer/file/detail_page.dart @@ -229,7 +229,7 @@ class _DetailPageState extends State { } if (_selectedIndex == _files.length - 1 && !_hasLoadedTillEnd) { final result = await widget.config.asyncLoader( - kGalleryLoadStartTime, + galleryLoadStartTime, _files[_selectedIndex].creationTime - 1, limit: kLoadLimit, ); diff --git a/lib/ui/viewer/file/fading_app_bar.dart b/lib/ui/viewer/file/fading_app_bar.dart index 1ddfd2ce1..e13c1a431 100644 --- a/lib/ui/viewer/file/fading_app_bar.dart +++ b/lib/ui/viewer/file/fading_app_bar.dart @@ -109,7 +109,7 @@ class FadingAppBarState extends State { PopupMenuButton( itemBuilder: (context) { final List items = []; - if (widget.file.isRemoteFile()) { + if (widget.file.isRemoteFile) { items.add( PopupMenuItem( value: 1, @@ -317,7 +317,7 @@ class FadingAppBarState extends State { if (type == FileType.livePhoto) { final io.File liveVideo = await getFileFromServer(file, liveVideo: true); if (liveVideo == null) { - _logger.warning("Failed to find live video" + file.tag()); + _logger.warning("Failed to find live video" + file.tag); } else { final videoTitle = file_path.basenameWithoutExtension(file.title) + file_path.extension(liveVideo.path); diff --git a/lib/ui/viewer/file/fading_bottom_bar.dart b/lib/ui/viewer/file/fading_bottom_bar.dart index 607714cd7..61dd57ff2 100644 --- a/lib/ui/viewer/file/fading_bottom_bar.dart +++ b/lib/ui/viewer/file/fading_bottom_bar.dart @@ -106,7 +106,7 @@ class FadingBottomBarState extends State { if (widget.file.uploadedFileID != null && widget.file.ownerID == Configuration.instance.getUserID()) { final bool isArchived = - widget.file.magicMetadata.visibility == kVisibilityArchive; + widget.file.magicMetadata.visibility == visibilityArchive; children.add( Tooltip( message: isArchived ? "Unhide" : "Hide", @@ -123,7 +123,7 @@ class FadingBottomBarState extends State { await changeVisibility( context, [widget.file], - isArchived ? kVisibilityVisible : kVisibilityArchive, + isArchived ? visibilityVisible : visibilityArchive, ); safeRefresh(); }, diff --git a/lib/ui/viewer/file/file_info_widget.dart b/lib/ui/viewer/file/file_info_widget.dart index db97e3fad..69edf56c5 100644 --- a/lib/ui/viewer/file/file_info_widget.dart +++ b/lib/ui/viewer/file/file_info_widget.dart @@ -136,7 +136,7 @@ class _FileInfoWidgetState extends State { ), ), title: Text( - file.getDisplayName(), + file.displayName, ), subtitle: Row( children: [ @@ -358,7 +358,7 @@ class _FileInfoWidgetState extends State { ); } return FutureBuilder( - future: widget.file.getAsset(), + future: widget.file.getAsset, builder: (context, snapshot) { if (snapshot.hasData) { return Text( diff --git a/lib/ui/viewer/file/thumbnail_widget.dart b/lib/ui/viewer/file/thumbnail_widget.dart index cecdf7541..c3a2a8399 100644 --- a/lib/ui/viewer/file/thumbnail_widget.dart +++ b/lib/ui/viewer/file/thumbnail_widget.dart @@ -35,7 +35,7 @@ class ThumbnailWidget extends StatefulWidget { this.shouldShowArchiveStatus = false, this.diskLoadDeferDuration, this.serverLoadDeferDuration, - }) : super(key: key ?? Key(file.tag())); + }) : super(key: key ?? Key(file.tag)); @override State createState() => _ThumbnailWidgetState(); @@ -60,7 +60,7 @@ class _ThumbnailWidgetState extends State { super.dispose(); Future.delayed(const Duration(milliseconds: 10), () { // Cancel request only if the widget has been unmounted - if (!mounted && widget.file.isRemoteFile() && !_hasLoadedThumbnail) { + if (!mounted && widget.file.isRemoteFile && !_hasLoadedThumbnail) { removePendingGetThumbnailRequestIfAny(widget.file); } }); @@ -76,7 +76,7 @@ class _ThumbnailWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.file.isRemoteFile()) { + if (widget.file.isRemoteFile) { _loadNetworkImage(); } else { _loadLocalImage(context); @@ -138,7 +138,7 @@ class _ThumbnailWidgetState extends State { !_isLoadingLocalThumbnail) { _isLoadingLocalThumbnail = true; final cachedSmallThumbnail = - ThumbnailLruCache.get(widget.file, kThumbnailSmallSize); + ThumbnailLruCache.get(widget.file, thumbnailSmallSize); if (cachedSmallThumbnail != null) { _imageProvider = Image.memory(cachedSmallThumbnail).image; _hasLoadedThumbnail = true; @@ -160,7 +160,7 @@ class _ThumbnailWidgetState extends State { getThumbnailFromLocal(widget.file).then((thumbData) async { if (thumbData == null) { if (widget.file.uploadedFileID != null) { - _logger.fine("Removing localID reference for " + widget.file.tag()); + _logger.fine("Removing localID reference for " + widget.file.tag); widget.file.localID = null; if (widget.file is TrashFile) { TrashDB.instance.update(widget.file); @@ -170,7 +170,7 @@ class _ThumbnailWidgetState extends State { _loadNetworkImage(); } else { if (await doesLocalFileExist(widget.file) == false) { - _logger.info("Deleting file " + widget.file.tag()); + _logger.info("Deleting file " + widget.file.tag); FilesDB.instance.deleteLocalFile(widget.file); Bus.instance.fire( LocalPhotosUpdatedEvent( @@ -187,7 +187,7 @@ class _ThumbnailWidgetState extends State { final imageProvider = Image.memory(thumbData).image; _cacheAndRender(imageProvider); } - ThumbnailLruCache.put(widget.file, thumbData, kThumbnailSmallSize); + ThumbnailLruCache.put(widget.file, thumbData, thumbnailSmallSize); }).catchError((e) { _logger.warning("Could not load image: ", e); _errorLoadingLocalThumbnail = true; diff --git a/lib/ui/viewer/file/video_widget.dart b/lib/ui/viewer/file/video_widget.dart index 162cc5caf..26a59353c 100644 --- a/lib/ui/viewer/file/video_widget.dart +++ b/lib/ui/viewer/file/video_widget.dart @@ -6,8 +6,10 @@ import 'package:chewie/chewie.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/models/file.dart'; +import 'package:photos/services/files_service.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; import 'package:photos/ui/viewer/file/video_controls.dart'; import 'package:photos/utils/file_util.dart'; @@ -43,9 +45,10 @@ class _VideoWidgetState extends State { @override void initState() { super.initState(); - if (widget.file.isRemoteFile()) { + if (widget.file.isRemoteFile) { _loadNetworkVideo(); - } else if (widget.file.isSharedMediaToAppSandbox()) { + _setFileSizeIfNull(); + } else if (widget.file.isSharedMediaToAppSandbox) { final localFile = io.File(getSharedMediaFilePath(widget.file)); if (localFile.existsSync()) { _logger.fine("loading from app cache"); @@ -54,7 +57,7 @@ class _VideoWidgetState extends State { _loadNetworkVideo(); } } else { - widget.file.getAsset().then((asset) async { + widget.file.getAsset.then((asset) async { if (asset == null || !(await asset.exists)) { if (widget.file.uploadedFileID != null) { _loadNetworkVideo(); @@ -68,13 +71,25 @@ class _VideoWidgetState extends State { } } + void _setFileSizeIfNull() { + if (widget.file.fileSize == null && + widget.file.ownerID == Configuration.instance.getUserID()) { + FilesService.instance + .getFileSize(widget.file.uploadedFileID) + .then((value) { + widget.file.fileSize = value; + setState(() {}); + }); + } + } + void _loadNetworkVideo() { getFileFromServer( widget.file, progressCallback: (count, total) { if (mounted) { setState(() { - _progress = count / total; + _progress = count / (widget.file.fileSize ?? total); if (_progress == 1) { showShortToast(context, "Decrypting video..."); } @@ -123,11 +138,11 @@ class _VideoWidgetState extends State { final contentWithDetector = GestureDetector( child: content, onVerticalDragUpdate: (d) => { - if (d.delta.dy > kDragSensitivity) {Navigator.of(context).pop()} + if (d.delta.dy > dragSensitivity) {Navigator.of(context).pop()} }, ); return VisibilityDetector( - key: Key(widget.file.tag()), + key: Key(widget.file.tag), onVisibilityChanged: (info) { if (info.visibleFraction < 1) { if (mounted && _chewieController != null) { @@ -136,7 +151,7 @@ class _VideoWidgetState extends State { } }, child: Hero( - tag: widget.tagPrefix + widget.file.tag(), + tag: widget.tagPrefix + widget.file.tag, child: contentWithDetector, ), ); diff --git a/lib/ui/viewer/file/zoomable_image.dart b/lib/ui/viewer/file/zoomable_image.dart index b311d8a4f..f0c585e50 100644 --- a/lib/ui/viewer/file/zoomable_image.dart +++ b/lib/ui/viewer/file/zoomable_image.dart @@ -65,7 +65,7 @@ class _ZoomableImageState extends State @override Widget build(BuildContext context) { - if (_photo.isRemoteFile()) { + if (_photo.isRemoteFile) { _loadNetworkImage(); } else { _loadLocalImage(context); @@ -81,7 +81,7 @@ class _ZoomableImageState extends State minScale: PhotoViewComputedScale.contained, gaplessPlayback: true, heroAttributes: PhotoViewHeroAttributes( - tag: widget.tagPrefix + _photo.tag(), + tag: widget.tagPrefix + _photo.tag, ), backgroundDecoration: widget.backgroundDecoration, ), @@ -93,7 +93,7 @@ class _ZoomableImageState extends State final GestureDragUpdateCallback verticalDragCallback = _isZooming ? null : (d) => { - if (!_isZooming && d.delta.dy > kDragSensitivity) + if (!_isZooming && d.delta.dy > dragSensitivity) {Navigator.of(context).pop()} }; return GestureDetector( @@ -143,8 +143,7 @@ class _ZoomableImageState extends State if (!_loadedSmallThumbnail && !_loadedLargeThumbnail && !_loadedFinalImage) { - final cachedThumbnail = - ThumbnailLruCache.get(_photo, kThumbnailSmallSize); + final cachedThumbnail = ThumbnailLruCache.get(_photo, thumbnailSmallSize); if (cachedThumbnail != null) { _imageProvider = Image.memory(cachedThumbnail).image; _loadedSmallThumbnail = true; @@ -155,7 +154,7 @@ class _ZoomableImageState extends State !_loadedLargeThumbnail && !_loadedFinalImage) { _loadingLargeThumbnail = true; - getThumbnailFromLocal(_photo, size: kThumbnailLargeSize, quality: 100) + getThumbnailFromLocal(_photo, size: thumbnailLargeSize, quality: 100) .then((cachedThumbnail) { if (cachedThumbnail != null) { _onLargeThumbnailLoaded(Image.memory(cachedThumbnail).image, context); @@ -221,5 +220,5 @@ class _ZoomableImageState extends State } } - bool _isGIF() => _photo.getDisplayName().toLowerCase().endsWith(".gif"); + bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif"); } diff --git a/lib/ui/viewer/file/zoomable_live_image.dart b/lib/ui/viewer/file/zoomable_live_image.dart index 673be3ae7..e2298dc4e 100644 --- a/lib/ui/viewer/file/zoomable_live_image.dart +++ b/lib/ui/viewer/file/zoomable_live_image.dart @@ -121,14 +121,14 @@ class _ZoomableLiveImageState extends State return; } _isLoadingVideoPlayer = true; - if (_file.isRemoteFile() && !(await isFileCached(_file, liveVideo: true))) { + if (_file.isRemoteFile && !(await isFileCached(_file, liveVideo: true))) { showToast(context, "Downloading...", toastLength: Toast.LENGTH_LONG); } var videoFile = await getFile(widget.file, liveVideo: true) .timeout(const Duration(seconds: 15)) .onError((e, s) { - _logger.info("getFile failed ${_file.tag()}", e); + _logger.info("getFile failed ${_file.tag}", e); return null; }); @@ -140,7 +140,7 @@ class _ZoomableLiveImageState extends State videoFile = await getFileFromServer(widget.file, liveVideo: true) .timeout(const Duration(seconds: 15)) .onError((e, s) { - _logger.info("getRemoteFile failed ${_file.tag()}", e); + _logger.info("getRemoteFile failed ${_file.tag}", e); return null; }); } @@ -167,10 +167,10 @@ class _ZoomableLiveImageState extends State void _showLivePhotoToast() async { final preferences = await SharedPreferences.getInstance(); - final int promptTillNow = preferences.getInt(kLivePhotoToastCounterKey) ?? 0; - if (promptTillNow < kMaxLivePhotoToastCount && mounted) { + final int promptTillNow = preferences.getInt(livePhotoToastCounterKey) ?? 0; + if (promptTillNow < maxLivePhotoToastCount && mounted) { showToast(context, "Press and hold to play video"); - preferences.setInt(kLivePhotoToastCounterKey, promptTillNow + 1); + preferences.setInt(livePhotoToastCounterKey, promptTillNow + 1); } } } diff --git a/lib/ui/viewer/gallery/archive_page.dart b/lib/ui/viewer/gallery/archive_page.dart index 8d32828a7..05b074a05 100644 --- a/lib/ui/viewer/gallery/archive_page.dart +++ b/lib/ui/viewer/gallery/archive_page.dart @@ -29,11 +29,11 @@ class ArchivePage extends StatelessWidget { Widget build(Object context) { final gallery = Gallery( asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - return FilesDB.instance.getAllUploadedFiles( + return FilesDB.instance.getAllPendingOrUploadedFiles( creationStartTime, creationEndTime, Configuration.instance.getUserID(), - visibility: kVisibilityArchive, + visibility: visibilityArchive, limit: limit, asc: asc, ); diff --git a/lib/ui/viewer/gallery/collection_page.dart b/lib/ui/viewer/gallery/collection_page.dart index 5fa50c65f..dc0e5e725 100644 --- a/lib/ui/viewer/gallery/collection_page.dart +++ b/lib/ui/viewer/gallery/collection_page.dart @@ -6,8 +6,10 @@ import 'package:photos/db/files_db.dart'; import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/files_updated_event.dart'; import 'package:photos/models/collection_items.dart'; +import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/models/selected_files.dart'; +import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; @@ -29,14 +31,23 @@ class CollectionPage extends StatelessWidget { Widget build(Object context) { final initialFiles = c.thumbnail != null ? [c.thumbnail] : null; final gallery = Gallery( - asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - return FilesDB.instance.getFilesInCollection( + asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async { + final FileLoadResult result = + await FilesDB.instance.getFilesInCollection( c.collection.id, creationStartTime, creationEndTime, limit: limit, asc: asc, ); + // hide ignored files from home page UI + final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs; + result.files.removeWhere( + (f) => + f.uploadedFileID == null && + IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f), + ); + return result; }, reloadEvent: Bus.instance .on() diff --git a/lib/ui/viewer/gallery/device_all_page.dart b/lib/ui/viewer/gallery/device_all_page.dart deleted file mode 100644 index 6d0f363f6..000000000 --- a/lib/ui/viewer/gallery/device_all_page.dart +++ /dev/null @@ -1,61 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/db/files_db.dart'; -import 'package:photos/events/files_updated_event.dart'; -import 'package:photos/events/local_photos_updated_event.dart'; -import 'package:photos/models/gallery_type.dart'; -import 'package:photos/models/selected_files.dart'; -import 'package:photos/ui/viewer/gallery/gallery.dart'; -import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; -import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; - -// This page is used to display all photos which are present on the local device -class DeviceAllPage extends StatelessWidget { - final _selectedFiles = SelectedFiles(); - - DeviceAllPage({Key key}) : super(key: key); - - @override - Widget build(Object context) { - final gallery = Gallery( - asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - return FilesDB.instance.getLocalDeviceFiles( - creationStartTime, - creationEndTime, - limit: limit, - asc: asc, - ); - }, - reloadEvent: Bus.instance.on(), - removalEventTypes: const { - EventType.deletedFromDevice, - EventType.deletedFromEverywhere, - }, - tagPrefix: "device_all", - selectedFiles: _selectedFiles, - initialFiles: null, - ); - return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: GalleryAppBarWidget( - GalleryType.localAll, - "On device", - _selectedFiles, - ), - ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - GalleryOverlayWidget( - GalleryType.localFolder, - _selectedFiles, - ) - ], - ), - ); - } -} diff --git a/lib/ui/viewer/gallery/device_folder_page.dart b/lib/ui/viewer/gallery/device_folder_page.dart index 024506003..1ed2ec63d 100644 --- a/lib/ui/viewer/gallery/device_folder_page.dart +++ b/lib/ui/viewer/gallery/device_folder_page.dart @@ -3,30 +3,31 @@ import 'package:flutter/material.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/events/files_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; -import 'package:photos/models/device_folder.dart'; +import 'package:photos/models/device_collection.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/models/selected_files.dart'; +import 'package:photos/services/remote_sync_service.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; class DeviceFolderPage extends StatelessWidget { - final DeviceFolder folder; + final DeviceCollection deviceCollection; final _selectedFiles = SelectedFiles(); - DeviceFolderPage(this.folder, {Key key}) : super(key: key); + DeviceFolderPage(this.deviceCollection, {Key key}) : super(key: key); @override Widget build(Object context) { final gallery = Gallery( asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - return FilesDB.instance.getFilesInPath( - folder.path, + return FilesDB.instance.getFilesInDeviceCollection( + deviceCollection, creationStartTime, creationEndTime, limit: limit, @@ -38,21 +39,21 @@ class DeviceFolderPage extends StatelessWidget { EventType.deletedFromDevice, EventType.deletedFromEverywhere, }, - tagPrefix: "device_folder:" + folder.path, + tagPrefix: "device_folder:" + deviceCollection.name, selectedFiles: _selectedFiles, header: Configuration.instance.hasConfiguredAccount() - ? _getHeaderWidget() - : Container(), - initialFiles: [folder.thumbnail], + ? BackupConfigurationHeaderWidget(deviceCollection) + : const SizedBox.shrink(), + initialFiles: [deviceCollection.thumbnail], ); return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(50.0), child: GalleryAppBarWidget( GalleryType.localFolder, - folder.name, + deviceCollection.name, _selectedFiles, - path: folder.thumbnail.deviceFolder, + deviceCollection: deviceCollection, ), ), body: Stack( @@ -67,16 +68,13 @@ class DeviceFolderPage extends StatelessWidget { ), ); } - - Widget _getHeaderWidget() { - return BackupConfigurationHeaderWidget(folder.path); - } } class BackupConfigurationHeaderWidget extends StatefulWidget { - final String path; + final DeviceCollection deviceCollection; - const BackupConfigurationHeaderWidget(this.path, {Key key}) : super(key: key); + const BackupConfigurationHeaderWidget(this.deviceCollection, {Key key}) + : super(key: key); @override State createState() => @@ -85,10 +83,16 @@ class BackupConfigurationHeaderWidget extends StatefulWidget { class _BackupConfigurationHeaderWidgetState extends State { + bool _isBackedUp; + + @override + void initState() { + _isBackedUp = widget.deviceCollection.shouldBackup; + super.initState(); + } + @override Widget build(BuildContext context) { - final isBackedUp = - Configuration.instance.getPathsToBackUp().contains(widget.path); return Container( padding: const EdgeInsets.only(left: 20, right: 12, top: 4, bottom: 4), margin: const EdgeInsets.only(bottom: 12), @@ -96,7 +100,7 @@ class _BackupConfigurationHeaderWidgetState child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - isBackedUp + _isBackedUp ? const Text("Backup enabled") : Text( "Backup disabled", @@ -108,17 +112,13 @@ class _BackupConfigurationHeaderWidgetState ), ), Switch( - value: isBackedUp, + value: _isBackedUp, onChanged: (value) async { - final current = Configuration.instance.getPathsToBackUp(); - if (value) { - current.add(widget.path); - } else { - current.remove(widget.path); - } - await Configuration.instance.setPathsToBackUp(current); + await RemoteSyncService.instance.updateDeviceFolderSyncStatus( + {widget.deviceCollection.id: value}, + ); + _isBackedUp = value; setState(() {}); - Bus.instance.fire(BackupFoldersUpdatedEvent()); }, ), ], diff --git a/lib/ui/viewer/gallery/empte_state.dart b/lib/ui/viewer/gallery/empty_state.dart similarity index 100% rename from lib/ui/viewer/gallery/empte_state.dart rename to lib/ui/viewer/gallery/empty_state.dart diff --git a/lib/ui/viewer/gallery/gallery.dart b/lib/ui/viewer/gallery/gallery.dart index c085e5421..64409ab79 100644 --- a/lib/ui/viewer/gallery/gallery.dart +++ b/lib/ui/viewer/gallery/gallery.dart @@ -15,7 +15,7 @@ import 'package:photos/models/selected_files.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/huge_listview/huge_listview.dart'; import 'package:photos/ui/huge_listview/lazy_loading_gallery.dart'; -import 'package:photos/ui/viewer/gallery/empte_state.dart'; +import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/utils/date_time_util.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -118,8 +118,8 @@ class _GalleryState extends State { try { final startTime = DateTime.now().microsecondsSinceEpoch; final result = await widget.asyncLoader( - kGalleryLoadStartTime, - kGalleryLoadEndTime, + galleryLoadStartTime, + galleryLoadEndTime, limit: limit, ); final endTime = DateTime.now().microsecondsSinceEpoch; diff --git a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index ad3007aaf..52deb6910 100644 --- a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -10,6 +10,7 @@ import 'package:photos/db/files_db.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/models/collection.dart'; +import 'package:photos/models/device_collection.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/models/magic_metadata.dart'; import 'package:photos/models/selected_files.dart'; @@ -20,12 +21,13 @@ import 'package:photos/ui/common/rename_dialog.dart'; import 'package:photos/ui/sharing/share_collection_widget.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/magic_util.dart'; +import 'package:photos/utils/toast_util.dart'; class GalleryAppBarWidget extends StatefulWidget { final GalleryType type; final String title; final SelectedFiles selectedFiles; - final String path; + final DeviceCollection deviceCollection; final Collection collection; const GalleryAppBarWidget( @@ -33,7 +35,7 @@ class GalleryAppBarWidget extends StatefulWidget { this.title, this.selectedFiles, { Key key, - this.path, + this.deviceCollection, this.collection, }) : super(key: key); @@ -127,8 +129,7 @@ class _GalleryAppBarWidgetState extends State { final List actions = []; if (Configuration.instance.hasConfiguredAccount() && widget.selectedFiles.files.isEmpty && - (widget.type == GalleryType.localFolder || - widget.type == GalleryType.ownedCollection)) { + widget.type == GalleryType.ownedCollection) { actions.add( Tooltip( message: "Share", @@ -162,7 +163,7 @@ class _GalleryAppBarWidgetState extends State { PopupMenuButton( itemBuilder: (context) { final List items = []; - if (widget.collection.type == CollectionType.album) { + if (widget.collection.type != CollectionType.favorites) { items.add( PopupMenuItem( value: 1, @@ -172,7 +173,7 @@ class _GalleryAppBarWidgetState extends State { Padding( padding: EdgeInsets.all(8), ), - Text("Rename"), + Text("Rename album"), ], ), ), @@ -188,25 +189,42 @@ class _GalleryAppBarWidgetState extends State { const Padding( padding: EdgeInsets.all(8), ), - Text(isArchived ? "Unhide" : "Hide"), + Text(isArchived ? "Unhide album" : "Hide album"), ], ), ), ); + if (widget.collection.type != CollectionType.favorites) { + items.add( + PopupMenuItem( + value: 3, + child: Row( + children: const [ + Icon(Icons.delete_outline), + Padding( + padding: EdgeInsets.all(8), + ), + Text("Delete album"), + ], + ), + ), + ); + } return items; }, onSelected: (value) async { if (value == 1) { await _renameAlbum(context); - } - if (value == 2) { + } else if (value == 2) { await changeCollectionVisibility( context, widget.collection, widget.collection.isArchived() - ? kVisibilityVisible - : kVisibilityArchive, + ? visibilityVisible + : visibilityArchive, ); + } else if (value == 3) { + await _trashCollection(); } }, ), @@ -215,20 +233,47 @@ class _GalleryAppBarWidgetState extends State { return actions; } + Future _trashCollection() async { + final result = await showChoiceDialog( + context, + "Delete album?", + "Files that are unique to this album " + "will be moved to trash, and this album will be deleted.", + firstAction: "Cancel", + secondAction: "Delete album", + secondActionColor: Colors.red, + ); + if (result != DialogUserChoice.secondChoice) { + return; + } + final dialog = createProgressDialog( + context, + "Please wait, deleting album", + ); + await dialog.show(); + try { + await CollectionsService.instance.trashCollection(widget.collection); + + showShortToast(context, "Successfully deleted album"); + await dialog.hide(); + Navigator.of(context).pop(); + } catch (e, s) { + _logger.severe("failed to trash collection", e, s); + await dialog.hide(); + showGenericErrorDialog(context); + rethrow; + } + } + Future _showShareCollectionDialog() async { var collection = widget.collection; final dialog = createProgressDialog(context, "Please wait..."); await dialog.show(); try { - if (collection == null) { - if (widget.type == GalleryType.localFolder) { - collection = - await CollectionsService.instance.getOrCreateForPath(widget.path); - } else { - throw Exception( - "Cannot create a collection of type" + widget.type.toString(), - ); - } + if (collection == null || widget.type != GalleryType.ownedCollection) { + throw Exception( + "Cannot share empty collection of type ${widget.type}", + ); } else { final sharees = await CollectionsService.instance.getSharees(collection.id); @@ -238,7 +283,9 @@ class _GalleryAppBarWidgetState extends State { return showDialog( context: context, builder: (BuildContext context) { - return SharingDialog(collection); + return SharingDialog( + collection, + ); }, ); } catch (e, s) { diff --git a/lib/ui/viewer/gallery/gallery_footer_widget.dart b/lib/ui/viewer/gallery/gallery_footer_widget.dart index d2d313cbe..68064be63 100644 --- a/lib/ui/viewer/gallery/gallery_footer_widget.dart +++ b/lib/ui/viewer/gallery/gallery_footer_widget.dart @@ -27,7 +27,6 @@ class GalleryFooterWidget extends StatelessWidget { ); } }, - paddingValue: 6, text: "Preserve more", iconData: Icons.cloud_upload_outlined, ), diff --git a/lib/ui/viewer/gallery/gallery_overlay_widget.dart b/lib/ui/viewer/gallery/gallery_overlay_widget.dart index 7f211d7e9..3bd841236 100644 --- a/lib/ui/viewer/gallery/gallery_overlay_widget.dart +++ b/lib/ui/viewer/gallery/gallery_overlay_widget.dart @@ -267,8 +267,7 @@ class _OverlayWidgetState extends State { String msg = "Add"; IconData iconData = Platform.isAndroid ? Icons.add : CupertinoIcons.add; // show upload icon instead of add for files selected in local gallery - if (widget.type == GalleryType.localFolder || - widget.type == GalleryType.localAll) { + if (widget.type == GalleryType.localFolder) { msg = "Upload"; iconData = Icons.cloud_upload_outlined; } @@ -321,7 +320,6 @@ class _OverlayWidgetState extends State { if (widget.type == GalleryType.homepage || widget.type == GalleryType.archive || widget.type == GalleryType.localFolder || - widget.type == GalleryType.localAll || widget.type == GalleryType.searchResults) { actions.add( Tooltip( @@ -384,7 +382,7 @@ class _OverlayWidgetState extends State { onPressed: () { _handleVisibilityChangeRequest( context, - showArchive ? kVisibilityArchive : kVisibilityVisible, + showArchive ? visibilityArchive : visibilityVisible, ); }, ), diff --git a/lib/ui/viewer/search/collections/files_from_holiday_page.dart b/lib/ui/viewer/search/collections/files_from_holiday_page.dart deleted file mode 100644 index d3f95837c..000000000 --- a/lib/ui/viewer/search/collections/files_from_holiday_page.dart +++ /dev/null @@ -1,77 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/events/files_updated_event.dart'; -import 'package:photos/events/local_photos_updated_event.dart'; -import 'package:photos/models/file_load_result.dart'; -import 'package:photos/models/gallery_type.dart'; -import 'package:photos/models/search/holiday_search_result.dart'; -import 'package:photos/models/selected_files.dart'; -import 'package:photos/ui/viewer/gallery/gallery.dart'; -import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; -import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; - -class FilesFromHolidayPage extends StatelessWidget { - final HolidaySearchResult holidaySearchResult; - final String tagPrefix; - - final _selectedFiles = SelectedFiles(); - static const GalleryType appBarType = GalleryType.searchResults; - static const GalleryType overlayType = GalleryType.searchResults; - - FilesFromHolidayPage( - this.holidaySearchResult, - this.tagPrefix, { - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final gallery = Gallery( - asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - final result = holidaySearchResult.files - .where( - (file) => - file.creationTime >= creationStartTime && - file.creationTime <= creationEndTime, - ) - .toList(); - return Future.value( - FileLoadResult( - result, - result.length < holidaySearchResult.files.length, - ), - ); - }, - reloadEvent: Bus.instance.on(), - removalEventTypes: const { - EventType.deletedFromRemote, - EventType.deletedFromEverywhere, - }, - tagPrefix: tagPrefix, - selectedFiles: _selectedFiles, - initialFiles: [holidaySearchResult.files[0]], - ); - return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: GalleryAppBarWidget( - appBarType, - holidaySearchResult.holidayName, - _selectedFiles, - ), - ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - GalleryOverlayWidget( - overlayType, - _selectedFiles, - ) - ], - ), - ); - } -} diff --git a/lib/ui/viewer/search/collections/files_from_year_page.dart b/lib/ui/viewer/search/collections/files_from_year_page.dart deleted file mode 100644 index 4faccf57b..000000000 --- a/lib/ui/viewer/search/collections/files_from_year_page.dart +++ /dev/null @@ -1,77 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/events/files_updated_event.dart'; -import 'package:photos/events/local_photos_updated_event.dart'; -import 'package:photos/models/file_load_result.dart'; -import 'package:photos/models/gallery_type.dart'; -import 'package:photos/models/search/year_search_result.dart'; -import 'package:photos/models/selected_files.dart'; -import 'package:photos/ui/viewer/gallery/gallery.dart'; -import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; -import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; - -class FilesFromYearPage extends StatelessWidget { - final YearSearchResult yearSearchResult; - final String tagPrefix; - - final _selectedFiles = SelectedFiles(); - static const GalleryType appBarType = GalleryType.searchResults; - static const GalleryType overlayType = GalleryType.searchResults; - - FilesFromYearPage( - this.yearSearchResult, - this.tagPrefix, { - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final gallery = Gallery( - asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - final result = yearSearchResult.files - .where( - (file) => - file.creationTime >= creationStartTime && - file.creationTime <= creationEndTime, - ) - .toList(); - return Future.value( - FileLoadResult( - result, - result.length < yearSearchResult.files.length, - ), - ); - }, - reloadEvent: Bus.instance.on(), - removalEventTypes: const { - EventType.deletedFromRemote, - EventType.deletedFromEverywhere, - }, - tagPrefix: tagPrefix, - selectedFiles: _selectedFiles, - initialFiles: [yearSearchResult.files[0]], - ); - return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: GalleryAppBarWidget( - appBarType, - yearSearchResult.year.toString(), - _selectedFiles, - ), - ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - GalleryOverlayWidget( - overlayType, - _selectedFiles, - ) - ], - ), - ); - } -} diff --git a/lib/ui/viewer/search/collections/files_in_location_page.dart b/lib/ui/viewer/search/collections/files_in_location_page.dart deleted file mode 100644 index f39391bc1..000000000 --- a/lib/ui/viewer/search/collections/files_in_location_page.dart +++ /dev/null @@ -1,77 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/events/files_updated_event.dart'; -import 'package:photos/events/local_photos_updated_event.dart'; -import 'package:photos/models/file_load_result.dart'; -import 'package:photos/models/gallery_type.dart'; -import 'package:photos/models/search/location_search_result.dart'; -import 'package:photos/models/selected_files.dart'; -import 'package:photos/ui/viewer/gallery/gallery.dart'; -import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; -import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; - -class FilesInLocationPage extends StatelessWidget { - final LocationSearchResult locationSearchResult; - final String tagPrefix; - - final _selectedFiles = SelectedFiles(); - static const GalleryType appBarType = GalleryType.searchResults; - static const GalleryType overlayType = GalleryType.searchResults; - - FilesInLocationPage( - this.locationSearchResult, - this.tagPrefix, { - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final gallery = Gallery( - asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - final result = locationSearchResult.files - .where( - (file) => - file.creationTime >= creationStartTime && - file.creationTime <= creationEndTime, - ) - .toList(); - return Future.value( - FileLoadResult( - result, - result.length < locationSearchResult.files.length, - ), - ); - }, - reloadEvent: Bus.instance.on(), - removalEventTypes: const { - EventType.deletedFromRemote, - EventType.deletedFromEverywhere, - }, - tagPrefix: tagPrefix, - selectedFiles: _selectedFiles, - initialFiles: [locationSearchResult.files[0]], - ); - return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: GalleryAppBarWidget( - appBarType, - locationSearchResult.location, - _selectedFiles, - ), - ), - body: Stack( - alignment: Alignment.bottomCenter, - children: [ - gallery, - GalleryOverlayWidget( - overlayType, - _selectedFiles, - ) - ], - ), - ); - } -} diff --git a/lib/ui/viewer/search/search_result_widgets/file_result_widget.dart b/lib/ui/viewer/search/result/file_result_widget.dart similarity index 94% rename from lib/ui/viewer/search/search_result_widgets/file_result_widget.dart rename to lib/ui/viewer/search/result/file_result_widget.dart index bf03d0a4c..94dda7b70 100644 --- a/lib/ui/viewer/search/search_result_widgets/file_result_widget.dart +++ b/lib/ui/viewer/search/result/file_result_widget.dart @@ -5,11 +5,12 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/file.dart'; import 'package:photos/models/search/file_search_result.dart'; import 'package:photos/ui/viewer/file/detail_page.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart'; +import 'package:photos/ui/viewer/search/result/search_thumbnail_widget.dart'; import 'package:photos/utils/navigation_util.dart'; class FileSearchResultWidget extends StatelessWidget { final FileSearchResult matchedFile; + const FileSearchResultWidget(this.matchedFile, {Key key}) : super(key: key); @override @@ -24,7 +25,7 @@ class FileSearchResultWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SearchResultThumbnailWidget( + SearchThumbnailWidget( matchedFile.file, "file_details", ), diff --git a/lib/ui/viewer/search/search_result_widgets/no_result_widget.dart b/lib/ui/viewer/search/result/no_result_widget.dart similarity index 95% rename from lib/ui/viewer/search/search_result_widgets/no_result_widget.dart rename to lib/ui/viewer/search/result/no_result_widget.dart index 2b8e57e8e..25a0a379d 100644 --- a/lib/ui/viewer/search/search_result_widgets/no_result_widget.dart +++ b/lib/ui/viewer/search/result/no_result_widget.dart @@ -57,10 +57,11 @@ class NoResultWidget extends StatelessWidget { Container( margin: const EdgeInsets.only(bottom: 20, top: 12), child: Text( - '''\u2022 Places (e.g. "London") + '''\u2022 Album names (e.g. "Camera") +\u2022 Types of files (e.g. "Videos", ".gif") \u2022 Years and months (e.g. "2022", "January") \u2022 Holidays (e.g. "Christmas") -\u2022 Album names (e.g. "Recents")''', +''', style: TextStyle( fontSize: 14, color: Theme.of(context) diff --git a/lib/ui/viewer/search/collections/files_from_month_page.dart b/lib/ui/viewer/search/result/search_result_page.dart similarity index 79% rename from lib/ui/viewer/search/collections/files_from_month_page.dart rename to lib/ui/viewer/search/result/search_result_page.dart index d55e3de71..102f745d8 100644 --- a/lib/ui/viewer/search/collections/files_from_month_page.dart +++ b/lib/ui/viewer/search/result/search_result_page.dart @@ -4,33 +4,33 @@ import 'package:flutter/material.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/files_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; +import 'package:photos/models/file.dart'; import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/gallery_type.dart'; -import 'package:photos/models/search/month_search_result.dart'; +import 'package:photos/models/search/search_result.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; -class FilesFromMonthPage extends StatelessWidget { - final MonthSearchResult monthSearchResult; - final String tagPrefix; +class SearchResultPage extends StatelessWidget { + final SearchResult searchResult; final _selectedFiles = SelectedFiles(); static const GalleryType appBarType = GalleryType.searchResults; static const GalleryType overlayType = GalleryType.searchResults; - FilesFromMonthPage( - this.monthSearchResult, - this.tagPrefix, { + SearchResultPage( + this.searchResult, { Key key, }) : super(key: key); @override Widget build(BuildContext context) { + final List files = searchResult.resultFiles(); final gallery = Gallery( asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) { - final result = monthSearchResult.files + final result = files .where( (file) => file.creationTime >= creationStartTime && @@ -40,7 +40,7 @@ class FilesFromMonthPage extends StatelessWidget { return Future.value( FileLoadResult( result, - result.length < monthSearchResult.files.length, + result.length < files.length, ), ); }, @@ -49,16 +49,16 @@ class FilesFromMonthPage extends StatelessWidget { EventType.deletedFromRemote, EventType.deletedFromEverywhere, }, - tagPrefix: tagPrefix, + tagPrefix: searchResult.heroTag(), selectedFiles: _selectedFiles, - initialFiles: [monthSearchResult.files[0]], + initialFiles: [searchResult.previewThumbnail()], ); return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(50.0), child: GalleryAppBarWidget( appBarType, - monthSearchResult.month, + searchResult.name(), _selectedFiles, ), ), diff --git a/lib/ui/viewer/search/search_result_widgets/collection_result_widget.dart b/lib/ui/viewer/search/result/search_result_widget.dart similarity index 58% rename from lib/ui/viewer/search/search_result_widgets/collection_result_widget.dart rename to lib/ui/viewer/search/result/search_result_widget.dart index b135bc183..7588975f0 100644 --- a/lib/ui/viewer/search/search_result_widgets/collection_result_widget.dart +++ b/lib/ui/viewer/search/result/search_result_widget.dart @@ -1,21 +1,26 @@ -// @dart=2.9 - import 'package:flutter/material.dart'; -import 'package:photos/db/files_db.dart'; import 'package:photos/ente_theme_data.dart'; -import 'package:photos/models/search/album_search_result.dart'; -import 'package:photos/ui/viewer/gallery/collection_page.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart'; +import 'package:photos/models/search/search_result.dart'; +import 'package:photos/ui/viewer/search/result/search_result_page.dart'; +import 'package:photos/ui/viewer/search/result/search_thumbnail_widget.dart'; import 'package:photos/utils/navigation_util.dart'; -class AlbumSearchResultWidget extends StatelessWidget { - final AlbumSearchResult albumSearchResult; +class SearchResultWidget extends StatelessWidget { + final SearchResult searchResult; + final Future? resultCount; + final Function? onResultTap; - const AlbumSearchResultWidget(this.albumSearchResult, {Key key}) - : super(key: key); + const SearchResultWidget( + this.searchResult, { + Key? key, + this.resultCount, + this.onResultTap, + }) : super(key: key); @override Widget build(BuildContext context) { + final heroTagPrefix = searchResult.heroTag(); + return GestureDetector( behavior: HitTestBehavior.opaque, child: Container( @@ -23,19 +28,19 @@ class AlbumSearchResultWidget extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), child: Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SearchResultThumbnailWidget( - albumSearchResult.collectionWithThumbnail.thumbnail, - "collection_search", + SearchThumbnailWidget( + searchResult.previewThumbnail(), + heroTagPrefix, ), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Album', + _resultTypeName(searchResult.type()), style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.subTextColor, @@ -45,18 +50,17 @@ class AlbumSearchResultWidget extends StatelessWidget { SizedBox( width: 220, child: Text( - albumSearchResult.collectionWithThumbnail.collection.name, + searchResult.name(), style: const TextStyle(fontSize: 18), overflow: TextOverflow.ellipsis, ), ), const SizedBox(height: 2), FutureBuilder( - future: FilesDB.instance.collectionFileCount( - albumSearchResult.collectionWithThumbnail.collection.id, - ), + future: resultCount ?? + Future.value(searchResult.resultFiles().length), builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data > 0) { + if (snapshot.hasData && snapshot.data! > 0) { final noOfMemories = snapshot.data; return RichText( text: TextSpan( @@ -78,7 +82,7 @@ class AlbumSearchResultWidget extends StatelessWidget { return const SizedBox.shrink(); } }, - ), + ) ], ), const Spacer(), @@ -91,14 +95,38 @@ class AlbumSearchResultWidget extends StatelessWidget { ), ), onTap: () { - routeToPage( - context, - CollectionPage( - albumSearchResult.collectionWithThumbnail, - tagPrefix: "collection_search", - ), - ); + if (onResultTap != null) { + onResultTap!(); + } else { + routeToPage( + context, + SearchResultPage(searchResult), + ); + } }, ); } + + String _resultTypeName(ResultType type) { + switch (type) { + case ResultType.collection: + return "Album"; + case ResultType.year: + return "Year"; + case ResultType.month: + return "Month"; + case ResultType.file: + return "Memory"; + case ResultType.event: + return "Day"; + case ResultType.location: + return "Location"; + case ResultType.fileType: + return "Type"; + case ResultType.fileExtension: + return "File Extension"; + default: + return type.name.toUpperCase(); + } + } } diff --git a/lib/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart b/lib/ui/viewer/search/result/search_thumbnail_widget.dart similarity index 81% rename from lib/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart rename to lib/ui/viewer/search/result/search_thumbnail_widget.dart index 1dccc7e86..54829e49b 100644 --- a/lib/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart +++ b/lib/ui/viewer/search/result/search_thumbnail_widget.dart @@ -4,11 +4,11 @@ import 'package:flutter/widgets.dart'; import 'package:photos/models/file.dart'; import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; -class SearchResultThumbnailWidget extends StatelessWidget { +class SearchThumbnailWidget extends StatelessWidget { final File file; final String tagPrefix; - const SearchResultThumbnailWidget( + const SearchThumbnailWidget( this.file, this.tagPrefix, { Key key, @@ -17,7 +17,7 @@ class SearchResultThumbnailWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Hero( - tag: tagPrefix + file.tag(), + tag: tagPrefix + file.tag, child: SizedBox( height: 58, width: 58, diff --git a/lib/ui/viewer/search/search_result_widgets/holiday_result_widget.dart b/lib/ui/viewer/search/search_result_widgets/holiday_result_widget.dart deleted file mode 100644 index 8347a63f9..000000000 --- a/lib/ui/viewer/search/search_result_widgets/holiday_result_widget.dart +++ /dev/null @@ -1,91 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; -import 'package:photos/models/search/holiday_search_result.dart'; -import 'package:photos/ui/viewer/search/collections/files_from_holiday_page.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart'; -import 'package:photos/utils/navigation_util.dart'; - -class HolidaySearchResultWidget extends StatelessWidget { - static const String _tagPrefix = "holiday_search"; - - final HolidaySearchResult holidaySearchResult; - const HolidaySearchResultWidget(this.holidaySearchResult, {Key key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - final noOfMemories = holidaySearchResult.files.length; - final heroTagPrefix = _tagPrefix + holidaySearchResult.holidayName; - - return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - color: Theme.of(context).colorScheme.searchResultsColor, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SearchResultThumbnailWidget( - holidaySearchResult.files[0], - heroTagPrefix, - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Date', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.subTextColor, - ), - ), - const SizedBox(height: 6), - SizedBox( - width: 220, - child: Text( - holidaySearchResult.holidayName, - style: const TextStyle(fontSize: 18), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(height: 2), - RichText( - text: TextSpan( - style: TextStyle( - color: Theme.of(context) - .colorScheme - .searchResultsCountTextColor, - ), - children: [ - TextSpan(text: noOfMemories.toString()), - TextSpan( - text: noOfMemories != 1 ? ' memories' : ' memory', - ), - ], - ), - ), - ], - ), - const Spacer(), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.subTextColor, - ), - ], - ), - ), - ), - onTap: () { - routeToPage( - context, - FilesFromHolidayPage(holidaySearchResult, heroTagPrefix), - ); - }, - ); - } -} diff --git a/lib/ui/viewer/search/search_result_widgets/location_result_widget.dart b/lib/ui/viewer/search/search_result_widgets/location_result_widget.dart deleted file mode 100644 index 4ee7d5e7b..000000000 --- a/lib/ui/viewer/search/search_result_widgets/location_result_widget.dart +++ /dev/null @@ -1,91 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; -import 'package:photos/models/search/location_search_result.dart'; -import 'package:photos/ui/viewer/search/collections/files_in_location_page.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart'; -import 'package:photos/utils/navigation_util.dart'; - -class LocationSearchResultWidget extends StatelessWidget { - static const String _tagPrefix = "location_search"; - - final LocationSearchResult locationSearchResult; - const LocationSearchResultWidget(this.locationSearchResult, {Key key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - final noOfMemories = locationSearchResult.files.length; - final heroTagPrefix = _tagPrefix + locationSearchResult.location; - - return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - color: Theme.of(context).colorScheme.searchResultsColor, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SearchResultThumbnailWidget( - locationSearchResult.files[0], - heroTagPrefix, - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Location', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.subTextColor, - ), - ), - const SizedBox(height: 6), - SizedBox( - width: 220, - child: Text( - locationSearchResult.location, - style: const TextStyle(fontSize: 18), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(height: 2), - RichText( - text: TextSpan( - style: TextStyle( - color: Theme.of(context) - .colorScheme - .searchResultsCountTextColor, - ), - children: [ - TextSpan(text: noOfMemories.toString()), - TextSpan( - text: noOfMemories != 1 ? ' memories' : ' memory', - ), - ], - ), - ), - ], - ), - const Spacer(), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.subTextColor, - ), - ], - ), - ), - ), - onTap: () { - routeToPage( - context, - FilesInLocationPage(locationSearchResult, heroTagPrefix), - ); - }, - ); - } -} diff --git a/lib/ui/viewer/search/search_result_widgets/month_result_widget.dart b/lib/ui/viewer/search/search_result_widgets/month_result_widget.dart deleted file mode 100644 index 07ca2f253..000000000 --- a/lib/ui/viewer/search/search_result_widgets/month_result_widget.dart +++ /dev/null @@ -1,91 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; -import 'package:photos/models/search/month_search_result.dart'; -import 'package:photos/ui/viewer/search/collections/files_from_month_page.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart'; -import 'package:photos/utils/navigation_util.dart'; - -class MonthSearchResultWidget extends StatelessWidget { - static const String _tagPrefix = "month_search"; - - final MonthSearchResult monthSearchResult; - const MonthSearchResultWidget(this.monthSearchResult, {Key key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - final noOfMemories = monthSearchResult.files.length; - final heroTagPrefix = _tagPrefix + monthSearchResult.month; - - return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - color: Theme.of(context).colorScheme.searchResultsColor, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SearchResultThumbnailWidget( - monthSearchResult.files[0], - heroTagPrefix, - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Date', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.subTextColor, - ), - ), - const SizedBox(height: 6), - SizedBox( - width: 220, - child: Text( - monthSearchResult.month, - style: const TextStyle(fontSize: 18), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(height: 2), - RichText( - text: TextSpan( - style: TextStyle( - color: Theme.of(context) - .colorScheme - .searchResultsCountTextColor, - ), - children: [ - TextSpan(text: noOfMemories.toString()), - TextSpan( - text: noOfMemories != 1 ? ' memories' : ' memory', - ), - ], - ), - ), - ], - ), - const Spacer(), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.subTextColor, - ), - ], - ), - ), - ), - onTap: () { - routeToPage( - context, - FilesFromMonthPage(monthSearchResult, heroTagPrefix), - ); - }, - ); - } -} diff --git a/lib/ui/viewer/search/search_result_widgets/year_result_widget.dart b/lib/ui/viewer/search/search_result_widgets/year_result_widget.dart deleted file mode 100644 index 687b77519..000000000 --- a/lib/ui/viewer/search/search_result_widgets/year_result_widget.dart +++ /dev/null @@ -1,87 +0,0 @@ -// @dart=2.9 - -import 'package:flutter/material.dart'; -import 'package:photos/ente_theme_data.dart'; -import 'package:photos/models/search/year_search_result.dart'; -import 'package:photos/ui/viewer/search/collections/files_from_year_page.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/search_result_thumbnail_widget.dart'; -import 'package:photos/utils/navigation_util.dart'; - -class YearSearchResultWidget extends StatelessWidget { - static const String _tagPrefix = "year_search"; - - final YearSearchResult yearSearchResult; - const YearSearchResultWidget(this.yearSearchResult, {Key key}) - : super(key: key); - @override - Widget build(BuildContext context) { - final noOfMemories = yearSearchResult.files.length; - final heroTagPrefix = _tagPrefix + yearSearchResult.year; - - return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - color: Theme.of(context).colorScheme.searchResultsColor, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SearchResultThumbnailWidget( - yearSearchResult.files[0], - heroTagPrefix, - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Date', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.subTextColor, - ), - ), - const SizedBox(height: 6), - Text( - yearSearchResult.year, - style: const TextStyle(fontSize: 18), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - RichText( - text: TextSpan( - style: TextStyle( - color: Theme.of(context) - .colorScheme - .searchResultsCountTextColor, - ), - children: [ - TextSpan(text: noOfMemories.toString()), - TextSpan( - text: noOfMemories != 1 ? ' memories' : ' memory', - ), - ], - ), - ), - ], - ), - const Spacer(), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.subTextColor, - ), - ], - ), - ), - ), - onTap: () { - routeToPage( - context, - FilesFromYearPage(yearSearchResult, heroTagPrefix), - ); - }, - ); - } -} diff --git a/lib/ui/viewer/search/search_suggestions.dart b/lib/ui/viewer/search/search_suggestions.dart index 08d1961a9..e19343956 100644 --- a/lib/ui/viewer/search/search_suggestions.dart +++ b/lib/ui/viewer/search/search_suggestions.dart @@ -2,23 +2,20 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:photos/db/files_db.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/search/album_search_result.dart'; import 'package:photos/models/search/file_search_result.dart'; -import 'package:photos/models/search/holiday_search_result.dart'; -import 'package:photos/models/search/location_search_result.dart'; -import 'package:photos/models/search/month_search_result.dart'; -import 'package:photos/models/search/search_results.dart'; -import 'package:photos/models/search/year_search_result.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/collection_result_widget.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/file_result_widget.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/holiday_result_widget.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/location_result_widget.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/month_result_widget.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/year_result_widget.dart'; +import 'package:photos/models/search/generic_search_result.dart'; +import 'package:photos/models/search/search_result.dart'; +import 'package:photos/ui/viewer/gallery/collection_page.dart'; +import 'package:photos/ui/viewer/search/result/file_result_widget.dart'; +import 'package:photos/ui/viewer/search/result/search_result_widget.dart'; +import 'package:photos/utils/navigation_util.dart'; class SearchSuggestionsWidget extends StatelessWidget { final List results; + const SearchSuggestionsWidget( this.results, { Key key, @@ -62,17 +59,24 @@ class SearchSuggestionsWidget extends StatelessWidget { } final result = results[index]; if (result is AlbumSearchResult) { - return AlbumSearchResultWidget(result); - } else if (result is LocationSearchResult) { - return LocationSearchResultWidget(result); + final AlbumSearchResult albumSearchResult = result; + return SearchResultWidget( + result, + resultCount: FilesDB.instance.collectionFileCount( + albumSearchResult.collectionWithThumbnail.collection.id, + ), + onResultTap: () => routeToPage( + context, + CollectionPage( + albumSearchResult.collectionWithThumbnail, + tagPrefix: result.heroTag(), + ), + ), + ); } else if (result is FileSearchResult) { return FileSearchResultWidget(result); - } else if (result is YearSearchResult) { - return YearSearchResultWidget(result); - } else if (result is HolidaySearchResult) { - return HolidaySearchResultWidget(result); - } else if (result is MonthSearchResult) { - return MonthSearchResultWidget(result); + } else if (result is GenericSearchResult) { + return SearchResultWidget(result); } else { Logger('SearchSuggestionsWidget') .info("Invalid/Unsupported value"); diff --git a/lib/ui/viewer/search/search_widget.dart b/lib/ui/viewer/search/search_widget.dart index d360b3562..cc15e6d87 100644 --- a/lib/ui/viewer/search/search_widget.dart +++ b/lib/ui/viewer/search/search_widget.dart @@ -3,10 +3,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; import 'package:photos/ente_theme_data.dart'; -import 'package:photos/models/search/search_results.dart'; +import 'package:photos/models/search/search_result.dart'; +import 'package:photos/services/feature_flag_service.dart'; import 'package:photos/services/search_service.dart'; -import 'package:photos/ui/viewer/search/search_result_widgets/no_result_widget.dart'; +import 'package:photos/ui/viewer/search/result/no_result_widget.dart'; import 'package:photos/ui/viewer/search/search_suffix_icon_widget.dart'; import 'package:photos/ui/viewer/search/search_suggestions.dart'; import 'package:photos/utils/date_time_util.dart'; @@ -47,6 +49,7 @@ class _SearchIconWidgetState extends State { class SearchWidget extends StatefulWidget { const SearchWidget({Key key}) : super(key: key); + @override State createState() => _SearchWidgetState(); } @@ -56,6 +59,7 @@ class _SearchWidgetState extends State { final List _results = []; final _searchService = SearchService.instance; final _debouncer = Debouncer(const Duration(milliseconds: 100)); + final Logger _logger = Logger((_SearchWidgetState).toString()); @override Widget build(BuildContext context) { @@ -83,7 +87,7 @@ class _SearchWidgetState extends State { keyboardType: TextInputType.visiblePassword, // Above parameters are to disable auto-suggestion decoration: InputDecoration( - hintText: "Places, moments, albums...", + hintText: "Albums, months, days, years, ...", filled: true, contentPadding: const EdgeInsets.symmetric( horizontal: 16, @@ -124,7 +128,7 @@ class _SearchWidgetState extends State { _query = value; final List allResults = await getSearchResultsForQuery(value); - /*checking if _query == value to make sure that the results are from the current query + /*checking if _query == value to make sure that the results are from the current query and not from the previous query (race condition).*/ if (mounted && _query == value) { setState(() { @@ -177,24 +181,43 @@ class _SearchWidgetState extends State { completer.complete(allResults); return; } - if (_isYearValid(query)) { - final yearResults = await _searchService.getYearSearchResults(query); - allResults.addAll(yearResults); + try { + if (_isYearValid(query)) { + final yearResults = await _searchService.getYearSearchResults(query); + allResults.addAll(yearResults); + } + + final holidayResults = + await _searchService.getHolidaySearchResults(query); + allResults.addAll(holidayResults); + + final fileTypeSearchResults = + await _searchService.getFileTypeResults(query); + allResults.addAll(fileTypeSearchResults); + + final fileExtnResult = + await _searchService.getFileExtensionResults(query); + allResults.addAll(fileExtnResult); + + final collectionResults = + await _searchService.getCollectionSearchResults(query); + allResults.addAll(collectionResults); + + if (FeatureFlagService.instance.isInternalUserOrDebugBuild() && + query.startsWith("l:")) { + final locationResults = await _searchService + .getLocationSearchResults(query.replaceAll("l:", "")); + allResults.addAll(locationResults); + } + + final monthResults = await _searchService.getMonthSearchResults(query); + allResults.addAll(monthResults); + + final possibleEvents = await _searchService.getDateResults(query); + allResults.addAll(possibleEvents); + } catch (e, s) { + _logger.severe("error during search", e, s); } - - final holidayResults = await _searchService.getHolidaySearchResults(query); - allResults.addAll(holidayResults); - - final collectionResults = - await _searchService.getCollectionSearchResults(query); - allResults.addAll(collectionResults); - - final locationResults = - await _searchService.getLocationSearchResults(query); - allResults.addAll(locationResults); - - final monthResults = await _searchService.getMonthSearchResults(query); - allResults.addAll(monthResults); completer.complete(allResults); } diff --git a/lib/utils/auth_util.dart b/lib/utils/auth_util.dart index 99d392f85..808a88c09 100644 --- a/lib/utils/auth_util.dart +++ b/lib/utils/auth_util.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'package:local_auth/auth_strings.dart'; import 'package:local_auth/local_auth.dart'; import 'package:logging/logging.dart'; diff --git a/lib/utils/crypto_util.dart b/lib/utils/crypto_util.dart index 90f2b3209..57ff6c40d 100644 --- a/lib/utils/crypto_util.dart +++ b/lib/utils/crypto_util.dart @@ -1,11 +1,10 @@ -// @dart=2.9 - import 'dart:io' as io; import 'dart:typed_data'; import 'package:computer/computer.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; +import 'package:photos/models/derived_key_result.dart'; import 'package:photos/models/encryption_result.dart'; const int encryptionChunkSize = 4 * 1024 * 1024; @@ -200,9 +199,10 @@ class CryptoUtil { static Uint8List decryptSync( Uint8List cipher, - Uint8List key, + Uint8List? key, Uint8List nonce, ) { + assert(key != null, "key can not be null"); final args = {}; args["cipher"] = cipher; args["nonce"] = nonce; @@ -235,7 +235,7 @@ class CryptoUtil { static Future encryptFile( String sourceFilePath, String destinationFilePath, { - Uint8List key, + Uint8List? key, }) { final args = {}; args["sourceFilePath"] = sourceFilePath; @@ -330,11 +330,3 @@ class CryptoUtil { ); } } - -class DerivedKeyResult { - final Uint8List key; - final int memLimit; - final int opsLimit; - - DerivedKeyResult(this.key, this.memLimit, this.opsLimit); -} diff --git a/lib/utils/data_util.dart b/lib/utils/data_util.dart index aa202e438..3dcae58fa 100644 --- a/lib/utils/data_util.dart +++ b/lib/utils/data_util.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'dart:math'; double convertBytesToGBs(final int bytes, {int precision = 2}) { @@ -8,15 +6,15 @@ double convertBytesToGBs(final int bytes, {int precision = 2}) { ); } -final kStorageUnits = ["bytes", "KB", "MB", "GB"]; +final storageUnits = ["bytes", "KB", "MB", "GB"]; String convertBytesToReadableFormat(int bytes) { int storageUnitIndex = 0; - while (bytes >= 1024 && storageUnitIndex < kStorageUnits.length - 1) { + while (bytes >= 1024 && storageUnitIndex < storageUnits.length - 1) { storageUnitIndex++; bytes = (bytes / 1024).round(); } - return bytes.toString() + " " + kStorageUnits[storageUnitIndex]; + return bytes.toString() + " " + storageUnits[storageUnitIndex]; } String formatBytes(int bytes, [int decimals = 2]) { @@ -24,5 +22,5 @@ String formatBytes(int bytes, [int decimals = 2]) { const k = 1024; final int dm = decimals < 0 ? 0 : decimals; final int i = (log(bytes) / log(k)).floor(); - return ((bytes / pow(k, i)).toStringAsFixed(dm)) + ' ' + kStorageUnits[i]; + return ((bytes / pow(k, i)).toStringAsFixed(dm)) + ' ' + storageUnits[i]; } diff --git a/lib/utils/date_time_util.dart b/lib/utils/date_time_util.dart index 6da8645ba..13688ebd9 100644 --- a/lib/utils/date_time_util.dart +++ b/lib/utils/date_time_util.dart @@ -1,8 +1,8 @@ -// @dart=2.9 - import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +const Set monthWith31Days = {1, 3, 5, 7, 8, 10, 12}; +const Set monthWith30Days = {4, 6, 9, 11}; Map _months = { 1: "Jan", 2: "Feb", @@ -44,40 +44,41 @@ Map _days = { }; final currentYear = int.parse(DateTime.now().year.toString()); +const searchStartYear = 1970; //Jun 2022 String getMonthAndYear(DateTime dateTime) { - return _months[dateTime.month] + " " + dateTime.year.toString(); + return _months[dateTime.month]! + " " + dateTime.year.toString(); } //Thu, 30 Jun String getDayAndMonth(DateTime dateTime) { - return _days[dateTime.weekday] + + return _days[dateTime.weekday]! + ", " + dateTime.day.toString() + " " + - _months[dateTime.month]; + _months[dateTime.month]!; } //30 Jun, 2022 String getDateAndMonthAndYear(DateTime dateTime) { return dateTime.day.toString() + " " + - _months[dateTime.month] + + _months[dateTime.month]! + ", " + dateTime.year.toString(); } String getDay(DateTime dateTime) { - return _days[dateTime.weekday]; + return _days[dateTime.weekday]!; } String getMonth(DateTime dateTime) { - return _months[dateTime.month]; + return _months[dateTime.month]!; } String getFullMonth(DateTime dateTime) { - return _fullMonths[dateTime.month]; + return _fullMonths[dateTime.month]!; } String getAbbreviationOfYear(DateTime dateTime) { @@ -201,7 +202,7 @@ Widget getDayWidget( getDayTitle(timestamp), style: (getDayTitle(timestamp) == "Today" && !smallerTodayFont) ? Theme.of(context).textTheme.headline5 - : Theme.of(context).textTheme.caption.copyWith( + : Theme.of(context).textTheme.caption?.copyWith( fontSize: 16, fontWeight: FontWeight.w600, fontFamily: 'Inter-SemiBold', @@ -232,7 +233,8 @@ String secondsToHHMMSS(int value) { h = value ~/ 3600; m = ((value - h * 3600)) ~/ 60; s = value - (h * 3600) - (m * 60); - final String hourLeft = h.toString().length < 2 ? "0" + h.toString() : h.toString(); + final String hourLeft = + h.toString().length < 2 ? "0" + h.toString() : h.toString(); final String minuteLeft = m.toString().length < 2 ? "0" + m.toString() : m.toString(); @@ -244,3 +246,25 @@ String secondsToHHMMSS(int value) { return result; } + +bool isValidDate({ + required int day, + required int month, + required int year, +}) { + if (day < 0 || day > 31 || month < 0 || month > 12 || year < 0) { + return false; + } + if (monthWith30Days.contains(month) && day > 30) { + return false; + } + if (month == 2) { + if (day > 29) { + return false; + } + if (day == 29 && year % 4 != 0) { + return false; + } + } + return true; +} diff --git a/lib/utils/debouncer.dart b/lib/utils/debouncer.dart index 8b1eafe85..fe1d300cb 100644 --- a/lib/utils/debouncer.dart +++ b/lib/utils/debouncer.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'dart:async'; import 'package:flutter/material.dart'; @@ -7,13 +5,13 @@ import 'package:flutter/material.dart'; class Debouncer { final Duration _duration; final ValueNotifier _debounceActiveNotifier = ValueNotifier(false); - Timer _debounceTimer; + Timer? _debounceTimer; Debouncer(this._duration); void run(Future Function() fn) { if (isActive()) { - _debounceTimer.cancel(); + _debounceTimer!.cancel(); } _debounceTimer = Timer(_duration, () async { await fn(); @@ -24,11 +22,11 @@ class Debouncer { void cancelDebounce() { if (_debounceTimer != null) { - _debounceTimer.cancel(); + _debounceTimer!.cancel(); } } - bool isActive() => _debounceTimer != null && _debounceTimer.isActive; + bool isActive() => _debounceTimer != null && _debounceTimer!.isActive; ValueNotifier get debounceActiveNotifier { return _debounceActiveNotifier; diff --git a/lib/utils/delete_file_util.dart b/lib/utils/delete_file_util.dart index 3e186af9b..8c20cc06b 100644 --- a/lib/utils/delete_file_util.dart +++ b/lib/utils/delete_file_util.dart @@ -34,7 +34,7 @@ Future deleteFilesFromEverywhere( ) async { final dialog = createProgressDialog(context, "Deleting..."); await dialog.show(); - _logger.info("Trying to delete files " + files.toString()); + _logger.info("Trying to deleteFilesFromEverywhere " + files.toString()); final List localAssetIDs = []; final List localSharedMediaIDs = []; final List alreadyDeletedIDs = []; // to ignore already deleted files @@ -44,7 +44,7 @@ Future deleteFilesFromEverywhere( if (!(await _localFileExist(file))) { _logger.warning("Already deleted " + file.toString()); alreadyDeletedIDs.add(file.localID); - } else if (file.isSharedMediaToAppSandbox()) { + } else if (file.isSharedMediaToAppSandbox) { localSharedMediaIDs.add(file.localID); } else { localAssetIDs.add(file.localID); @@ -151,7 +151,8 @@ Future deleteFilesFromRemoteOnly( final dialog = createProgressDialog(context, "Deleting..."); await dialog.show(); _logger.info( - "Trying to delete files " + files.map((f) => f.uploadedFileID).toString(), + "Trying to deleteFilesFromRemoteOnly " + + files.map((f) => f.uploadedFileID).toString(), ); final updatedCollectionIDs = {}; final List uploadedFileIDs = []; @@ -192,7 +193,7 @@ Future deleteFilesOnDeviceOnly( ) async { final dialog = createProgressDialog(context, "Deleting..."); await dialog.show(); - _logger.info("Trying to delete files " + files.toString()); + _logger.info("Trying to deleteFilesOnDeviceOnly" + files.toString()); final List localAssetIDs = []; final List localSharedMediaIDs = []; final List alreadyDeletedIDs = []; // to ignore already deleted files @@ -202,7 +203,7 @@ Future deleteFilesOnDeviceOnly( if (!(await _localFileExist(file))) { _logger.warning("Already deleted " + file.toString()); alreadyDeletedIDs.add(file.localID); - } else if (file.isSharedMediaToAppSandbox()) { + } else if (file.isSharedMediaToAppSandbox) { localSharedMediaIDs.add(file.localID); } else { localAssetIDs.add(file.localID); @@ -310,8 +311,8 @@ Future deleteLocalFiles( final List localAssetIDs = []; final List localSharedMediaIDs = []; for (String id in localIDs) { - if (id.startsWith(kOldSharedMediaIdentifier) || - id.startsWith(kSharedMediaIdentifier)) { + if (id.startsWith(oldSharedMediaIdentifier) || + id.startsWith(sharedMediaIdentifier)) { localSharedMediaIDs.add(id); } else { localAssetIDs.add(id); @@ -321,7 +322,7 @@ Future deleteLocalFiles( if (Platform.isAndroid) { final androidInfo = await DeviceInfoPlugin().androidInfo; - if (androidInfo.version.sdkInt < kAndroid11SDKINT) { + if (androidInfo.version.sdkInt < android11SDKINT) { deletedIDs .addAll(await _deleteLocalFilesInBatches(context, localAssetIDs)); } else { @@ -419,11 +420,11 @@ Future> _deleteLocalFilesInBatches( } Future _localFileExist(File file) { - if (file.isSharedMediaToAppSandbox()) { + if (file.isSharedMediaToAppSandbox) { final localFile = io.File(getSharedMediaFilePath(file)); return localFile.exists(); } else { - return file.getAsset().then((asset) { + return file.getAsset.then((asset) { if (asset == null) { return false; } diff --git a/lib/utils/diff_fetcher.dart b/lib/utils/diff_fetcher.dart index ba4449cfd..4953ed4c1 100644 --- a/lib/utils/diff_fetcher.dart +++ b/lib/utils/diff_fetcher.dart @@ -71,6 +71,9 @@ class DiffFetcher { file.thumbnailDecryptionHeader = item["thumbnail"]["decryptionHeader"]; file.metadataDecryptionHeader = item["metadata"]["decryptionHeader"]; + if (item["info"] != null) { + file.fileSize = item["info"]["fileSize"]; + } final fileDecryptionKey = decryptFileKey(file); final encodedMetadata = await CryptoUtil.decryptChaCha( diff --git a/lib/utils/exif_util.dart b/lib/utils/exif_util.dart index 3c1914088..dd1eff63e 100644 --- a/lib/utils/exif_util.dart +++ b/lib/utils/exif_util.dart @@ -19,7 +19,7 @@ Future> getExif(File file) async { try { final originFile = await getFile(file, isOrigin: true); final exif = await readExifFromFile(originFile); - if (!file.isRemoteFile() && io.Platform.isIOS) { + if (!file.isRemoteFile && io.Platform.isIOS) { await originFile.delete(); } return exif; diff --git a/lib/utils/file_download_util.dart b/lib/utils/file_download_util.dart index 0affb1709..2cdfd9e2d 100644 --- a/lib/utils/file_download_util.dart +++ b/lib/utils/file_download_util.dart @@ -27,7 +27,7 @@ Future downloadAndDecrypt( return Network.instance .getDio() .download( - file.getDownloadUrl(), + file.downloadUrl, encryptedFilePath, options: Options( headers: {"X-Auth-Token": Configuration.instance.getToken()}, diff --git a/lib/utils/file_sync_util.dart b/lib/utils/file_sync_util.dart deleted file mode 100644 index 65ccbcf90..000000000 --- a/lib/utils/file_sync_util.dart +++ /dev/null @@ -1,238 +0,0 @@ -// @dart=2.9 - -import 'dart:math'; - -import 'package:computer/computer.dart'; -import 'package:logging/logging.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:photos/models/file.dart'; - -final _logger = Logger("FileSyncUtil"); -const ignoreSizeConstraint = SizeConstraint(ignoreSize: true); -const assetFetchPageSize = 2000; -Future> getDeviceFiles( - int fromTime, - int toTime, - Computer computer, -) async { - final pathEntities = await _getGalleryList(fromTime, toTime); - List files = []; - AssetPathEntity recents; - for (AssetPathEntity pathEntity in pathEntities) { - if (pathEntity.name == "Recent" || pathEntity.name == "Recents") { - recents = pathEntity; - } else { - files = await _computeFiles(pathEntity, fromTime, files, computer); - } - } - if (recents != null) { - files = await _computeFiles(recents, fromTime, files, computer); - } - files.sort( - (first, second) => first.creationTime.compareTo(second.creationTime), - ); - return files; -} - -Future> 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 assets = []; - for (final assetPath in assetPaths) { - for (final asset in await _getAllAssetLists(assetPath)) { - assets.add(LocalAsset(asset.id, assetPath.name)); - } - } - return assets; -} - -Future> getUnsyncedFiles( - List assets, - Set existingIDs, - Set invalidIDs, - Computer computer, -) async { - final Map args = {}; - args['assets'] = assets; - args['existingIDs'] = existingIDs; - args['invalidIDs'] = invalidIDs; - final unsyncedAssets = - await computer.compute(_getUnsyncedAssets, param: args); - if (unsyncedAssets.isEmpty) { - return []; - } - return _convertToFiles(unsyncedAssets, computer); -} - -List _getUnsyncedAssets(Map args) { - final List assets = args['assets']; - final Set existingIDs = args['existingIDs']; - final Set invalidIDs = args['invalidIDs']; - final List unsyncedAssets = []; - for (final asset in assets) { - if (!existingIDs.contains(asset.id) && !invalidIDs.contains(asset.id)) { - unsyncedAssets.add(asset); - } - } - return unsyncedAssets; -} - -Future> _convertToFiles( - List assets, - Computer computer, -) async { - final List recents = []; - final List entities = []; - for (final asset in assets) { - if (asset.path == "Recent" || asset.path == "Recents") { - recents.add(asset); - } else { - entities.add( - LocalAssetEntity(await AssetEntity.fromId(asset.id), asset.path), - ); - } - } - // Ignore duplicate items in recents - for (final recent in recents) { - bool presentInOthers = false; - for (final entity in entities) { - if (recent.id == entity.entity.id) { - presentInOthers = true; - break; - } - } - if (!presentInOthers) { - entities.add( - LocalAssetEntity(await AssetEntity.fromId(recent.id), recent.path), - ); - } - } - return await computer.compute(_getFilesFromAssets, param: entities); -} - -Future> _getGalleryList( - final int fromTime, - final int toTime, -) async { - final filterOptionGroup = FilterOptionGroup(); - filterOptionGroup.setOption( - AssetType.image, - const FilterOption(needTitle: true, sizeConstraint: ignoreSizeConstraint), - ); - filterOptionGroup.setOption( - AssetType.video, - const FilterOption(needTitle: true, sizeConstraint: ignoreSizeConstraint), - ); - - filterOptionGroup.updateTimeCond = DateTimeCond( - min: DateTime.fromMicrosecondsSinceEpoch(fromTime), - max: DateTime.fromMicrosecondsSinceEpoch(toTime), - ); - final galleryList = await PhotoManager.getAssetPathList( - hasAll: true, - type: RequestType.common, - filterOption: filterOptionGroup, - ); - - galleryList.sort((s1, s2) { - return s2.assetCount.compareTo(s1.assetCount); - }); - - return galleryList; -} - -Future> _computeFiles( - AssetPathEntity pathEntity, - int fromTime, - List files, - Computer computer, -) async { - final Map args = {}; - args["pathEntity"] = pathEntity; - args["assetList"] = await _getAllAssetLists(pathEntity); - args["fromTime"] = fromTime; - args["files"] = files; - return await computer.compute(_getFiles, param: args); -} - -Future> _getAllAssetLists(AssetPathEntity pathEntity) async { - final List result = []; - int currentPage = 0; - List currentPageResult = []; - do { - currentPageResult = await pathEntity.getAssetListPaged( - page: currentPage, - size: assetFetchPageSize, - ); - result.addAll(currentPageResult); - currentPage = currentPage + 1; - } while (currentPageResult.length >= assetFetchPageSize); - return result; -} - -Future> _getFiles(Map args) async { - final pathEntity = args["pathEntity"]; - final assetList = args["assetList"]; - final fromTime = args["fromTime"]; - final files = args["files"]; - for (AssetEntity entity in assetList) { - if (max( - entity.createDateTime.microsecondsSinceEpoch, - entity.modifiedDateTime.microsecondsSinceEpoch, - ) > - fromTime) { - try { - final file = await File.fromAsset(pathEntity.name, entity); - if (!files.contains(file)) { - files.add(file); - } - } catch (e) { - _logger.severe(e); - } - } - } - return files; -} - -Future> _getFilesFromAssets(List assets) async { - final List files = []; - for (final asset in assets) { - files.add( - await File.fromAsset( - asset.path, - asset.entity, - ), - ); - } - return files; -} - -class LocalAsset { - final String id; - final String path; - - LocalAsset( - this.id, - this.path, - ); -} - -class LocalAssetEntity { - final AssetEntity entity; - final String path; - - LocalAssetEntity(this.entity, this.path); -} diff --git a/lib/utils/file_uploader.dart b/lib/utils/file_uploader.dart index 15279eb5b..8ebc4a42f 100644 --- a/lib/utils/file_uploader.dart +++ b/lib/utils/file_uploader.dart @@ -285,7 +285,7 @@ class FileUploader { fileOnDisk.updationTime != -1 && fileOnDisk.collectionID == collectionID; if (wasAlreadyUploaded) { - debugPrint("File is already uploaded ${fileOnDisk.tag()}"); + debugPrint("File is already uploaded ${fileOnDisk.tag}"); return fileOnDisk; } @@ -520,8 +520,8 @@ class FileUploader { ); if (sameLocalSameCollection != null) { _logger.fine( - "sameLocalSameCollection: \n toUpload ${fileToUpload.tag()} " - "\n existing: ${sameLocalSameCollection.tag()}", + "sameLocalSameCollection: \n toUpload ${fileToUpload.tag} " + "\n existing: ${sameLocalSameCollection.tag}", ); // should delete the fileToUploadEntry await FilesDB.instance.deleteByGeneratedID(fileToUpload.generatedID); @@ -545,8 +545,8 @@ class FileUploader { // update the local id of the existing file and delete the fileToUpload // entry _logger.fine( - "fileMissingLocalButSameCollection: \n toUpload ${fileToUpload.tag()} " - "\n existing: ${fileMissingLocalButSameCollection.tag()}", + "fileMissingLocalButSameCollection: \n toUpload ${fileToUpload.tag} " + "\n existing: ${fileMissingLocalButSameCollection.tag}", ); fileMissingLocalButSameCollection.localID = fileToUpload.localID; // set localID for the given uploadedID across collections @@ -571,9 +571,9 @@ class FileUploader { orElse: () => null, ); if (fileExistsButDifferentCollection != null) { - debugPrint( - "fileExistsButDifferentCollection: \n toUpload ${fileToUpload.tag()} " - "\n existing: ${fileExistsButDifferentCollection.tag()}", + _logger.fine( + "fileExistsButDifferentCollection: \n toUpload ${fileToUpload.tag} " + "\n existing: ${fileExistsButDifferentCollection.tag}", ); final linkedFile = await CollectionsService.instance .linkLocalFileToExistingUploadedFileInAnotherCollection( @@ -609,7 +609,7 @@ class FileUploader { // for upload. Shared Media should only be cleared when the upload // succeeds. if (io.Platform.isIOS || - (uploadCompleted && file.isSharedMediaToAppSandbox())) { + (uploadCompleted && file.isSharedMediaToAppSandbox)) { await mediaUploadData.sourceFile.delete(); } } diff --git a/lib/utils/file_uploader_util.dart b/lib/utils/file_uploader_util.dart index 0d3823ac4..2cf773fd8 100644 --- a/lib/utils/file_uploader_util.dart +++ b/lib/utils/file_uploader_util.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'dart:async'; import 'dart:io' as io; import 'dart:typed_data'; @@ -18,6 +16,7 @@ import 'package:photos/models/file.dart' as ente; import 'package:photos/models/file_type.dart'; import 'package:photos/models/location.dart'; import 'package:photos/utils/crypto_util.dart'; +// ignore: import_of_legacy_library_into_null_safe import 'package:photos/utils/file_util.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; @@ -26,10 +25,10 @@ const kMaximumThumbnailCompressionAttempts = 2; const kLivePhotoHashSeparator = ':'; class MediaUploadData { - final io.File sourceFile; - final Uint8List thumbnail; + final io.File? sourceFile; + final Uint8List? thumbnail; final bool isDeleted; - final FileHashData hashData; + final FileHashData? hashData; MediaUploadData( this.sourceFile, @@ -41,17 +40,17 @@ class MediaUploadData { class FileHashData { // For livePhotos, the fileHash value will be imageHash:videoHash - final String fileHash; + final String? fileHash; // zipHash is used to take care of existing live photo uploads from older // mobile clients - String zipHash; + String? zipHash; FileHashData(this.fileHash, {this.zipHash}); } Future getUploadDataFromEnteFile(ente.File file) async { - if (file.isSharedMediaToAppSandbox()) { + if (file.isSharedMediaToAppSandbox) { return await _getMediaUploadDataFromAppCache(file); } else { return await _getMediaUploadDataFromAssetFile(file); @@ -59,19 +58,19 @@ Future getUploadDataFromEnteFile(ente.File file) async { } Future _getMediaUploadDataFromAssetFile(ente.File file) async { - io.File sourceFile; - Uint8List thumbnailData; + io.File? sourceFile; + Uint8List? thumbnailData; bool isDeleted; - String fileHash, zipHash; + String? zipHash; + String fileHash; // The timeouts are to safeguard against https://github.com/CaiJingLong/flutter_photo_manager/issues/467 - final asset = await file - .getAsset() + final asset = await file.getAsset .timeout(const Duration(seconds: 3)) .catchError((e) async { if (e is TimeoutException) { _logger.info("Asset fetch timed out for " + file.toString()); - return await file.getAsset(); + return await file.getAsset; } else { throw e; } @@ -98,7 +97,7 @@ Future _getMediaUploadDataFromAssetFile(ente.File file) async { fileHash = Sodium.bin2base64(await CryptoUtil.getHash(sourceFile)); if (file.fileType == FileType.livePhoto && io.Platform.isIOS) { - final io.File videoUrl = await Motionphoto.getLivePhotoFile(file.localID); + final io.File? videoUrl = await Motionphoto.getLivePhotoFile(file.localID!); if (videoUrl == null || !videoUrl.existsSync()) { final String errMsg = "missing livePhoto url for ${file.toString()} with subType ${file.fileSubType}"; @@ -128,14 +127,14 @@ Future _getMediaUploadDataFromAssetFile(ente.File file) async { } thumbnailData = await asset.thumbnailDataWithSize( - const ThumbnailSize(kThumbnailLargeSize, kThumbnailLargeSize), - quality: kThumbnailQuality, + const ThumbnailSize(thumbnailLargeSize, thumbnailLargeSize), + quality: thumbnailQuality, ); if (thumbnailData == null) { throw InvalidFileError("unable to get asset thumbData"); } int compressionAttempts = 0; - while (thumbnailData.length > kThumbnailDataLimit && + while (thumbnailData!.length > thumbnailDataLimit && compressionAttempts < kMaximumThumbnailCompressionAttempts) { _logger.info("Thumbnail size " + thumbnailData.length.toString()); thumbnailData = await compressThumbnail(thumbnailData); @@ -144,7 +143,7 @@ Future _getMediaUploadDataFromAssetFile(ente.File file) async { compressionAttempts++; } - isDeleted = asset == null || !(await asset.exists); + isDeleted = !(await asset.exists); return MediaUploadData( sourceFile, thumbnailData, @@ -156,20 +155,20 @@ Future _getMediaUploadDataFromAssetFile(ente.File file) async { Future _decorateEnteFileData(ente.File file, AssetEntity asset) async { // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads if (file.location == null || - (file.location.latitude == 0 && file.location.longitude == 0)) { + (file.location!.latitude == 0 && file.location!.longitude == 0)) { final latLong = await asset.latlngAsync(); file.location = Location(latLong.latitude, latLong.longitude); } - if (file.title == null || file.title.isEmpty) { - _logger.warning("Title was missing ${file.tag()}"); + if (file.title == null || file.title!.isEmpty) { + _logger.warning("Title was missing ${file.tag}"); file.title = await asset.titleAsync; } } Future _getMediaUploadDataFromAppCache(ente.File file) async { io.File sourceFile; - Uint8List thumbnailData; + Uint8List? thumbnailData; const bool isDeleted = false; final localPath = getSharedMediaFilePath(file); sourceFile = io.File(localPath); @@ -194,7 +193,7 @@ Future _getMediaUploadDataFromAppCache(ente.File file) async { } } -Future getThumbnailFromInAppCacheFile(ente.File file) async { +Future getThumbnailFromInAppCacheFile(ente.File file) async { var localFile = io.File(getSharedMediaFilePath(file)); if (!localFile.existsSync()) { return null; @@ -204,14 +203,14 @@ Future getThumbnailFromInAppCacheFile(ente.File file) async { video: localFile.path, imageFormat: ImageFormat.JPEG, thumbnailPath: (await getTemporaryDirectory()).path, - maxWidth: kThumbnailLargeSize, + maxWidth: thumbnailLargeSize, quality: 80, ); - localFile = io.File(thumbnailFilePath); + localFile = io.File(thumbnailFilePath!); } var thumbnailData = await localFile.readAsBytes(); int compressionAttempts = 0; - while (thumbnailData.length > kThumbnailDataLimit && + while (thumbnailData.length > thumbnailDataLimit && compressionAttempts < kMaximumThumbnailCompressionAttempts) { _logger.info("Thumbnail size " + thumbnailData.length.toString()); thumbnailData = await compressThumbnail(thumbnailData); diff --git a/lib/utils/file_util.dart b/lib/utils/file_util.dart index 2072d4961..ede6f3e46 100644 --- a/lib/utils/file_util.dart +++ b/lib/utils/file_util.dart @@ -39,10 +39,10 @@ Future getFile( bool isOrigin = false, } // only relevant for live photos ) async { - if (file.isRemoteFile()) { + if (file.isRemoteFile) { return getFileFromServer(file, liveVideo: liveVideo); } else { - final String key = file.tag() + liveVideo.toString() + isOrigin.toString(); + final String key = file.tag + liveVideo.toString() + isOrigin.toString(); final cachedFile = FileLruCache.get(key); if (cachedFile == null) { final diskFile = await _getLocalDiskFile( @@ -70,7 +70,7 @@ Future _getLocalDiskFile( bool liveVideo = false, bool isOrigin = false, }) async { - if (file.isSharedMediaToAppSandbox()) { + if (file.isSharedMediaToAppSandbox) { final localFile = io.File(getSharedMediaFilePath(file)); return localFile.exists().then((exist) { return exist ? localFile : null; @@ -78,7 +78,7 @@ Future _getLocalDiskFile( } else if (file.fileType == FileType.livePhoto && liveVideo) { return Motionphoto.getLivePhotoFile(file.localID); } else { - return file.getAsset().then((asset) async { + return file.getAsset.then((asset) async { if (asset == null || !(await asset.exists)) { return null; } @@ -92,19 +92,19 @@ String getSharedMediaFilePath(ente.File file) { } String getSharedMediaPathFromLocalID(String localID) { - if (localID.startsWith(kOldSharedMediaIdentifier)) { + if (localID.startsWith(oldSharedMediaIdentifier)) { return Configuration.instance.getOldSharedMediaCacheDirectory() + "/" + - localID.replaceAll(kOldSharedMediaIdentifier, ''); + localID.replaceAll(oldSharedMediaIdentifier, ''); } else { return Configuration.instance.getSharedMediaDirectory() + "/" + - localID.replaceAll(kSharedMediaIdentifier, ''); + localID.replaceAll(sharedMediaIdentifier, ''); } } void preloadThumbnail(ente.File file) { - if (file.isRemoteFile()) { + if (file.isRemoteFile) { getThumbnailFromServer(file); } else { getThumbnailFromLocal(file); @@ -122,8 +122,7 @@ Future getFileFromServer( final cacheManager = (file.fileType == FileType.video || liveVideo) ? VideoCacheManager.instance : DefaultCacheManager(); - final fileFromCache = - await cacheManager.getFileFromCache(file.getDownloadUrl()); + final fileFromCache = await cacheManager.getFileFromCache(file.downloadUrl); if (fileFromCache != null) { return fileFromCache.file; } @@ -154,7 +153,7 @@ Future isFileCached(ente.File file, {bool liveVideo = false}) async { final cacheManager = (file.fileType == FileType.video || liveVideo) ? VideoCacheManager.instance : DefaultCacheManager(); - final fileInfo = await cacheManager.getFileFromCache(file.getDownloadUrl()); + final fileInfo = await cacheManager.getFileFromCache(file.downloadUrl); return fileInfo != null; } @@ -222,9 +221,9 @@ Future<_LivePhoto> _downloadLivePhoto( await imageFile.delete(); } imageFileCache = await DefaultCacheManager().putFile( - file.getDownloadUrl(), + file.downloadUrl, await imageConvertedFile.readAsBytes(), - eTag: file.getDownloadUrl(), + eTag: file.downloadUrl, maxAge: const Duration(days: 365), fileExtension: fileExtension, ); @@ -234,9 +233,9 @@ Future<_LivePhoto> _downloadLivePhoto( await videoFile.create(recursive: true); await videoFile.writeAsBytes(data); videoFileCache = await VideoCacheManager.instance.putFile( - file.getDownloadUrl(), + file.downloadUrl, await videoFile.readAsBytes(), - eTag: file.getDownloadUrl(), + eTag: file.downloadUrl, maxAge: const Duration(days: 365), fileExtension: fileExtension, ); @@ -246,7 +245,7 @@ Future<_LivePhoto> _downloadLivePhoto( } return _LivePhoto(imageFileCache, videoFileCache); }).catchError((e) { - _logger.warning("failed to download live photos : ${file.tag()}", e); + _logger.warning("failed to download live photos : ${file.tag}", e); throw e; }); } @@ -274,16 +273,16 @@ Future _downloadAndCache( await decryptedFile.delete(); } final cachedFile = await cacheManager.putFile( - file.getDownloadUrl(), + file.downloadUrl, await outputFile.readAsBytes(), - eTag: file.getDownloadUrl(), + eTag: file.downloadUrl, maxAge: const Duration(days: 365), fileExtension: fileExtension, ); await outputFile.delete(); return cachedFile; }).catchError((e) { - _logger.warning("failed to download file : ${file.tag()}", e); + _logger.warning("failed to download file : ${file.tag}", e); throw e; }); } @@ -301,17 +300,17 @@ String getExtension(String nameOrPath) { Future compressThumbnail(Uint8List thumbnail) { return FlutterImageCompress.compressWithList( thumbnail, - minHeight: kCompressedThumbnailResolution, - minWidth: kCompressedThumbnailResolution, + minHeight: compressedThumbnailResolution, + minWidth: compressedThumbnailResolution, quality: 25, ); } Future clearCache(ente.File file) async { if (file.fileType == FileType.video) { - VideoCacheManager.instance.removeFile(file.getDownloadUrl()); + VideoCacheManager.instance.removeFile(file.downloadUrl); } else { - DefaultCacheManager().removeFile(file.getDownloadUrl()); + DefaultCacheManager().removeFile(file.downloadUrl); } final cachedThumbnail = io.File( Configuration.instance.getThumbnailCacheDirectory() + diff --git a/lib/utils/hex.dart b/lib/utils/hex.dart index 887dafa54..efe0b8c6a 100644 --- a/lib/utils/hex.dart +++ b/lib/utils/hex.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import "dart:convert"; import "dart:typed_data"; diff --git a/lib/utils/local_settings.dart b/lib/utils/local_settings.dart index d121ebf39..c254b0dc9 100644 --- a/lib/utils/local_settings.dart +++ b/lib/utils/local_settings.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'package:shared_preferences/shared_preferences.dart'; enum AlbumSortKey { @@ -13,15 +11,14 @@ class LocalSettings { static final LocalSettings instance = LocalSettings._privateConstructor(); static const kCollectionSortPref = "collection_sort_pref"; - SharedPreferences _prefs; + late SharedPreferences _prefs; Future init() async { _prefs = await SharedPreferences.getInstance(); } AlbumSortKey albumSortKey() { - return AlbumSortKey.values[_prefs.getInt(kCollectionSortPref) ?? 0] ?? - AlbumSortKey.lastUpdated; + return AlbumSortKey.values[_prefs.getInt(kCollectionSortPref) ?? 0]; } Future setAlbumSortKey(AlbumSortKey key) { diff --git a/lib/utils/magic_util.dart b/lib/utils/magic_util.dart index aaf57f56e..e0f480980 100644 --- a/lib/utils/magic_util.dart +++ b/lib/utils/magic_util.dart @@ -23,14 +23,14 @@ Future changeVisibility( ) async { final dialog = createProgressDialog( context, - newVisibility == kVisibilityArchive ? "Hiding..." : "Unhiding...", + newVisibility == visibilityArchive ? "Hiding..." : "Unhiding...", ); await dialog.show(); try { await FileMagicService.instance.changeVisibility(files, newVisibility); showShortToast( context, - newVisibility == kVisibilityArchive + newVisibility == visibilityArchive ? "Successfully hidden" : "Successfully unhidden", ); @@ -50,17 +50,17 @@ Future changeCollectionVisibility( ) async { final dialog = createProgressDialog( context, - newVisibility == kVisibilityArchive ? "Hiding..." : "Unhiding...", + newVisibility == visibilityArchive ? "Hiding..." : "Unhiding...", ); await dialog.show(); try { - final Map update = {kMagicKeyVisibility: newVisibility}; + final Map update = {magicKeyVisibility: newVisibility}; await CollectionsService.instance.updateMagicMetadata(collection, update); // Force reload home gallery to pull in the now unarchived files Bus.instance.fire(ForceReloadHomeGalleryEvent()); showShortToast( context, - newVisibility == kVisibilityArchive + newVisibility == visibilityArchive ? "Successfully hidden" : "Successfully unhidden", ); @@ -82,7 +82,7 @@ Future editTime( await _updatePublicMetadata( context, files, - kPubMagicKeyEditedTime, + pubMagicKeyEditedTime, editedTime, ); return true; @@ -97,7 +97,7 @@ Future editFilename( File file, ) async { try { - final fileName = file.getDisplayName(); + final fileName = file.displayName; final nameWithoutExt = basenameWithoutExtension(fileName); final extName = extension(fileName); var result = await showDialog( @@ -115,7 +115,7 @@ Future editFilename( await _updatePublicMetadata( context, List.of([file]), - kPubMagicKeyEditedName, + pubMagicKeyEditedName, result, ); return true; @@ -152,5 +152,5 @@ Future _updatePublicMetadata( } bool _shouldReloadGallery(String key) { - return key == kPubMagicKeyEditedTime; + return key == pubMagicKeyEditedTime; } diff --git a/lib/utils/navigation_util.dart b/lib/utils/navigation_util.dart index e6e92d4ea..38a3dbe53 100644 --- a/lib/utils/navigation_util.dart +++ b/lib/utils/navigation_util.dart @@ -1,10 +1,8 @@ -// @dart=2.9 - import 'dart:io'; import 'package:flutter/material.dart'; -Future routeToPage( +Future routeToPage( BuildContext context, Widget page, { bool forceCustomPageRoute = false, @@ -63,13 +61,13 @@ class SwipeableRouteBuilder extends PageRoute { const CupertinoPageTransitionsBuilder(); // Default iOS/macOS (to get the swipe right to go back gesture) // final PageTransitionsBuilder matchingBuilder = const FadeUpwardsPageTransitionsBuilder(); // Default Android/Linux/Windows - SwipeableRouteBuilder({this.pageBuilder}); + SwipeableRouteBuilder({required this.pageBuilder}); @override - Color get barrierColor => null; + Null get barrierColor => null; @override - String get barrierLabel => null; + Null get barrierLabel => null; @override Widget buildPage( @@ -110,21 +108,21 @@ class SwipeableRouteBuilder extends PageRoute { class TransparentRoute extends PageRoute { TransparentRoute({ - @required this.builder, - RouteSettings settings, + required this.builder, + RouteSettings? settings, }) : assert(builder != null), super(settings: settings, fullscreenDialog: false); - final WidgetBuilder builder; + final WidgetBuilder? builder; @override bool get opaque => false; @override - Color get barrierColor => null; + Null get barrierColor => null; @override - String get barrierLabel => null; + Null get barrierLabel => null; @override bool get maintainState => true; @@ -138,7 +136,7 @@ class TransparentRoute extends PageRoute { Animation animation, Animation secondaryAnimation, ) { - final result = builder(context); + final result = builder!(context); return FadeTransition( opacity: Tween(begin: 0, end: 1).animate(animation), child: Semantics( diff --git a/lib/utils/share_util.dart b/lib/utils/share_util.dart index 48a5e6878..2a3562081 100644 --- a/lib/utils/share_util.dart +++ b/lib/utils/share_util.dart @@ -46,7 +46,8 @@ Future share( Rect shareButtonRect(BuildContext context, GlobalKey shareButtonKey) { Size size = MediaQuery.of(context).size; - final RenderBox renderBox = shareButtonKey?.currentContext?.findRenderObject(); + final RenderBox renderBox = + shareButtonKey?.currentContext?.findRenderObject(); if (renderBox == null) { return Rect.fromLTWH(0, 0, size.width, size.height / 2); } @@ -83,7 +84,7 @@ Future> convertIncomingSharedMediaToFile( ioFile = ioFile.renameSync( Configuration.instance.getSharedMediaDirectory() + "/" + enteFile.title, ); - enteFile.localID = kSharedMediaIdentifier + enteFile.title; + enteFile.localID = sharedMediaIdentifier + enteFile.title; enteFile.collectionID = collectionID; enteFile.fileType = media.type == SharedMediaType.IMAGE ? FileType.image : FileType.video; diff --git a/lib/utils/thumbnail_util.dart b/lib/utils/thumbnail_util.dart index 8f78c2179..13bc57841 100644 --- a/lib/utils/thumbnail_util.dart +++ b/lib/utils/thumbnail_util.dart @@ -62,8 +62,8 @@ Future getThumbnailFromServer(File file) async { Future getThumbnailFromLocal( File file, { - int size = kThumbnailSmallSize, - int quality = kThumbnailQuality, + int size = thumbnailSmallSize, + int quality = thumbnailQuality, }) async { final lruCachedThumbnail = ThumbnailLruCache.get(file, size); if (lruCachedThumbnail != null) { @@ -75,7 +75,7 @@ Future getThumbnailFromLocal( ThumbnailLruCache.put(file, data); return data; } - if (file.isSharedMediaToAppSandbox()) { + if (file.isSharedMediaToAppSandbox) { //todo:neeraj support specifying size/quality return getThumbnailFromInAppCacheFile(file).then((data) { if (data != null) { @@ -84,7 +84,7 @@ Future getThumbnailFromLocal( return data; }); } else { - return file.getAsset().then((asset) async { + return file.getAsset.then((asset) async { if (asset == null || !(await asset.exists)) { return null; } @@ -130,7 +130,7 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { Uint8List encryptedThumbnail; try { encryptedThumbnail = (await Network.instance.getDio().get( - file.getThumbnailUrl(), + file.thumbnailUrl, options: Options( headers: {"X-Auth-Token": Configuration.instance.getToken()}, responseType: ResponseType.bytes, @@ -154,7 +154,7 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { Sodium.base642bin(file.thumbnailDecryptionHeader), ); final thumbnailSize = data.length; - if (thumbnailSize > kThumbnailDataLimit) { + if (thumbnailSize > thumbnailDataLimit) { data = await compressThumbnail(data); } ThumbnailLruCache.put(item.file, data); diff --git a/lib/utils/toast_util.dart b/lib/utils/toast_util.dart index 00bafb915..ec21ced86 100644 --- a/lib/utils/toast_util.dart +++ b/lib/utils/toast_util.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'dart:io'; import 'package:flutter/material.dart'; @@ -7,7 +5,7 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:photos/ente_theme_data.dart'; -Future showToast( +Future showToast( BuildContext context, String message, { toastLength = Toast.LENGTH_LONG, diff --git a/lib/utils/validator_util.dart b/lib/utils/validator_util.dart index 0e5b6952d..2ab3aeca7 100644 --- a/lib/utils/validator_util.dart +++ b/lib/utils/validator_util.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'dart:convert'; import 'dart:typed_data'; @@ -9,9 +7,9 @@ import 'package:photos/models/key_attributes.dart'; Logger _logger = Logger("Validator"); void validatePreVerificationStateCheck( - KeyAttributes keyAttr, - String password, - String encryptedToken, + KeyAttributes? keyAttr, + String? password, + String? encryptedToken, ) { nullOrEmptyArgCheck(encryptedToken, "encryptedToken"); nullOrEmptyArgCheck(password, "userPassword"); @@ -27,12 +25,12 @@ void validatePreVerificationStateCheck( "secretKeyDecryptionNonce", ); nullOrEmptyArgCheck(keyAttr.publicKey, "publicKey"); - if ((keyAttr.memLimit ?? 0) <= 0 || (keyAttr.opsLimit ?? 0) <= 0) { - throw ArgumentError("Key mem/OpsLimit can not be null or <0"); + if (keyAttr.memLimit <= 0 || keyAttr.opsLimit <= 0) { + throw ArgumentError("Key mem/OpsLimit can not be <0"); } // check password encoding issues try { - final Uint8List passwordL = utf8.encode(password); + final Uint8List passwordL = utf8.encode(password!) as Uint8List; try { utf8.decode(passwordL); } catch (e) { @@ -45,7 +43,7 @@ void validatePreVerificationStateCheck( } } -void nullOrEmptyArgCheck(String value, String name) { +void nullOrEmptyArgCheck(String? value, String name) { if (value == null || value.isEmpty) { throw ArgumentError("Critical: $name is nullOrEmpty"); } diff --git a/pubspec.lock b/pubspec.lock index 1a855fc11..79568869f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -911,7 +911,7 @@ packages: name: photo_manager url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.4" photo_view: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 86c79e63b..28a188942 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: connectivity: ^3.0.3 cupertino_icons: ^1.0.0 device_info: ^2.0.2 - dio: ^4.0.0 + dio: ^4.0.6 dots_indicator: ^2.0.0 dotted_border: ^2.0.0+2 email_validator: ^2.0.1 @@ -88,7 +88,7 @@ dependencies: path: #dart path_provider: ^2.0.1 pedantic: ^1.9.2 - photo_manager: ^2.0.8 + photo_manager: 2.1.4 photo_view: ^0.14.0 pinput: ^1.2.2 provider: ^6.0.0