Merge pull request #410 from ente-io/rewrite_device_sync
Rewrite Local Import
This commit is contained in:
commit
123bb011b9
179 changed files with 3749 additions and 3410 deletions
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
8
lib/core/cache/image_cache.dart
vendored
8
lib/core/cache/image_cache.dart
vendored
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
12
lib/core/cache/lru_map.dart
vendored
12
lib/core/cache/lru_map.dart
vendored
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
18
lib/core/cache/thumbnail_cache.dart
vendored
18
lib/core/cache/thumbnail_cache.dart
vendored
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
2
lib/core/cache/video_cache_manager.dart
vendored
2
lib/core/cache/video_cache_manager.dart
vendored
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
class VideoCacheManager {
|
||||
|
|
|
@ -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)!;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
class InvalidFileError extends ArgumentError {
|
||||
InvalidFileError(String message) : super(message);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
|
||||
class YearsData {
|
||||
|
|
|
@ -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
376
lib/db/device_files_db.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ?? '{}';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
8
lib/events/local_import_progress.dart
Normal file
8
lib/events/local_import_progress.dart
Normal 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);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
class BackupStatus {
|
||||
final List<String> localIDs;
|
||||
final int size;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class CollectionFileItem {
|
||||
|
@ -14,9 +12,9 @@ class CollectionFileItem {
|
|||
);
|
||||
|
||||
CollectionFileItem copyWith({
|
||||
int id,
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
int? id,
|
||||
String? encryptedKey,
|
||||
String? keyDecryptionNonce,
|
||||
}) {
|
||||
return CollectionFileItem(
|
||||
id ?? this.id,
|
||||
|
@ -33,7 +31,7 @@ class CollectionFileItem {
|
|||
};
|
||||
}
|
||||
|
||||
factory CollectionFileItem.fromMap(Map<String, dynamic> map) {
|
||||
static fromMap(Map<String, dynamic>? map) {
|
||||
if (map == null) return null;
|
||||
|
||||
return CollectionFileItem(
|
||||
|
|
|
@ -1,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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
class DerivedKeyResult {
|
||||
final Uint8List key;
|
||||
final Uint8List salt;
|
||||
final int memLimit;
|
||||
final int opsLimit;
|
||||
|
||||
DerivedKeyResult(this.key, this.salt);
|
||||
DerivedKeyResult(this.key, this.memLimit, this.opsLimit);
|
||||
}
|
||||
|
|
24
lib/models/device_collection.dart
Normal file
24
lib/models/device_collection.dart
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/models/file.dart';
|
||||
|
||||
class FileLoadResult {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/models/file.dart';
|
||||
|
||||
class GalleryItemsFilter {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/models/key_attributes.dart';
|
||||
import 'package:photos/models/private_key_attributes.dart';
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
// @dart=2.9
|
||||
|
||||
class Location {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
|
||||
Location(this.latitude, this.longitude);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/models/file.dart';
|
||||
|
||||
class Memory {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
class PrivateKeyAttributes {
|
||||
final String key;
|
||||
final String recoveryKey;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
class PublicKey {
|
||||
final String email;
|
||||
final String publicKey;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,29 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/search/search_results.dart';
|
||||
import 'package:photos/models/search/search_result.dart';
|
||||
|
||||
class FileSearchResult extends SearchResult {
|
||||
final File file;
|
||||
|
||||
FileSearchResult(this.file);
|
||||
|
||||
@override
|
||||
String name() {
|
||||
return file.displayName;
|
||||
}
|
||||
|
||||
@override
|
||||
ResultType type() {
|
||||
return ResultType.file;
|
||||
}
|
||||
|
||||
@override
|
||||
File previewThumbnail() {
|
||||
return file;
|
||||
}
|
||||
|
||||
@override
|
||||
List<File> resultFiles() {
|
||||
// for fileSearchResult, the file detailed page view will be opened
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
30
lib/models/search/generic_search_result.dart
Normal file
30
lib/models/search/generic_search_result.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/search/search_results.dart';
|
||||
|
||||
class MonthSearchResult extends SearchResult {
|
||||
final String month;
|
||||
final List<File> files;
|
||||
MonthSearchResult(this.month, this.files);
|
||||
}
|
||||
|
||||
class MonthData {
|
||||
final String name;
|
||||
final int monthNumber;
|
||||
MonthData(this.name, this.monthNumber);
|
||||
}
|
26
lib/models/search/search_result.dart
Normal file
26
lib/models/search/search_result.dart
Normal 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
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
|
||||
|
||||
class SearchResult {}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:photos/models/file.dart';
|
||||
|
||||
class TrashFile extends File {
|
||||
// time when file was put in the trash for first time
|
||||
int createdAt;
|
||||
late int createdAt;
|
||||
|
||||
// for non-deleted trash items, updateAt is usually equal to the latest time
|
||||
// when the file was moved to trash
|
||||
int updateAt;
|
||||
late int updateAt;
|
||||
|
||||
// time after which will will be deleted from trash & user's storage usage
|
||||
// will go down
|
||||
int deleteBy;
|
||||
late int deleteBy;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
// @dart=2.9
|
||||
|
||||
class TrashRequest {
|
||||
final int fileID;
|
||||
final int collectionID;
|
||||
|
||||
TrashRequest(this.fileID, this.collectionID)
|
||||
: assert(fileID != null),
|
||||
assert(collectionID != null);
|
||||
TrashRequest(this.fileID, this.collectionID);
|
||||
|
||||
factory TrashRequest.fromJson(Map<String, dynamic> json) {
|
||||
return TrashRequest(json['fileID'], json['collectionID']);
|
||||
|
|
30
lib/models/upload_strategy.dart
Normal file
30
lib/models/upload_strategy.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class AppLifecycleService {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,9 +32,9 @@ class FileMagicService {
|
|||
FileMagicService._privateConstructor();
|
||||
|
||||
Future<void> changeVisibility(List<File> files, int visibility) async {
|
||||
final Map<String, dynamic> update = {kMagicKeyVisibility: visibility};
|
||||
final Map<String, dynamic> update = {magicKeyVisibility: visibility};
|
||||
await _updateMagicData(files, update);
|
||||
if (visibility == kVisibilityVisible) {
|
||||
if (visibility == visibilityVisible) {
|
||||
// Force reload home gallery to pull in the now unarchived files
|
||||
Bus.instance.fire(ForceReloadHomeGalleryEvent());
|
||||
Bus.instance
|
||||
|
@ -64,7 +64,8 @@ class FileMagicService {
|
|||
// read the existing magic metadata and apply new updates to existing data
|
||||
// current update is simple replace. This will be enhanced in the future,
|
||||
// as required.
|
||||
final Map<String, dynamic> jsonToUpdate = jsonDecode(file.pubMmdEncodedJson);
|
||||
final Map<String, dynamic> jsonToUpdate =
|
||||
jsonDecode(file.pubMmdEncodedJson);
|
||||
newMetadataUpdate.forEach((key, value) {
|
||||
jsonToUpdate[key] = value;
|
||||
});
|
||||
|
@ -134,7 +135,8 @@ class FileMagicService {
|
|||
// read the existing magic metadata and apply new updates to existing data
|
||||
// current update is simple replace. This will be enhanced in the future,
|
||||
// as required.
|
||||
final Map<String, dynamic> jsonToUpdate = jsonDecode(file.mMdEncodedJson);
|
||||
final Map<String, dynamic> jsonToUpdate =
|
||||
jsonDecode(file.mMdEncodedJson);
|
||||
newMetadataUpdate.forEach((key, value) {
|
||||
jsonToUpdate[key] = value;
|
||||
});
|
||||
|
|
37
lib/services/files_service.dart
Normal file
37
lib/services/files_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,36 @@ class IgnoredFilesService {
|
|||
return false;
|
||||
}
|
||||
|
||||
// removeIgnoredMappings is used to remove the ignore mapping for the given
|
||||
// set of files so that they can be uploaded.
|
||||
Future<void> removeIgnoredMappings(List<File> files) async {
|
||||
final List<IgnoredFile> ignoredFiles = [];
|
||||
final Set<String> idsToRemoveFromCache = {};
|
||||
final Set<String> currentlyIgnoredIDs = await ignoredIDs;
|
||||
for (final file in files) {
|
||||
// check if upload is not skipped for file. If not, no need to remove
|
||||
// any mapping
|
||||
if (!shouldSkipUpload(currentlyIgnoredIDs, file)) {
|
||||
continue;
|
||||
}
|
||||
final id = _getIgnoreID(file.localID, file.deviceFolder, file.title);
|
||||
idsToRemoveFromCache.add(id);
|
||||
ignoredFiles.add(
|
||||
IgnoredFile(file.localID, file.title, file.deviceFolder, ""),
|
||||
);
|
||||
}
|
||||
|
||||
if (ignoredFiles.isNotEmpty) {
|
||||
await _db.removeIgnoredEntries(ignoredFiles);
|
||||
currentlyIgnoredIDs.removeAll(idsToRemoveFromCache);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reset() async {
|
||||
await _db.clearTable();
|
||||
_ignoredIDs = null;
|
||||
}
|
||||
|
||||
Future<Set<String>> _loadExistingIDs() async {
|
||||
_logger.fine('loading existing IDs');
|
||||
final result = await _db.getAll();
|
||||
|
@ -66,7 +96,7 @@ class IgnoredFilesService {
|
|||
);
|
||||
}
|
||||
|
||||
// _computeIgnoreID will return null if don't have sufficient information
|
||||
// _getIgnoreID will return null if don't have sufficient information
|
||||
// to ignore the file based on the platform. Uploads from web or files shared to
|
||||
// end usually don't have local id.
|
||||
// For Android: It returns deviceFolder-title as ID for Android.
|
||||
|
|
333
lib/services/local/local_sync_util.dart
Normal file
333
lib/services/local/local_sync_util.dart
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -33,7 +33,7 @@ class MemoriesService extends ChangeNotifier {
|
|||
// Intention of delay is to give more CPU cycles to other tasks
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
_memoriesDB.clearMemoriesSeenBeforeTime(
|
||||
DateTime.now().microsecondsSinceEpoch - (7 * kMicroSecondsInDay),
|
||||
DateTime.now().microsecondsSinceEpoch - (7 * microSecondsInDay),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,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) {
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -181,6 +181,7 @@ class TrashSyncService {
|
|||
data: params,
|
||||
);
|
||||
await _trashDB.clearTable();
|
||||
unawaited(syncTrash());
|
||||
Bus.instance.fire(TrashUpdatedEvent());
|
||||
Bus.instance.fire(ForceReloadTrashPageEvent());
|
||||
} catch (e, s) {
|
||||
|
|
|
@ -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
156
lib/theme/colors.dart
Normal 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
51
lib/theme/effects.dart
Normal 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
36
lib/theme/ente_theme.dart
Normal 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
117
lib/theme/text_style.dart
Normal 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),
|
||||
);
|
||||
}
|
|
@ -75,7 +75,6 @@ class DeleteAccountPage extends StatelessWidget {
|
|||
),
|
||||
GradientButton(
|
||||
text: "Yes, send feedback",
|
||||
paddingValue: 4,
|
||||
iconData: Icons.check,
|
||||
onTap: () async {
|
||||
await sendEmail(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue