Merge pull request #410 from ente-io/rewrite_device_sync

Rewrite Local Import
This commit is contained in:
Neeraj Gupta 2022-09-27 10:46:51 +05:30 committed by GitHub
commit 123bb011b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
179 changed files with 3749 additions and 3410 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

376
lib/db/device_files_db.dart Normal file
View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -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<BoxShadow> get shadowMenu => brightness == Brightness.light
? [
BoxShadow(blurRadius: 6, color: Colors.white.withOpacity(0.16)),
BoxShadow(
blurRadius: 6,
color: Colors.white.withOpacity(0.12),
offset: const Offset(0, 3),
),
]
: [
BoxShadow(blurRadius: 6, color: Colors.black.withOpacity(0.50)),
BoxShadow(
blurRadius: 6,
color: Colors.black.withOpacity(0.25),
offset: const Offset(0, 3),
),
];
EnteTheme get enteTheme =>
brightness == Brightness.light ? lightTheme : darkTheme;
}
OutlinedButtonThemeData buildOutlinedButtonThemeData({
Color bgDisabled,
Color bgEnabled,
Color fgDisabled,
Color fgEnabled,
required Color bgDisabled,
required Color bgEnabled,
required Color fgDisabled,
required Color fgEnabled,
}) {
return OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
@ -429,8 +402,8 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData({
}
ElevatedButtonThemeData buildElevatedButtonThemeData({
@required Color onPrimary, // text button color
@required Color primary,
required Color onPrimary, // text button color
required Color primary,
double elevation = 2, // background color of button
}) {
return ElevatedButtonThemeData(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

156
lib/theme/colors.dart Normal file
View file

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

51
lib/theme/effects.dart Normal file
View file

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

36
lib/theme/ente_theme.dart Normal file
View file

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

117
lib/theme/text_style.dart Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<DeviceFolder> folders;
const DeviceFoldersGridViewWidget(
this.folders, {
class DeviceFoldersGridViewWidget extends StatefulWidget {
const DeviceFoldersGridViewWidget({
Key key,
}) : super(key: key);
@override
State<DeviceFoldersGridViewWidget> createState() =>
_DeviceFoldersGridViewWidgetState();
}
class _DeviceFoldersGridViewWidgetState
extends State<DeviceFoldersGridViewWidget> {
StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
@override
void initState() {
_backupFoldersUpdatedEvent =
Bus.instance.on<BackupFoldersUpdatedEvent>().listen((event) {
if (mounted) {
setState(() {});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
final bool isMigrationDone =
LocalSyncService.instance.isDeviceFileMigrationDone();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
height: 170,
child: Align(
alignment: Alignment.centerLeft,
child: folders.isEmpty
? const EmptyState()
: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
physics: const ScrollPhysics(),
// to disable GridView's scrolling
itemBuilder: (context, index) {
return DeviceFolderIcon(folders[index]);
},
itemCount: folders.length,
),
child: FutureBuilder<List<DeviceCollection>>(
future: FilesDB.instance
.getDeviceCollections(includeCoverThumbnail: true),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data.isEmpty
? Padding(
padding: const EdgeInsets.all(22),
child: (isMigrationDone
? const EmptyState()
: const EmptyState(
text: "Importing....",
)),
)
: ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
physics: const ScrollPhysics(),
// to disable GridView's scrolling
itemBuilder: (context, index) {
final deviceCollection = snapshot.data[index];
return DeviceFolderIcon(deviceCollection);
},
itemCount: snapshot.data.length,
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return const EnteLoadingWidget();
}
},
),
),
),
);
}
@override
void dispose() {
_backupFoldersUpdatedEvent?.cancel();
super.dispose();
}
}

Some files were not shown because too many files have changed in this diff Show more