Merge pull request #3 from ente-io/collections
Replace `folders` with `collections`.
This commit is contained in:
commit
9f60842402
32 changed files with 1367 additions and 734 deletions
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:io' as io;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
@ -21,11 +22,13 @@ class Configuration {
|
|||
static const hasOptedForE2EKey = "has_opted_for_e2e_encryption";
|
||||
static const foldersToBackUpKey = "folders_to_back_up";
|
||||
static const keyKey = "key";
|
||||
static const secretKeyKey = "secret_key";
|
||||
static const keyAttributesKey = "key_attributes";
|
||||
|
||||
SharedPreferences _preferences;
|
||||
FlutterSecureStorage _secureStorage;
|
||||
String _key;
|
||||
String _secretKey;
|
||||
String _documentsDirectory;
|
||||
String _tempDirectory;
|
||||
String _thumbnailsDirectory;
|
||||
|
@ -39,11 +42,12 @@ class Configuration {
|
|||
new io.Directory(_tempDirectory).createSync(recursive: true);
|
||||
new io.Directory(_thumbnailsDirectory).createSync(recursive: true);
|
||||
_key = await _secureStorage.read(key: keyKey);
|
||||
_secretKey = await _secureStorage.read(key: secretKeyKey);
|
||||
}
|
||||
|
||||
Future<KeyAttributes> generateAndSaveKey(String passphrase) async {
|
||||
// Create a master key
|
||||
final key = CryptoUtil.generateMasterKey();
|
||||
final key = CryptoUtil.generateKey();
|
||||
|
||||
// Derive a key from the passphrase that will be used to encrypt and
|
||||
// decrypt the master key
|
||||
|
@ -51,26 +55,26 @@ class Configuration {
|
|||
final kek = CryptoUtil.deriveKey(utf8.encode(passphrase), kekSalt);
|
||||
|
||||
// Encrypt the key with this derived key
|
||||
final encryptedKeyData = await CryptoUtil.encrypt(key, key: kek);
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, kek);
|
||||
|
||||
// Hash the passphrase so that its correctness can be compared later
|
||||
final kekHash = await CryptoUtil.hash(kek);
|
||||
|
||||
// Generate a public-private keypair and encrypt the latter
|
||||
final keyPair = await CryptoUtil.generateKeyPair();
|
||||
final encryptedSecretKeyData =
|
||||
await CryptoUtil.encrypt(keyPair.sk, key: kek);
|
||||
final encryptedSecretKeyData = CryptoUtil.encryptSync(keyPair.sk, kek);
|
||||
|
||||
final attributes = KeyAttributes(
|
||||
Sodium.bin2base64(kekSalt),
|
||||
kekHash,
|
||||
encryptedKeyData.encryptedData.base64,
|
||||
encryptedKeyData.nonce.base64,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
Sodium.bin2base64(keyPair.pk),
|
||||
encryptedSecretKeyData.encryptedData.base64,
|
||||
encryptedSecretKeyData.nonce.base64,
|
||||
Sodium.bin2base64(encryptedSecretKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedSecretKeyData.nonce),
|
||||
);
|
||||
await setKey(Sodium.bin2base64(key));
|
||||
await setSecretKey(Sodium.bin2base64(keyPair.sk));
|
||||
await setKeyAttributes(attributes);
|
||||
return attributes;
|
||||
}
|
||||
|
@ -84,14 +88,22 @@ class Configuration {
|
|||
if (!correctPassphrase) {
|
||||
throw Exception("Incorrect passphrase");
|
||||
}
|
||||
final key = await CryptoUtil.decrypt(
|
||||
final key = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedKey),
|
||||
kek,
|
||||
Sodium.base642bin(attributes.keyDecryptionNonce));
|
||||
final secretKey = CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(attributes.encryptedSecretKey),
|
||||
kek,
|
||||
Sodium.base642bin(attributes.secretKeyDecryptionNonce));
|
||||
await setKey(Sodium.bin2base64(key));
|
||||
await setSecretKey(Sodium.bin2base64(secretKey));
|
||||
}
|
||||
|
||||
String getHttpEndpoint() {
|
||||
if (kDebugMode) {
|
||||
return "http://192.168.1.3:80";
|
||||
}
|
||||
return "https://api.staging.ente.io";
|
||||
}
|
||||
|
||||
|
@ -157,7 +169,7 @@ class Configuration {
|
|||
|
||||
KeyAttributes getKeyAttributes() {
|
||||
final jsonValue = _preferences.getString(keyAttributesKey);
|
||||
if (keyAttributesKey == null) {
|
||||
if (jsonValue == null) {
|
||||
return null;
|
||||
} else {
|
||||
return KeyAttributes.fromJson(jsonValue);
|
||||
|
@ -165,12 +177,29 @@ class Configuration {
|
|||
}
|
||||
|
||||
Future<void> setKey(String key) async {
|
||||
await _secureStorage.write(key: keyKey, value: key);
|
||||
_key = key;
|
||||
if (key == null) {
|
||||
await _secureStorage.delete(key: keyKey);
|
||||
} else {
|
||||
await _secureStorage.write(key: keyKey, value: key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setSecretKey(String secretKey) async {
|
||||
_secretKey = secretKey;
|
||||
if (secretKey == null) {
|
||||
await _secureStorage.delete(key: secretKeyKey);
|
||||
} else {
|
||||
await _secureStorage.write(key: secretKeyKey, value: secretKey);
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List getKey() {
|
||||
return Sodium.base642bin(_key);
|
||||
return _key == null ? null : Sodium.base642bin(_key);
|
||||
}
|
||||
|
||||
Uint8List getSecretKey() {
|
||||
return _secretKey == null ? null : Sodium.base642bin(_secretKey);
|
||||
}
|
||||
|
||||
String getDocumentsDirectory() {
|
||||
|
|
192
lib/db/collections_db.dart
Normal file
192
lib/db/collections_db.dart
Normal file
|
@ -0,0 +1,192 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/shared_collection.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
class CollectionsDB {
|
||||
static final _databaseName = "ente.collections.db";
|
||||
static final _databaseVersion = 1;
|
||||
|
||||
static final collectionsTable = 'collections';
|
||||
static final sharedCollectionsTable = 'shared_collections';
|
||||
|
||||
static final columnID = 'collection_id';
|
||||
static final columnOwnerID = 'owner_id';
|
||||
static final columnEncryptedKey = 'encrypted_key';
|
||||
static final columnKeyDecryptionNonce = 'key_decryption_nonce';
|
||||
static final columnName = 'name';
|
||||
static final columnType = 'type';
|
||||
static final columnEncryptedPath = 'encrypted_path';
|
||||
static final columnPathDecryptionNonce = 'path_decryption_nonce';
|
||||
static final columnCreationTime = 'creation_time';
|
||||
|
||||
CollectionsDB._privateConstructor();
|
||||
static final CollectionsDB instance = CollectionsDB._privateConstructor();
|
||||
|
||||
static Database _database;
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database;
|
||||
_database = await _initDatabase();
|
||||
return _database;
|
||||
}
|
||||
|
||||
_initDatabase() async {
|
||||
Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
String path = join(documentsDirectory.path, _databaseName);
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: _databaseVersion,
|
||||
onCreate: _onCreate,
|
||||
);
|
||||
}
|
||||
|
||||
Future _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE $collectionsTable (
|
||||
$columnID INTEGER PRIMARY KEY NOT NULL,
|
||||
$columnOwnerID INTEGER NOT NULL,
|
||||
$columnEncryptedKey TEXT NOT NULL,
|
||||
$columnKeyDecryptionNonce TEXT NOT NULL,
|
||||
$columnName TEXT NOT NULL,
|
||||
$columnType TEXT NOT NULL,
|
||||
$columnEncryptedPath TEXT,
|
||||
$columnPathDecryptionNonce TEXT,
|
||||
$columnCreationTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $sharedCollectionsTable (
|
||||
$columnID INTEGER PRIMARY KEY NOT NULL,
|
||||
$columnOwnerID INTEGER NOT NULL,
|
||||
$columnEncryptedKey TEXT NOT NULL,
|
||||
$columnName TEXT NOT NULL,
|
||||
$columnType TEXT NOT NULL,
|
||||
$columnCreationTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
Future<List<dynamic>> insert(List<Collection> collections) async {
|
||||
final db = await instance.database;
|
||||
var batch = db.batch();
|
||||
for (final collection in collections) {
|
||||
batch.insert(collectionsTable, _getRowForCollection(collection),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
return await batch.commit();
|
||||
}
|
||||
|
||||
Future<List<dynamic>> insertSharedCollections(
|
||||
List<SharedCollection> collections) async {
|
||||
final db = await instance.database;
|
||||
var batch = db.batch();
|
||||
for (final collection in collections) {
|
||||
batch.insert(
|
||||
sharedCollectionsTable, _getRowForSharedCollection(collection),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
return await batch.commit();
|
||||
}
|
||||
|
||||
Future<List<Collection>> getAllCollections() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(collectionsTable);
|
||||
final collections = List<Collection>();
|
||||
for (final row in rows) {
|
||||
collections.add(_convertToCollection(row));
|
||||
}
|
||||
return collections;
|
||||
}
|
||||
|
||||
Future<List<SharedCollection>> getAllSharedCollections() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(sharedCollectionsTable);
|
||||
final collections = List<SharedCollection>();
|
||||
for (final row in rows) {
|
||||
collections.add(_convertToSharedCollection(row));
|
||||
}
|
||||
return collections;
|
||||
}
|
||||
|
||||
Future<int> getLastCollectionCreationTime() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
collectionsTable,
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return int.parse(rows[0][columnCreationTime]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getLastSharedCollectionCreationTime() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
sharedCollectionsTable,
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return int.parse(rows[0][columnCreationTime]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getRowForCollection(Collection collection) {
|
||||
var row = new Map<String, dynamic>();
|
||||
row[columnID] = collection.id;
|
||||
row[columnOwnerID] = collection.ownerID;
|
||||
row[columnEncryptedKey] = collection.encryptedKey;
|
||||
row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce;
|
||||
row[columnName] = collection.name;
|
||||
row[columnType] = Collection.typeToString(collection.type);
|
||||
row[columnEncryptedPath] = collection.encryptedPath;
|
||||
row[columnPathDecryptionNonce] = collection.pathDecryptionNonce;
|
||||
row[columnCreationTime] = collection.creationTime;
|
||||
return row;
|
||||
}
|
||||
|
||||
Collection _convertToCollection(Map<String, dynamic> row) {
|
||||
return Collection(
|
||||
row[columnID],
|
||||
row[columnOwnerID],
|
||||
row[columnEncryptedKey],
|
||||
row[columnKeyDecryptionNonce],
|
||||
row[columnName],
|
||||
Collection.typeFromString(row[columnType]),
|
||||
row[columnEncryptedPath],
|
||||
row[columnPathDecryptionNonce],
|
||||
int.parse(row[columnCreationTime]),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getRowForSharedCollection(SharedCollection collection) {
|
||||
var row = new Map<String, dynamic>();
|
||||
row[columnID] = collection.id;
|
||||
row[columnOwnerID] = collection.ownerID;
|
||||
row[columnEncryptedKey] = collection.encryptedKey;
|
||||
row[columnName] = collection.name;
|
||||
row[columnType] = Collection.typeToString(collection.type);
|
||||
row[columnCreationTime] = collection.creationTime;
|
||||
return row;
|
||||
}
|
||||
|
||||
SharedCollection _convertToSharedCollection(Map<String, dynamic> row) {
|
||||
return SharedCollection(
|
||||
row[columnID],
|
||||
row[columnOwnerID],
|
||||
row[columnEncryptedKey],
|
||||
row[columnName],
|
||||
Collection.typeFromString(row[columnType]),
|
||||
int.parse(row[columnCreationTime]),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/models/decryption_params.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
|
@ -20,6 +19,7 @@ class FilesDB {
|
|||
static final columnGeneratedID = '_id';
|
||||
static final columnUploadedFileID = 'uploaded_file_id';
|
||||
static final columnOwnerID = 'owner_id';
|
||||
static final columnCollectionID = 'collection_id';
|
||||
static final columnLocalID = 'local_id';
|
||||
static final columnTitle = 'title';
|
||||
static final columnDeviceFolder = 'device_folder';
|
||||
|
@ -32,9 +32,11 @@ class FilesDB {
|
|||
static final columnCreationTime = 'creation_time';
|
||||
static final columnModificationTime = 'modification_time';
|
||||
static final columnUpdationTime = 'updation_time';
|
||||
static final columnFileDecryptionParams = 'file_decryption_params';
|
||||
static final columnThumbnailDecryptionParams = 'thumbnail_decryption_params';
|
||||
static final columnMetadataDecryptionParams = 'metadata_decryption_params';
|
||||
static final columnEncryptedKey = 'encrypted_key';
|
||||
static final columnKeyDecryptionNonce = 'key_decryption_nonce';
|
||||
static final columnFileDecryptionHeader = 'file_decryption_header';
|
||||
static final columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
|
||||
static final columnMetadataDecryptionHeader = 'metadata_decryption_header';
|
||||
|
||||
// make this a singleton class
|
||||
FilesDB._privateConstructor();
|
||||
|
@ -65,20 +67,23 @@ class FilesDB {
|
|||
$columnLocalID TEXT,
|
||||
$columnUploadedFileID INTEGER,
|
||||
$columnOwnerID INTEGER,
|
||||
$columnCollectionID INTEGER,
|
||||
$columnTitle TEXT NOT NULL,
|
||||
$columnDeviceFolder TEXT NOT NULL,
|
||||
$columnLatitude REAL,
|
||||
$columnLongitude REAL,
|
||||
$columnFileType INTEGER,
|
||||
$columnRemoteFolderID INTEGER,
|
||||
$columnIsEncrypted INTEGER DEFAULT 0,
|
||||
$columnIsEncrypted INTEGER DEFAULT 1,
|
||||
$columnModificationTime TEXT NOT NULL,
|
||||
$columnEncryptedKey TEXT,
|
||||
$columnKeyDecryptionNonce TEXT,
|
||||
$columnFileDecryptionHeader TEXT,
|
||||
$columnThumbnailDecryptionHeader TEXT,
|
||||
$columnMetadataDecryptionHeader TEXT,
|
||||
$columnIsDeleted INTEGER DEFAULT 0,
|
||||
$columnCreationTime TEXT NOT NULL,
|
||||
$columnModificationTime TEXT NOT NULL,
|
||||
$columnUpdationTime TEXT,
|
||||
$columnFileDecryptionParams TEXT,
|
||||
$columnThumbnailDecryptionParams TEXT,
|
||||
$columnMetadataDecryptionParams TEXT
|
||||
$columnUpdationTime TEXT
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
@ -144,6 +149,20 @@ class FilesDB {
|
|||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getAllInCollection(
|
||||
int collectionID, int beforeCreationTime, int limit) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
table,
|
||||
where:
|
||||
'$columnCollectionID = ? AND $columnIsDeleted = 0 AND $columnCreationTime < ?',
|
||||
whereArgs: [collectionID, beforeCreationTime],
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: limit,
|
||||
);
|
||||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getFilesCreatedWithinDuration(
|
||||
int startCreationTime, int endCreationTime) async {
|
||||
final db = await instance.database;
|
||||
|
@ -226,19 +245,26 @@ class FilesDB {
|
|||
Future<int> update(
|
||||
int generatedID,
|
||||
int uploadedID,
|
||||
int ownerID,
|
||||
int collectionID,
|
||||
int updationTime,
|
||||
DecryptionParams fileDecryptionParams,
|
||||
DecryptionParams thumbnailDecryptionParams,
|
||||
DecryptionParams metadataDecryptionParams,
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
String fileDecryptionHeader,
|
||||
String thumbnailDecryptionHeader,
|
||||
String metadataDecryptionHeader,
|
||||
) async {
|
||||
final db = await instance.database;
|
||||
final values = new Map<String, dynamic>();
|
||||
values[columnUploadedFileID] = uploadedID;
|
||||
values[columnOwnerID] = ownerID;
|
||||
values[columnCollectionID] = collectionID;
|
||||
values[columnUpdationTime] = updationTime;
|
||||
values[columnFileDecryptionParams] = fileDecryptionParams.toJson();
|
||||
values[columnThumbnailDecryptionParams] =
|
||||
thumbnailDecryptionParams.toJson();
|
||||
values[columnMetadataDecryptionParams] = metadataDecryptionParams.toJson();
|
||||
values[columnEncryptedKey] = encryptedKey;
|
||||
values[columnKeyDecryptionNonce] = keyDecryptionNonce;
|
||||
values[columnFileDecryptionHeader] = fileDecryptionHeader;
|
||||
values[columnThumbnailDecryptionHeader] = thumbnailDecryptionHeader;
|
||||
values[columnMetadataDecryptionHeader] = metadataDecryptionHeader;
|
||||
return await db.update(
|
||||
table,
|
||||
values,
|
||||
|
@ -325,6 +351,22 @@ class FilesDB {
|
|||
}
|
||||
}
|
||||
|
||||
Future<File> getLatestFileInCollection(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
where: '$columnCollectionID =?',
|
||||
whereArgs: [collectionID],
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return _getFileFromRow(rows[0]);
|
||||
} else {
|
||||
throw ("No file found in collection " + collectionID.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> getLastSyncedFileInRemoteFolder(int folderID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
|
@ -369,6 +411,7 @@ class FilesDB {
|
|||
row[columnLocalID] = file.localID;
|
||||
row[columnUploadedFileID] = file.uploadedFileID;
|
||||
row[columnOwnerID] = file.ownerID;
|
||||
row[columnCollectionID] = file.collectionID;
|
||||
row[columnTitle] = file.title;
|
||||
row[columnDeviceFolder] = file.deviceFolder;
|
||||
if (file.location != null) {
|
||||
|
@ -390,16 +433,11 @@ class FilesDB {
|
|||
row[columnCreationTime] = file.creationTime;
|
||||
row[columnModificationTime] = file.modificationTime;
|
||||
row[columnUpdationTime] = file.updationTime;
|
||||
row[columnFileDecryptionParams] = file.fileDecryptionParams == null
|
||||
? null
|
||||
: file.fileDecryptionParams.toJson();
|
||||
row[columnThumbnailDecryptionParams] =
|
||||
file.thumbnailDecryptionParams == null
|
||||
? null
|
||||
: file.thumbnailDecryptionParams.toJson();
|
||||
row[columnMetadataDecryptionParams] = file.metadataDecryptionParams == null
|
||||
? null
|
||||
: file.metadataDecryptionParams.toJson();
|
||||
row[columnEncryptedKey] = file.encryptedKey;
|
||||
row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
|
||||
row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
|
||||
row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
|
||||
row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
|
||||
return row;
|
||||
}
|
||||
|
||||
|
@ -408,7 +446,8 @@ class FilesDB {
|
|||
file.generatedID = row[columnGeneratedID];
|
||||
file.localID = row[columnLocalID];
|
||||
file.uploadedFileID = row[columnUploadedFileID];
|
||||
file.ownerID = row[columnUploadedFileID];
|
||||
file.ownerID = row[columnOwnerID];
|
||||
file.collectionID = row[columnCollectionID];
|
||||
file.title = row[columnTitle];
|
||||
file.deviceFolder = row[columnDeviceFolder];
|
||||
if (row[columnLatitude] != null && row[columnLongitude] != null) {
|
||||
|
@ -422,12 +461,11 @@ class FilesDB {
|
|||
file.updationTime = row[columnUpdationTime] == null
|
||||
? -1
|
||||
: int.parse(row[columnUpdationTime]);
|
||||
file.fileDecryptionParams =
|
||||
DecryptionParams.fromJson(row[columnFileDecryptionParams]);
|
||||
file.thumbnailDecryptionParams =
|
||||
DecryptionParams.fromJson(row[columnThumbnailDecryptionParams]);
|
||||
file.metadataDecryptionParams =
|
||||
DecryptionParams.fromJson(row[columnMetadataDecryptionParams]);
|
||||
file.encryptedKey = row[columnEncryptedKey];
|
||||
file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
|
||||
file.fileDecryptionHeader = row[columnFileDecryptionHeader];
|
||||
file.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
|
||||
file.metadataDecryptionHeader = row[columnMetadataDecryptionHeader];
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/services/favorites_service.dart';
|
||||
import 'package:photos/services/folder_service.dart';
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/ui/home_widget.dart';
|
||||
|
@ -51,11 +50,8 @@ void _main() async {
|
|||
}
|
||||
|
||||
void _sync() async {
|
||||
FolderSharingService.instance.sync().catchError((e) {
|
||||
_logger.warning(e);
|
||||
});
|
||||
SyncService.instance.sync().catchError((e) {
|
||||
_logger.warning(e);
|
||||
SyncService.instance.sync().catchError((e, s) {
|
||||
_logger.severe("Sync error", e, s);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -85,6 +81,9 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
|
|||
hintColor: Colors.grey,
|
||||
accentColor: Colors.pink[400],
|
||||
buttonColor: Colors.pink,
|
||||
buttonTheme: ButtonThemeData().copyWith(
|
||||
buttonColor: Colors.pink,
|
||||
),
|
||||
toggleableActiveColor: Colors.pink[400],
|
||||
),
|
||||
home: HomeWidget(_title),
|
||||
|
|
146
lib/models/collection.dart
Normal file
146
lib/models/collection.dart
Normal file
|
@ -0,0 +1,146 @@
|
|||
import 'dart:convert';
|
||||
|
||||
class Collection {
|
||||
final int id;
|
||||
final int ownerID;
|
||||
final String encryptedKey;
|
||||
final String keyDecryptionNonce;
|
||||
final String name;
|
||||
final CollectionType type;
|
||||
final String encryptedPath;
|
||||
final String pathDecryptionNonce;
|
||||
final int creationTime;
|
||||
|
||||
Collection(
|
||||
this.id,
|
||||
this.ownerID,
|
||||
this.encryptedKey,
|
||||
this.keyDecryptionNonce,
|
||||
this.name,
|
||||
this.type,
|
||||
this.encryptedPath,
|
||||
this.pathDecryptionNonce,
|
||||
this.creationTime,
|
||||
);
|
||||
|
||||
Collection copyWith({
|
||||
int id,
|
||||
int ownerID,
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
String name,
|
||||
CollectionType type,
|
||||
String encryptedPath,
|
||||
String pathDecryptionNonce,
|
||||
int creationTime,
|
||||
List<String> sharees,
|
||||
}) {
|
||||
return Collection(
|
||||
id ?? this.id,
|
||||
ownerID ?? this.ownerID,
|
||||
encryptedKey ?? this.encryptedKey,
|
||||
keyDecryptionNonce ?? this.keyDecryptionNonce,
|
||||
name ?? this.name,
|
||||
type ?? this.type,
|
||||
encryptedPath ?? this.encryptedPath,
|
||||
encryptedPath ?? this.pathDecryptionNonce,
|
||||
creationTime ?? this.creationTime,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'ownerID': ownerID,
|
||||
'encryptedKey': encryptedKey,
|
||||
'keyDecryptionNonce': keyDecryptionNonce,
|
||||
'name': name,
|
||||
'type': typeToString(type),
|
||||
'creationTime': creationTime,
|
||||
'encryptedPath': encryptedPath,
|
||||
'pathDecryptionNonce': pathDecryptionNonce,
|
||||
};
|
||||
}
|
||||
|
||||
factory Collection.fromMap(Map<String, dynamic> map) {
|
||||
if (map == null) return null;
|
||||
|
||||
return Collection(
|
||||
map['id'],
|
||||
map['ownerID'],
|
||||
map['encryptedKey'],
|
||||
map['keyDecryptionNonce'],
|
||||
map['name'],
|
||||
typeFromString(map['type']),
|
||||
map['encryptedPath'],
|
||||
map['pathDecryptionNonce'],
|
||||
map['creationTime'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory Collection.fromJson(String source) =>
|
||||
Collection.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Collection(id: $id, ownerID: $ownerID, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, type: $type, encryptedPath: $encryptedPath, pathDecryptionNonce: $pathDecryptionNonce, creationTime: $creationTime)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) {
|
||||
if (identical(this, o)) return true;
|
||||
|
||||
return o is Collection &&
|
||||
o.id == id &&
|
||||
o.ownerID == ownerID &&
|
||||
o.encryptedKey == encryptedKey &&
|
||||
o.keyDecryptionNonce == keyDecryptionNonce &&
|
||||
o.name == name &&
|
||||
o.type == type &&
|
||||
o.encryptedPath == encryptedPath &&
|
||||
o.pathDecryptionNonce == pathDecryptionNonce &&
|
||||
o.creationTime == creationTime;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
ownerID.hashCode ^
|
||||
encryptedKey.hashCode ^
|
||||
keyDecryptionNonce.hashCode ^
|
||||
name.hashCode ^
|
||||
type.hashCode ^
|
||||
encryptedPath.hashCode ^
|
||||
pathDecryptionNonce.hashCode ^
|
||||
creationTime.hashCode;
|
||||
}
|
||||
|
||||
static CollectionType typeFromString(String type) {
|
||||
switch (type) {
|
||||
case "folder":
|
||||
return CollectionType.folder;
|
||||
case "favorites":
|
||||
return CollectionType.favorites;
|
||||
}
|
||||
return CollectionType.album;
|
||||
}
|
||||
|
||||
static String typeToString(CollectionType type) {
|
||||
switch (type) {
|
||||
case CollectionType.folder:
|
||||
return "folder";
|
||||
case CollectionType.favorites:
|
||||
return "favorites";
|
||||
default:
|
||||
return "album";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CollectionType {
|
||||
folder,
|
||||
favorites,
|
||||
album,
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
import 'dart:convert';
|
||||
|
||||
class DecryptionParams {
|
||||
final String encryptedKey;
|
||||
final String keyDecryptionNonce;
|
||||
String header;
|
||||
String nonce;
|
||||
|
||||
DecryptionParams({
|
||||
this.encryptedKey,
|
||||
this.keyDecryptionNonce,
|
||||
this.header,
|
||||
this.nonce,
|
||||
});
|
||||
|
||||
DecryptionParams copyWith({
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
String header,
|
||||
String nonce,
|
||||
}) {
|
||||
return DecryptionParams(
|
||||
encryptedKey: encryptedKey ?? this.encryptedKey,
|
||||
keyDecryptionNonce: keyDecryptionNonce ?? this.keyDecryptionNonce,
|
||||
header: header ?? this.header,
|
||||
nonce: nonce ?? this.nonce,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'encryptedKey': encryptedKey,
|
||||
'keyDecryptionNonce': keyDecryptionNonce,
|
||||
'header': header,
|
||||
'nonce': nonce,
|
||||
};
|
||||
}
|
||||
|
||||
factory DecryptionParams.fromMap(Map<String, dynamic> map) {
|
||||
if (map == null) return null;
|
||||
|
||||
return DecryptionParams(
|
||||
encryptedKey: map['encryptedKey'],
|
||||
keyDecryptionNonce: map['keyDecryptionNonce'],
|
||||
header: map['header'],
|
||||
nonce: map['nonce'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory DecryptionParams.fromJson(String source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return DecryptionParams.fromMap(json.decode(source));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DecryptionParams(encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, header: $header, nonce: $nonce)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) {
|
||||
if (identical(this, o)) return true;
|
||||
|
||||
return o is DecryptionParams &&
|
||||
o.encryptedKey == encryptedKey &&
|
||||
o.keyDecryptionNonce == keyDecryptionNonce &&
|
||||
o.header == header &&
|
||||
o.nonce == nonce;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return encryptedKey.hashCode ^
|
||||
keyDecryptionNonce.hashCode ^
|
||||
header.hashCode ^
|
||||
nonce.hashCode;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import 'package:photos/models/encryption_attribute.dart';
|
||||
|
||||
class EncryptedData {
|
||||
final EncryptionAttribute key;
|
||||
final EncryptionAttribute nonce;
|
||||
final EncryptionAttribute encryptedData;
|
||||
|
||||
EncryptedData(this.key, this.nonce, this.encryptedData);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import 'package:photos/models/encryption_attribute.dart';
|
||||
|
||||
class ChaChaAttributes {
|
||||
final EncryptionAttribute key;
|
||||
final EncryptionAttribute header;
|
||||
|
||||
ChaChaAttributes(this.key, this.header);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
|
||||
class EncryptionAttribute {
|
||||
String base64;
|
||||
Uint8List bytes;
|
||||
|
||||
EncryptionAttribute({this.base64, this.bytes}) {
|
||||
if (base64 != null) {
|
||||
this.bytes = Sodium.base642bin(base64);
|
||||
} else {
|
||||
this.base64 = Sodium.bin2base64(bytes);
|
||||
}
|
||||
}
|
||||
}
|
10
lib/models/encryption_result.dart
Normal file
10
lib/models/encryption_result.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
class EncryptionResult {
|
||||
final Uint8List encryptedData;
|
||||
final Uint8List key;
|
||||
final Uint8List header;
|
||||
final Uint8List nonce;
|
||||
|
||||
EncryptionResult({this.encryptedData, this.key, this.header, this.nonce});
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/models/decryption_params.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/models/location.dart';
|
||||
|
||||
|
@ -9,6 +8,7 @@ class File {
|
|||
int generatedID;
|
||||
int uploadedFileID;
|
||||
int ownerID;
|
||||
int collectionID;
|
||||
String localID;
|
||||
String title;
|
||||
String deviceFolder;
|
||||
|
@ -19,15 +19,18 @@ class File {
|
|||
int updationTime;
|
||||
Location location;
|
||||
FileType fileType;
|
||||
DecryptionParams fileDecryptionParams;
|
||||
DecryptionParams thumbnailDecryptionParams;
|
||||
DecryptionParams metadataDecryptionParams;
|
||||
String encryptedKey;
|
||||
String keyDecryptionNonce;
|
||||
String fileDecryptionHeader;
|
||||
String thumbnailDecryptionHeader;
|
||||
String metadataDecryptionHeader;
|
||||
|
||||
File();
|
||||
|
||||
File.fromJson(Map<String, dynamic> json) {
|
||||
uploadedFileID = json["id"];
|
||||
ownerID = json["ownerID"];
|
||||
collectionID = json["collectionID"];
|
||||
localID = json["deviceFileID"];
|
||||
deviceFolder = json["deviceFolder"];
|
||||
title = json["title"];
|
||||
|
@ -103,11 +106,8 @@ class File {
|
|||
}
|
||||
|
||||
String getDownloadUrl() {
|
||||
final api = isEncrypted ? "encrypted-files" : "files";
|
||||
return Configuration.instance.getHttpEndpoint() +
|
||||
"/" +
|
||||
api +
|
||||
"/download/" +
|
||||
"/files/download/" +
|
||||
uploadedFileID.toString() +
|
||||
"?token=" +
|
||||
Configuration.instance.getToken();
|
||||
|
@ -124,11 +124,8 @@ class File {
|
|||
}
|
||||
|
||||
String getThumbnailUrl() {
|
||||
final api = isEncrypted ? "encrypted-files" : "files";
|
||||
return Configuration.instance.getHttpEndpoint() +
|
||||
"/" +
|
||||
api +
|
||||
"/preview/" +
|
||||
"/files/preview/" +
|
||||
uploadedFileID.toString() +
|
||||
"?token=" +
|
||||
Configuration.instance.getToken();
|
||||
|
@ -137,10 +134,8 @@ class File {
|
|||
@override
|
||||
String toString() {
|
||||
return '''File(generatedId: $generatedID, uploadedFileId: $uploadedFileID,
|
||||
localId: $localID, title: $title, deviceFolder: $deviceFolder,
|
||||
fileDecryptionParams: $fileDecryptionParams,
|
||||
thumbnailDecryptionParams: $thumbnailDecryptionParams,
|
||||
metadataDecryptionParams: $metadataDecryptionParams,
|
||||
ownerID: $ownerID, collectionID: $collectionID,
|
||||
localId: $localID, title: $title, deviceFolder: $deviceFolder,
|
||||
location: $location, fileType: $fileType, creationTime: $creationTime,
|
||||
modificationTime: $modificationTime, updationTime: $updationTime)''';
|
||||
}
|
||||
|
|
96
lib/models/shared_collection.dart
Normal file
96
lib/models/shared_collection.dart
Normal file
|
@ -0,0 +1,96 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:photos/models/collection.dart';
|
||||
|
||||
class SharedCollection {
|
||||
final int id;
|
||||
final int ownerID;
|
||||
final String encryptedKey;
|
||||
final String name;
|
||||
final CollectionType type;
|
||||
final int creationTime;
|
||||
|
||||
SharedCollection(
|
||||
this.id,
|
||||
this.ownerID,
|
||||
this.encryptedKey,
|
||||
this.name,
|
||||
this.type,
|
||||
this.creationTime,
|
||||
);
|
||||
|
||||
SharedCollection copyWith({
|
||||
int id,
|
||||
int ownerID,
|
||||
String encryptedKey,
|
||||
String name,
|
||||
CollectionType type,
|
||||
int creationTime,
|
||||
}) {
|
||||
return SharedCollection(
|
||||
id ?? this.id,
|
||||
ownerID ?? this.ownerID,
|
||||
encryptedKey ?? this.encryptedKey,
|
||||
name ?? this.name,
|
||||
type ?? this.type,
|
||||
creationTime ?? this.creationTime,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'ownerID': ownerID,
|
||||
'encryptedKey': encryptedKey,
|
||||
'name': name,
|
||||
'type': Collection.typeToString(type),
|
||||
'creationTime': creationTime,
|
||||
};
|
||||
}
|
||||
|
||||
factory SharedCollection.fromMap(Map<String, dynamic> map) {
|
||||
if (map == null) return null;
|
||||
|
||||
return SharedCollection(
|
||||
map['id'],
|
||||
map['ownerID'],
|
||||
map['encryptedKey'],
|
||||
map['name'],
|
||||
Collection.typeFromString(map['type']),
|
||||
map['creationTime'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory SharedCollection.fromJson(String source) =>
|
||||
SharedCollection.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SharedCollection(id: $id, ownerID: $ownerID, encryptedKey: $encryptedKey, name: $name, type: $type, creationTime: $creationTime)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) {
|
||||
if (identical(this, o)) return true;
|
||||
|
||||
return o is SharedCollection &&
|
||||
o.id == id &&
|
||||
o.ownerID == ownerID &&
|
||||
o.encryptedKey == encryptedKey &&
|
||||
o.name == name &&
|
||||
o.type == type &&
|
||||
o.creationTime == creationTime;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
ownerID.hashCode ^
|
||||
encryptedKey.hashCode ^
|
||||
name.hashCode ^
|
||||
type.hashCode ^
|
||||
creationTime.hashCode;
|
||||
}
|
||||
}
|
204
lib/services/collections_service.dart
Normal file
204
lib/services/collections_service.dart
Normal file
|
@ -0,0 +1,204 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/db/collections_db.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/shared_collection.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
|
||||
class CollectionsService {
|
||||
final _logger = Logger("CollectionsService");
|
||||
|
||||
CollectionsDB _db;
|
||||
Configuration _config;
|
||||
final _localCollections = Map<String, Collection>();
|
||||
final _collectionIDToOwnedCollections = Map<int, Collection>();
|
||||
final _collectionIDToSharedCollections = Map<int, SharedCollection>();
|
||||
final _cachedKeys = Map<int, Uint8List>();
|
||||
|
||||
CollectionsService._privateConstructor() {
|
||||
_db = CollectionsDB.instance;
|
||||
_config = Configuration.instance;
|
||||
}
|
||||
|
||||
static final CollectionsService instance =
|
||||
CollectionsService._privateConstructor();
|
||||
|
||||
Future<void> sync() async {
|
||||
final lastCollectionCreationTime =
|
||||
await _db.getLastCollectionCreationTime();
|
||||
var collections =
|
||||
await getOwnedCollections(lastCollectionCreationTime ?? 0);
|
||||
await _db.insert(collections);
|
||||
collections = await _db.getAllCollections();
|
||||
for (final collection in collections) {
|
||||
_cacheCollectionAttributes(collection);
|
||||
}
|
||||
|
||||
final lastSharedCollectionCreationTime =
|
||||
await _db.getLastCollectionCreationTime();
|
||||
var sharedCollections =
|
||||
await getSharedCollections(lastSharedCollectionCreationTime ?? 0);
|
||||
await _db.insertSharedCollections(sharedCollections);
|
||||
sharedCollections = await _db.getAllSharedCollections();
|
||||
for (final collection in sharedCollections) {
|
||||
_collectionIDToSharedCollections[collection.id] = collection;
|
||||
}
|
||||
}
|
||||
|
||||
Collection getCollectionForPath(String path) {
|
||||
return _localCollections[path];
|
||||
}
|
||||
|
||||
Future<List<String>> getSharees(int collectionID) {
|
||||
return Dio()
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/sharees",
|
||||
queryParameters: {
|
||||
"collectionID": collectionID,
|
||||
},
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) {
|
||||
_logger.info(response.toString());
|
||||
final emails = List<String>();
|
||||
for (final email in response.data["emails"]) {
|
||||
emails.add(email);
|
||||
}
|
||||
return emails;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> share(int collectionID, String email, String publicKey) {
|
||||
final encryptedKey = CryptoUtil.sealSync(
|
||||
getCollectionKey(collectionID), Sodium.base642bin(publicKey));
|
||||
return Dio().post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/share",
|
||||
data: {
|
||||
"collectionID": collectionID,
|
||||
"email": email,
|
||||
"encryptedKey": Sodium.bin2base64(encryptedKey),
|
||||
},
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List getCollectionKey(int collectionID) {
|
||||
if (!_cachedKeys.containsKey(collectionID)) {
|
||||
var key;
|
||||
if (_collectionIDToOwnedCollections.containsKey(collectionID)) {
|
||||
final collection = _collectionIDToOwnedCollections[collectionID];
|
||||
final encryptedKey = Sodium.base642bin(collection.encryptedKey);
|
||||
key = CryptoUtil.decryptSync(encryptedKey, _config.getKey(),
|
||||
Sodium.base642bin(collection.keyDecryptionNonce));
|
||||
} else {
|
||||
final collection = _collectionIDToSharedCollections[collectionID];
|
||||
final encryptedKey = Sodium.base642bin(collection.encryptedKey);
|
||||
key = CryptoUtil.openSealSync(
|
||||
encryptedKey,
|
||||
Sodium.base642bin(_config.getKeyAttributes().publicKey),
|
||||
_config.getSecretKey());
|
||||
}
|
||||
_cachedKeys[collectionID] = key;
|
||||
}
|
||||
return _cachedKeys[collectionID];
|
||||
}
|
||||
|
||||
Future<List<Collection>> getOwnedCollections(int sinceTime) {
|
||||
return Dio()
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/owned",
|
||||
queryParameters: {
|
||||
"sinceTime": sinceTime,
|
||||
},
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) {
|
||||
final collections = List<Collection>();
|
||||
if (response != null) {
|
||||
final c = response.data["collections"];
|
||||
for (final collection in c) {
|
||||
collections.add(Collection.fromMap(collection));
|
||||
}
|
||||
}
|
||||
return collections;
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<SharedCollection>> getSharedCollections(int sinceTime) {
|
||||
return Dio()
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/shared",
|
||||
queryParameters: {
|
||||
"sinceTime": sinceTime,
|
||||
},
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) {
|
||||
final collections = List<SharedCollection>();
|
||||
if (response != null) {
|
||||
final c = response.data["collections"];
|
||||
for (final collection in c) {
|
||||
collections.add(SharedCollection.fromMap(collection));
|
||||
}
|
||||
}
|
||||
return collections;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Collection> getOrCreateForPath(String path) async {
|
||||
if (_localCollections.containsKey(path)) {
|
||||
return _localCollections[path];
|
||||
}
|
||||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
|
||||
final encryptedPath =
|
||||
CryptoUtil.encryptSync(utf8.encode(path), _config.getKey());
|
||||
final collection = await createCollection(Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
path,
|
||||
CollectionType.folder,
|
||||
Sodium.bin2base64(encryptedPath.encryptedData),
|
||||
Sodium.bin2base64(encryptedPath.nonce),
|
||||
null,
|
||||
));
|
||||
_cacheCollectionAttributes(collection);
|
||||
return collection;
|
||||
}
|
||||
|
||||
Future<Collection> createCollection(Collection collection) async {
|
||||
return Dio()
|
||||
.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/",
|
||||
data: collection.toMap(),
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) {
|
||||
return Collection.fromMap(response.data["collection"]);
|
||||
});
|
||||
}
|
||||
|
||||
void _cacheCollectionAttributes(Collection collection) {
|
||||
if (collection.ownerID == _config.getUserID()) {
|
||||
var path = utf8.decode(CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(collection.encryptedPath),
|
||||
_config.getKey(),
|
||||
Sodium.base642bin(collection.pathDecryptionNonce)));
|
||||
_localCollections[path] = collection;
|
||||
}
|
||||
_collectionIDToOwnedCollections[collection.id] = collection;
|
||||
getCollectionKey(collection.id);
|
||||
}
|
||||
}
|
|
@ -1,185 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/db/folders_db.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/remote_sync_event.dart';
|
||||
import 'package:photos/events/user_authenticated_event.dart';
|
||||
import 'package:photos/models/folder.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
|
||||
import '../core/event_bus.dart';
|
||||
|
||||
class FolderSharingService {
|
||||
final _logger = Logger("FolderSharingService");
|
||||
final _dio = Dio();
|
||||
static final _diffLimit = 100;
|
||||
|
||||
bool _isSyncInProgress = false;
|
||||
|
||||
FolderSharingService._privateConstructor() {
|
||||
Bus.instance.on<UserAuthenticatedEvent>().listen((event) {
|
||||
sync();
|
||||
});
|
||||
}
|
||||
|
||||
static final FolderSharingService instance =
|
||||
FolderSharingService._privateConstructor();
|
||||
|
||||
Future<void> sync() {
|
||||
_logger.info("Syncing...");
|
||||
if (_isSyncInProgress || !Configuration.instance.hasConfiguredAccount()) {
|
||||
return Future.value();
|
||||
}
|
||||
_isSyncInProgress = true;
|
||||
return getFolders().then((f) async {
|
||||
var folders = f.toSet();
|
||||
var currentFolders = await FoldersDB.instance.getFolders();
|
||||
for (final currentFolder in currentFolders) {
|
||||
if (!folders.contains(currentFolder)) {
|
||||
_logger.info("Folder deleted: " + currentFolder.toString());
|
||||
await FilesDB.instance.deleteFilesInRemoteFolder(currentFolder.id);
|
||||
await FoldersDB.instance.deleteFolder(currentFolder);
|
||||
}
|
||||
}
|
||||
for (final folder in folders) {
|
||||
if (folder.ownerID != Configuration.instance.getUserID()) {
|
||||
await syncDiff(folder);
|
||||
await FoldersDB.instance.putFolder(folder);
|
||||
}
|
||||
}
|
||||
Bus.instance.fire(RemoteSyncEvent(true));
|
||||
_isSyncInProgress = false;
|
||||
return Future.value();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncDiff(Folder folder) async {
|
||||
int lastSyncTimestamp = 0;
|
||||
try {
|
||||
File file =
|
||||
await FilesDB.instance.getLastSyncedFileInRemoteFolder(folder.id);
|
||||
lastSyncTimestamp = file.updationTime;
|
||||
} catch (e) {
|
||||
// Folder has never been synced
|
||||
}
|
||||
var diff = await getDiff(folder.id, lastSyncTimestamp, _diffLimit);
|
||||
for (File file in diff) {
|
||||
try {
|
||||
var existingPhoto =
|
||||
await FilesDB.instance.getMatchingRemoteFile(file.uploadedFileID);
|
||||
await FilesDB.instance.update(
|
||||
existingPhoto.generatedID,
|
||||
file.uploadedFileID,
|
||||
file.updationTime,
|
||||
file.fileDecryptionParams,
|
||||
file.thumbnailDecryptionParams,
|
||||
file.metadataDecryptionParams,
|
||||
);
|
||||
} catch (e) {
|
||||
await FilesDB.instance.insert(file);
|
||||
}
|
||||
}
|
||||
if (diff.length == _diffLimit) {
|
||||
await syncDiff(folder);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> getDiff(int folderId, int sinceTime, int limit) async {
|
||||
Response response = await _dio.get(
|
||||
Configuration.instance.getHttpEndpoint() +
|
||||
"/folders/diff/" +
|
||||
folderId.toString(),
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
queryParameters: {
|
||||
"sinceTime": sinceTime,
|
||||
"limit": limit,
|
||||
},
|
||||
).catchError((e) => _logger.severe(e));
|
||||
if (response != null) {
|
||||
return (response.data["diff"] as List).map((p) {
|
||||
File file = new File.fromJson(p);
|
||||
file.localID = null;
|
||||
file.remoteFolderID = folderId;
|
||||
return file;
|
||||
}).toList();
|
||||
} else {
|
||||
return List<File>();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Folder>> getFolders() async {
|
||||
return _dio
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/folders/",
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((foldersResponse) {
|
||||
return (foldersResponse.data as List)
|
||||
.map((f) => Folder.fromMap(f))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
Future<Folder> getFolder(String deviceFolder) async {
|
||||
return _dio
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/folders/folder/",
|
||||
queryParameters: {
|
||||
"deviceFolder": deviceFolder,
|
||||
},
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) {
|
||||
return Folder.fromMap(response.data);
|
||||
}).catchError((e) {
|
||||
try {
|
||||
return Folder(
|
||||
null,
|
||||
Configuration.instance.getEmail() + "s " + deviceFolder,
|
||||
Configuration.instance.getUserID(),
|
||||
deviceFolder,
|
||||
Set<int>(),
|
||||
null,
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<int, bool>> getSharingStatus(Folder folder) async {
|
||||
return _dio
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/users",
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) {
|
||||
final users = (response.data["users"] as List).toList();
|
||||
final result = Map<int, bool>();
|
||||
for (final user in users) {
|
||||
if (user["id"] != Configuration.instance.getUserID()) {
|
||||
result[user["id"]] = folder.sharedWith.contains(user["id"]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateFolder(Folder folder) {
|
||||
return _dio
|
||||
.put(Configuration.instance.getHttpEndpoint() + "/folders/",
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
data: folder.toMap())
|
||||
.then((response) => log(response.toString()))
|
||||
.catchError((error) => log(error.toString()));
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import 'package:photos/core/event_bus.dart';
|
|||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/photo_upload_event.dart';
|
||||
import 'package:photos/events/user_authenticated_event.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/utils/file_downloader.dart';
|
||||
import 'package:photos/repositories/file_repository.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
@ -29,7 +30,6 @@ class SyncService {
|
|||
Future<void> _existingSync;
|
||||
SharedPreferences _prefs;
|
||||
|
||||
static final _syncTimeKey = "sync_time";
|
||||
static final _encryptedFilesSyncTimeKey = "encrypted_files_sync_time";
|
||||
static final _dbUpdationTimeKey = "db_updation_time";
|
||||
static final _diffLimit = 100;
|
||||
|
@ -154,36 +154,15 @@ class SyncService {
|
|||
}
|
||||
|
||||
Future<void> _syncWithRemote() async {
|
||||
// TODO: Fix race conditions triggered due to concurrent syncs.
|
||||
// Add device_id/last_sync_timestamp to the upload request?
|
||||
if (!Configuration.instance.hasConfiguredAccount()) {
|
||||
return Future.error("Account not configured yet");
|
||||
}
|
||||
await _persistFilesDiff();
|
||||
await CollectionsService.instance.sync();
|
||||
await _persistEncryptedFilesDiff();
|
||||
await _uploadDiff();
|
||||
await _deletePhotosOnServer();
|
||||
}
|
||||
|
||||
Future<void> _persistFilesDiff() async {
|
||||
final diff = await _downloader.getFilesDiff(_getSyncTime(), _diffLimit);
|
||||
if (diff != null && diff.isNotEmpty) {
|
||||
await _storeDiff(diff, _syncTimeKey);
|
||||
FileRepository.instance.reloadFiles();
|
||||
if (diff.length == _diffLimit) {
|
||||
return await _persistFilesDiff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _getSyncTime() {
|
||||
var syncTime = _prefs.getInt(_syncTimeKey);
|
||||
if (syncTime == null) {
|
||||
syncTime = 0;
|
||||
}
|
||||
return syncTime;
|
||||
}
|
||||
|
||||
Future<void> _persistEncryptedFilesDiff() async {
|
||||
final diff = await _downloader.getEncryptedFilesDiff(
|
||||
_getEncryptedFilesSyncTime(), _diffLimit);
|
||||
|
@ -228,10 +207,14 @@ class SyncService {
|
|||
await _db.update(
|
||||
file.generatedID,
|
||||
uploadedFile.uploadedFileID,
|
||||
uploadedFile.ownerID,
|
||||
uploadedFile.collectionID,
|
||||
uploadedFile.updationTime,
|
||||
file.fileDecryptionParams,
|
||||
file.thumbnailDecryptionParams,
|
||||
file.metadataDecryptionParams,
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
file.fileDecryptionHeader,
|
||||
file.thumbnailDecryptionHeader,
|
||||
file.metadataDecryptionHeader,
|
||||
);
|
||||
Bus.instance.fire(PhotoUploadEvent(
|
||||
completed: i + 1, total: filesToBeUploaded.length));
|
||||
|
@ -245,20 +228,20 @@ class SyncService {
|
|||
Future _storeDiff(List<File> diff, String prefKey) async {
|
||||
for (File file in diff) {
|
||||
try {
|
||||
final existingPhoto = await _db.getMatchingFile(
|
||||
file.localID,
|
||||
file.title,
|
||||
file.deviceFolder,
|
||||
file.creationTime,
|
||||
file.modificationTime,
|
||||
final existingFile = await _db.getMatchingFile(file.localID, file.title,
|
||||
file.deviceFolder, file.creationTime, file.modificationTime,
|
||||
alternateTitle: getHEICFileNameForJPG(file));
|
||||
await _db.update(
|
||||
existingPhoto.generatedID,
|
||||
existingFile.generatedID,
|
||||
file.uploadedFileID,
|
||||
file.ownerID,
|
||||
file.collectionID,
|
||||
file.updationTime,
|
||||
file.fileDecryptionParams,
|
||||
file.thumbnailDecryptionParams,
|
||||
file.metadataDecryptionParams,
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
file.fileDecryptionHeader,
|
||||
file.thumbnailDecryptionHeader,
|
||||
file.metadataDecryptionHeader,
|
||||
);
|
||||
} catch (e) {
|
||||
file.localID = null; // File uploaded from a different device
|
||||
|
|
|
@ -47,6 +47,30 @@ class UserService {
|
|||
});
|
||||
}
|
||||
|
||||
Future<String> getPublicKey({String email, int userID}) async {
|
||||
final queryParams = Map<String, dynamic>();
|
||||
if (userID != null) {
|
||||
queryParams["userID"] = userID;
|
||||
} else {
|
||||
queryParams["email"] = email;
|
||||
}
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/users/public-key",
|
||||
queryParameters: queryParams,
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": Configuration.instance.getToken(),
|
||||
},
|
||||
),
|
||||
);
|
||||
return response.data["publicKey"];
|
||||
} on DioError catch (e) {
|
||||
_logger.info(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getCredentials(BuildContext context, String ott) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
final nothingToSeeHere = Center(child: Text("Nothing to see here! 👀"));
|
||||
|
||||
RaisedButton button(String text, {VoidCallback onPressed}) {
|
||||
return RaisedButton(
|
||||
child: Text(text),
|
||||
onPressed: onPressed,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:photos/core/event_bus.dart';
|
|||
import 'package:photos/events/user_authenticated_event.dart';
|
||||
import 'package:photos/repositories/file_repository.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/ui/email_entry_page.dart';
|
||||
import 'package:photos/ui/passphrase_entry_page.dart';
|
||||
import 'package:photos/ui/passphrase_reentry_page.dart';
|
||||
|
@ -18,7 +19,7 @@ import 'package:photos/utils/share_util.dart';
|
|||
enum GalleryAppBarType {
|
||||
homepage,
|
||||
local_folder,
|
||||
remote_folder,
|
||||
shared_collection,
|
||||
search_results,
|
||||
}
|
||||
|
||||
|
@ -87,18 +88,27 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
List<Widget> _getDefaultActions(BuildContext context) {
|
||||
List<Widget> actions = List<Widget>();
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SettingsPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
if (widget.type == GalleryAppBarType.homepage) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SettingsPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
} else if (widget.type == GalleryAppBarType.local_folder) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.share),
|
||||
onPressed: () {
|
||||
_showShareCollectionDialog();
|
||||
},
|
||||
));
|
||||
}
|
||||
} else {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.sync_disabled),
|
||||
|
@ -133,7 +143,12 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ShareFolderWidget(widget.title, widget.path);
|
||||
return ShareFolderWidget(
|
||||
widget.title,
|
||||
widget.path,
|
||||
collection:
|
||||
CollectionsService.instance.getCollectionForPath(widget.path),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -141,7 +156,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
List<Widget> _getActions(BuildContext context) {
|
||||
List<Widget> actions = List<Widget>();
|
||||
if (widget.selectedFiles.files.isNotEmpty) {
|
||||
if (widget.type != GalleryAppBarType.remote_folder &&
|
||||
if (widget.type != GalleryAppBarType.shared_collection &&
|
||||
widget.type != GalleryAppBarType.search_results) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
|
|
|
@ -21,6 +21,7 @@ import 'package:photos/ui/memories_widget.dart';
|
|||
import 'package:photos/ui/remote_folder_gallery_widget.dart';
|
||||
import 'package:photos/ui/search_page.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/ui/shared_collections_gallery.dart';
|
||||
import 'package:photos/utils/logging_util.dart';
|
||||
import 'package:shake/shake.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -38,7 +39,7 @@ class HomeWidget extends StatefulWidget {
|
|||
class _HomeWidgetState extends State<HomeWidget> {
|
||||
static final importantItemsFilter = ImportantItemsFilter();
|
||||
final _logger = Logger("HomeWidgetState");
|
||||
final _remoteFolderGalleryWidget = RemoteFolderGalleryWidget();
|
||||
final _sharedCollectionGallery = SharedCollectionGallery();
|
||||
final _deviceFolderGalleryWidget = DeviceFolderGalleryWidget();
|
||||
final _selectedFiles = SelectedFiles();
|
||||
final _memoriesWidget = MemoriesWidget();
|
||||
|
@ -80,7 +81,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
? _getMainGalleryWidget()
|
||||
: LoadingPhotosWidget(),
|
||||
_deviceFolderGalleryWidget,
|
||||
_remoteFolderGalleryWidget,
|
||||
_sharedCollectionGallery,
|
||||
],
|
||||
index: _selectedNavBarItem,
|
||||
),
|
||||
|
|
|
@ -91,7 +91,6 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
child: Text(
|
||||
"Verify",
|
||||
),
|
||||
color: Colors.pink,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/services/folder_service.dart';
|
||||
import 'package:photos/models/folder.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/ui/gallery.dart';
|
||||
|
@ -27,13 +26,13 @@ class _RemoteFolderPageState extends State<RemoteFolderPage> {
|
|||
? DateTime.now().microsecondsSinceEpoch
|
||||
: lastFile.creationTime,
|
||||
limit),
|
||||
onRefresh: () => FolderSharingService.instance.syncDiff(widget.folder),
|
||||
// onRefresh: () => FolderSharingService.instance.syncDiff(widget.folder),
|
||||
tagPrefix: "remote_folder",
|
||||
selectedFiles: _selectedFiles,
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: GalleryAppBarWidget(
|
||||
GalleryAppBarType.remote_folder,
|
||||
GalleryAppBarType.shared_collection,
|
||||
widget.folder.name,
|
||||
_selectedFiles,
|
||||
widget.folder.deviceFolder,
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:photos/services/folder_service.dart';
|
||||
import 'package:photos/models/folder.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/ui/common_elements.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/email_util.dart';
|
||||
import 'package:photos/utils/share_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
||||
class ShareFolderWidget extends StatefulWidget {
|
||||
final String title;
|
||||
final String path;
|
||||
final Collection collection;
|
||||
|
||||
const ShareFolderWidget(
|
||||
this.title,
|
||||
this.path, {
|
||||
this.collection,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -22,19 +30,15 @@ class ShareFolderWidget extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ShareFolderWidgetState extends State<ShareFolderWidget> {
|
||||
Folder _folder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Map<int, bool>>(
|
||||
future:
|
||||
FolderSharingService.instance.getFolder(widget.path).then((folder) {
|
||||
_folder = folder;
|
||||
return FolderSharingService.instance.getSharingStatus(folder);
|
||||
}),
|
||||
return FutureBuilder<List<String>>(
|
||||
future: widget.collection == null
|
||||
? Future.value(List<String>())
|
||||
: CollectionsService.instance.getSharees(widget.collection.id),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return _getSharingDialog(snapshot.data);
|
||||
return SharingDialog(widget.collection, snapshot.data);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
} else {
|
||||
|
@ -43,102 +47,146 @@ class _ShareFolderWidgetState extends State<ShareFolderWidget> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getSharingDialog(Map<int, bool> sharingStatus) {
|
||||
return AlertDialog(
|
||||
title: Text('Sharing'),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
// SharingCheckboxWidget(sharingStatus),
|
||||
SharingWidget(["vishnu@ente.io", "shanthy@ente.io"]),
|
||||
],
|
||||
),
|
||||
),
|
||||
// actions: <Widget>[
|
||||
// FlatButton(
|
||||
// child: Text("Save"),
|
||||
// onPressed: () async {
|
||||
// var sharedWith = Set<int>();
|
||||
// for (var user in sharingStatus.keys) {
|
||||
// if (sharingStatus[user]) {
|
||||
// sharedWith.add(user);
|
||||
// }
|
||||
// }
|
||||
// _folder.sharedWith.clear();
|
||||
// _folder.sharedWith.addAll(sharedWith);
|
||||
// await FolderSharingService.instance.updateFolder(_folder);
|
||||
// showToast("Sharing configuration updated successfully.");
|
||||
// Navigator.of(context).pop();
|
||||
// },
|
||||
// ),
|
||||
// ],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SharingWidget extends StatefulWidget {
|
||||
final List<String> emails;
|
||||
SharingWidget(this.emails, {Key key}) : super(key: key);
|
||||
class SharingDialog extends StatefulWidget {
|
||||
final Collection collection;
|
||||
final List<String> sharees;
|
||||
|
||||
SharingDialog(this.collection, this.sharees, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SharingWidgetState createState() => _SharingWidgetState();
|
||||
_SharingDialogState createState() => _SharingDialogState();
|
||||
}
|
||||
|
||||
class _SharingWidgetState extends State<SharingWidget> {
|
||||
class _SharingDialogState extends State<SharingDialog> {
|
||||
bool _showEntryField = false;
|
||||
List<String> _emails;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_emails = widget.emails;
|
||||
super.initState();
|
||||
}
|
||||
List<String> _sharees;
|
||||
String _email;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_sharees = widget.sharees;
|
||||
final children = List<Widget>();
|
||||
for (final email in _emails) {
|
||||
children.add(EmailItemWidget(email));
|
||||
if (!_showEntryField &&
|
||||
(widget.collection == null || _sharees.length == 0)) {
|
||||
children.add(Text("Click the + button to share this folder."));
|
||||
} else {
|
||||
for (final email in _sharees) {
|
||||
children.add(EmailItemWidget(email));
|
||||
}
|
||||
}
|
||||
if (_showEntryField) {
|
||||
children.add(TextField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "email@your-friend.com",
|
||||
),
|
||||
autofocus: true,
|
||||
onSubmitted: (s) {
|
||||
final progressDialog = createProgressDialog(context, "Sharing...");
|
||||
progressDialog.show();
|
||||
Future.delayed(Duration(milliseconds: 1000), () {
|
||||
progressDialog.hide();
|
||||
showToast("Shared with " + s + ".");
|
||||
setState(() {
|
||||
_emails.add(s);
|
||||
_showEntryField = false;
|
||||
});
|
||||
onChanged: (s) {
|
||||
setState(() {
|
||||
_email = s;
|
||||
});
|
||||
},
|
||||
onSubmitted: (s) {
|
||||
_addEmailToCollection(context);
|
||||
},
|
||||
));
|
||||
}
|
||||
children.add(Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
));
|
||||
children.add(Container(
|
||||
width: 220,
|
||||
child: OutlineButton(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
if (!_showEntryField) {
|
||||
children.add(Container(
|
||||
width: 220,
|
||||
child: OutlineButton(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showEntryField = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
));
|
||||
} else {
|
||||
children.add(Container(
|
||||
width: 220,
|
||||
child: button(
|
||||
"Add",
|
||||
onPressed: () async {
|
||||
await _addEmailToCollection(context);
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text("Sharing"),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
children: children,
|
||||
)),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showEntryField = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
));
|
||||
var column = Column(
|
||||
children: children,
|
||||
);
|
||||
return column;
|
||||
}
|
||||
|
||||
Future<void> _addEmailToCollection(BuildContext context) async {
|
||||
if (!isValidEmail(_email)) {
|
||||
showErrorDialog(context, "Invalid email address",
|
||||
"Please enter a valid email address");
|
||||
return;
|
||||
}
|
||||
final dialog = createProgressDialog(context, "Searching for user...");
|
||||
await dialog.show();
|
||||
final publicKey = await UserService.instance.getPublicKey(email: _email);
|
||||
await dialog.hide();
|
||||
if (publicKey == null) {
|
||||
Navigator.of(context).pop();
|
||||
final dialog = AlertDialog(
|
||||
title: Text("Invite to ente?"),
|
||||
content: Text("Looks like " +
|
||||
_email +
|
||||
" hasn't signed up for ente yet. Would you like to invite them?"),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text("Invite"),
|
||||
onPressed: () {
|
||||
shareText(
|
||||
"Hey, I've got some really nice photos to share. Please install ente.io so that I can share them privately.");
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return dialog;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (widget.collection == null) {
|
||||
log("Collection is null");
|
||||
// TODO: Create collection
|
||||
// TODO: Add files to collection
|
||||
}
|
||||
CollectionsService.instance
|
||||
.share(widget.collection.id, _email, publicKey)
|
||||
.then((value) {
|
||||
setState(() {
|
||||
_sharees.add(_email);
|
||||
_showEntryField = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,63 +200,21 @@ class EmailItemWidget extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
email,
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
Icon(
|
||||
Icons.remove_circle_outline,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SharingCheckboxWidget extends StatefulWidget {
|
||||
final Map<int, bool> sharingStatus;
|
||||
|
||||
const SharingCheckboxWidget(
|
||||
this.sharingStatus, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SharingCheckboxWidgetState createState() => _SharingCheckboxWidgetState();
|
||||
}
|
||||
|
||||
class _SharingCheckboxWidgetState extends State<SharingCheckboxWidget> {
|
||||
Map<int, bool> _sharingStatus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_sharingStatus = widget.sharingStatus;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final checkboxes = List<Widget>();
|
||||
for (final user in _sharingStatus.keys) {
|
||||
checkboxes.add(Row(
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
value: _sharingStatus[user],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_sharingStatus[user] = value;
|
||||
});
|
||||
}),
|
||||
Text(user.toString()),
|
||||
],
|
||||
));
|
||||
}
|
||||
return Column(children: checkboxes);
|
||||
padding: const EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
email,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.delete_forever,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
42
lib/ui/shared_collection_page.dart
Normal file
42
lib/ui/shared_collection_page.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/models/shared_collection.dart';
|
||||
import 'package:photos/ui/gallery.dart';
|
||||
import 'package:photos/ui/gallery_app_bar_widget.dart';
|
||||
|
||||
class SharedCollectionPage extends StatefulWidget {
|
||||
final SharedCollection collection;
|
||||
|
||||
const SharedCollectionPage(this.collection, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SharedCollectionPageState createState() => _SharedCollectionPageState();
|
||||
}
|
||||
|
||||
class _SharedCollectionPageState extends State<SharedCollectionPage> {
|
||||
final _selectedFiles = SelectedFiles();
|
||||
|
||||
@override
|
||||
Widget build(Object context) {
|
||||
var gallery = Gallery(
|
||||
asyncLoader: (lastFile, limit) => FilesDB.instance.getAllInCollection(
|
||||
widget.collection.id,
|
||||
lastFile == null
|
||||
? DateTime.now().microsecondsSinceEpoch
|
||||
: lastFile.creationTime,
|
||||
limit),
|
||||
// onRefresh: () => FolderSharingService.instance.syncDiff(widget.folder),
|
||||
tagPrefix: "shared_collection",
|
||||
selectedFiles: _selectedFiles,
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: GalleryAppBarWidget(
|
||||
GalleryAppBarType.shared_collection,
|
||||
widget.collection.name,
|
||||
_selectedFiles,
|
||||
),
|
||||
body: gallery,
|
||||
);
|
||||
}
|
||||
}
|
146
lib/ui/shared_collections_gallery.dart
Normal file
146
lib/ui/shared_collections_gallery.dart
Normal file
|
@ -0,0 +1,146 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/collections_db.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/remote_sync_event.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/shared_collection.dart';
|
||||
import 'package:photos/ui/common_elements.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/ui/shared_collection_page.dart';
|
||||
import 'package:photos/ui/thumbnail_widget.dart';
|
||||
|
||||
class SharedCollectionGallery extends StatefulWidget {
|
||||
const SharedCollectionGallery({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SharedCollectionGalleryState createState() =>
|
||||
_SharedCollectionGalleryState();
|
||||
}
|
||||
|
||||
class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
|
||||
Logger _logger = Logger("SharedCollectionGallery");
|
||||
StreamSubscription<RemoteSyncEvent> _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_subscription = Bus.instance.on<RemoteSyncEvent>().listen((event) {
|
||||
if (event.success) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<SharedCollectionWithThumbnail>>(
|
||||
future: CollectionsDB.instance
|
||||
.getAllSharedCollections()
|
||||
.then((collections) async {
|
||||
final c = List<SharedCollectionWithThumbnail>();
|
||||
for (final collection in collections) {
|
||||
var thumbnail;
|
||||
try {
|
||||
thumbnail =
|
||||
await FilesDB.instance.getLatestFileInCollection(collection.id);
|
||||
} catch (e) {
|
||||
_logger.warning(e.toString());
|
||||
}
|
||||
c.add(SharedCollectionWithThumbnail(collection, thumbnail));
|
||||
}
|
||||
return c;
|
||||
}),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
if (snapshot.data.isEmpty) {
|
||||
return nothingToSeeHere;
|
||||
} else {
|
||||
return _getSharedCollectionsGallery(snapshot.data);
|
||||
}
|
||||
} else if (snapshot.hasError) {
|
||||
_logger.shout(snapshot.error);
|
||||
return Center(child: Text(snapshot.error.toString()));
|
||||
} else {
|
||||
return loadWidget;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getSharedCollectionsGallery(
|
||||
List<SharedCollectionWithThumbnail> collections) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 24),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
physics: ScrollPhysics(), // to disable GridView's scrolling
|
||||
itemBuilder: (context, index) {
|
||||
return _buildCollection(context, collections[index]);
|
||||
},
|
||||
itemCount: collections.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollection(
|
||||
BuildContext context, SharedCollectionWithThumbnail c) {
|
||||
_logger.info("Building collection " + c.collection.toString());
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
child: c.thumbnail ==
|
||||
null // When the user has shared a folder without photos
|
||||
? Icon(Icons.error)
|
||||
: Hero(
|
||||
tag: "shared_collection" + c.thumbnail.tag(),
|
||||
child: ThumbnailWidget(c.thumbnail)),
|
||||
height: 150,
|
||||
width: 150,
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(2)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
c.collection.name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final page = SharedCollectionPage(c.collection);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SharedCollectionWithThumbnail {
|
||||
final SharedCollection collection;
|
||||
final File thumbnail;
|
||||
|
||||
SharedCollectionWithThumbnail(this.collection, this.thumbnail);
|
||||
}
|
|
@ -5,10 +5,7 @@ import 'dart:io' as io;
|
|||
import 'package:computer/computer.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:photos/models/encrypted_data_attributes.dart';
|
||||
import 'package:photos/models/encrypted_file_attributes.dart';
|
||||
import 'package:photos/models/encryption_attribute.dart';
|
||||
import 'package:photos/models/encryption_result.dart';
|
||||
|
||||
final int encryptionChunkSize = 4 * 1024 * 1024;
|
||||
final int decryptionChunkSize =
|
||||
|
@ -32,7 +29,7 @@ bool cryptoPwhashStrVerify(Map<String, dynamic> args) {
|
|||
return Sodium.cryptoPwhashStrVerify(args["hash"], args["input"]) == 0;
|
||||
}
|
||||
|
||||
ChaChaAttributes chachaEncrypt(Map<String, dynamic> args) {
|
||||
EncryptionResult chachaEncryptFile(Map<String, dynamic> args) {
|
||||
final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
|
||||
final logger = Logger("ChaChaEncrypt");
|
||||
final sourceFile = io.File(args["sourceFilePath"]);
|
||||
|
@ -63,8 +60,7 @@ ChaChaAttributes chachaEncrypt(Map<String, dynamic> args) {
|
|||
logger.info("Encryption time: " +
|
||||
(DateTime.now().millisecondsSinceEpoch - encryptionStartTime).toString());
|
||||
|
||||
return ChaChaAttributes(EncryptionAttribute(bytes: key),
|
||||
EncryptionAttribute(bytes: initPushResult.header));
|
||||
return EncryptionResult(key: key, header: initPushResult.header);
|
||||
}
|
||||
|
||||
void chachaDecrypt(Map<String, dynamic> args) {
|
||||
|
@ -100,65 +96,89 @@ void chachaDecrypt(Map<String, dynamic> args) {
|
|||
}
|
||||
|
||||
class CryptoUtil {
|
||||
static Future<EncryptedData> encrypt(Uint8List source,
|
||||
{Uint8List key}) async {
|
||||
if (key == null) {
|
||||
key = Sodium.cryptoSecretboxKeygen();
|
||||
}
|
||||
static EncryptionResult encryptSync(Uint8List source, Uint8List key) {
|
||||
final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes);
|
||||
|
||||
final args = Map<String, dynamic>();
|
||||
args["source"] = source;
|
||||
args["nonce"] = nonce;
|
||||
args["key"] = key;
|
||||
final encryptedData =
|
||||
await Computer().compute(cryptoSecretboxEasy, param: args);
|
||||
|
||||
return EncryptedData(
|
||||
EncryptionAttribute(bytes: key),
|
||||
EncryptionAttribute(bytes: nonce),
|
||||
EncryptionAttribute(bytes: encryptedData));
|
||||
final encryptedData = cryptoSecretboxEasy(args);
|
||||
return EncryptionResult(
|
||||
key: key, nonce: nonce, encryptedData: encryptedData);
|
||||
}
|
||||
|
||||
static Future<Uint8List> decrypt(
|
||||
Uint8List cipher, Uint8List key, Uint8List nonce,
|
||||
{bool background = false}) async {
|
||||
Uint8List cipher,
|
||||
Uint8List key,
|
||||
Uint8List nonce,
|
||||
) async {
|
||||
final args = Map<String, dynamic>();
|
||||
args["cipher"] = cipher;
|
||||
args["nonce"] = nonce;
|
||||
args["key"] = key;
|
||||
if (background) {
|
||||
return Computer().compute(cryptoSecretboxOpenEasy, param: args);
|
||||
} else {
|
||||
return cryptoSecretboxOpenEasy(args);
|
||||
}
|
||||
return Computer().compute(cryptoSecretboxOpenEasy, param: args);
|
||||
}
|
||||
|
||||
static Future<ChaChaAttributes> encryptFile(
|
||||
static Uint8List decryptSync(
|
||||
Uint8List cipher,
|
||||
Uint8List key,
|
||||
Uint8List nonce,
|
||||
) {
|
||||
final args = Map<String, dynamic>();
|
||||
args["cipher"] = cipher;
|
||||
args["nonce"] = nonce;
|
||||
args["key"] = key;
|
||||
return cryptoSecretboxOpenEasy(args);
|
||||
}
|
||||
|
||||
static EncryptionResult encryptChaCha(Uint8List source, Uint8List key) {
|
||||
final initPushResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
|
||||
final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
|
||||
initPushResult.state,
|
||||
source,
|
||||
null,
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305TagFinal);
|
||||
return EncryptionResult(
|
||||
encryptedData: encryptedData, header: initPushResult.header);
|
||||
}
|
||||
|
||||
static Uint8List decryptChaCha(
|
||||
Uint8List source, Uint8List key, Uint8List header) {
|
||||
final pullState =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305InitPull(header, key);
|
||||
final pullResult =
|
||||
Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, source, null);
|
||||
return pullResult.m;
|
||||
}
|
||||
|
||||
static Future<EncryptionResult> encryptFile(
|
||||
String sourceFilePath,
|
||||
String destinationFilePath,
|
||||
) {
|
||||
final args = Map<String, dynamic>();
|
||||
args["sourceFilePath"] = sourceFilePath;
|
||||
args["destinationFilePath"] = destinationFilePath;
|
||||
return Computer().compute(chachaEncrypt, param: args);
|
||||
return Computer().compute(chachaEncryptFile, param: args);
|
||||
}
|
||||
|
||||
static Future<void> decryptFile(
|
||||
String sourceFilePath,
|
||||
String destinationFilePath,
|
||||
ChaChaAttributes attributes,
|
||||
Uint8List header,
|
||||
Uint8List key,
|
||||
) {
|
||||
final args = Map<String, dynamic>();
|
||||
args["sourceFilePath"] = sourceFilePath;
|
||||
args["destinationFilePath"] = destinationFilePath;
|
||||
args["header"] = attributes.header.bytes;
|
||||
args["key"] = attributes.key.bytes;
|
||||
args["header"] = header;
|
||||
args["key"] = key;
|
||||
return Computer().compute(chachaDecrypt, param: args);
|
||||
}
|
||||
|
||||
static Uint8List generateMasterKey() {
|
||||
return Sodium.cryptoKdfKeygen();
|
||||
static Uint8List generateKey() {
|
||||
return Sodium.cryptoSecretboxKeygen();
|
||||
}
|
||||
|
||||
static Uint8List getSaltToDeriveKey() {
|
||||
|
@ -194,4 +214,13 @@ class CryptoUtil {
|
|||
static Future<KeyPair> generateKeyPair() async {
|
||||
return Sodium.cryptoBoxKeypair();
|
||||
}
|
||||
|
||||
static Uint8List openSealSync(
|
||||
Uint8List input, Uint8List publicKey, Uint8List secretKey) {
|
||||
return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
|
||||
}
|
||||
|
||||
static Uint8List sealSync(Uint8List input, Uint8List publicKey) {
|
||||
return Sodium.cryptoBoxSeal(input, publicKey);
|
||||
}
|
||||
}
|
||||
|
|
5
lib/utils/email_util.dart
Normal file
5
lib/utils/email_util.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
bool isValidEmail(String email) {
|
||||
return RegExp(
|
||||
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
|
||||
.hasMatch(email);
|
||||
}
|
|
@ -6,9 +6,9 @@ import 'package:logging/logging.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/remote_sync_event.dart';
|
||||
import 'package:photos/models/decryption_params.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
|
||||
class DiffFetcher {
|
||||
final _logger = Logger("FileDownloader");
|
||||
|
@ -17,9 +17,10 @@ class DiffFetcher {
|
|||
Future<List<File>> getEncryptedFilesDiff(int lastSyncTime, int limit) async {
|
||||
return _dio
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/encrypted-files/diff",
|
||||
Configuration.instance.getHttpEndpoint() + "/files/diff",
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
queryParameters: {
|
||||
"token": Configuration.instance.getToken(),
|
||||
"sinceTime": lastSyncTime,
|
||||
"limit": limit,
|
||||
},
|
||||
|
@ -34,23 +35,23 @@ class DiffFetcher {
|
|||
final file = File();
|
||||
file.uploadedFileID = item["id"];
|
||||
file.ownerID = item["ownerID"];
|
||||
file.collectionID = item["collectionID"];
|
||||
if (file.collectionID == 4) {
|
||||
_logger.info("Found");
|
||||
}
|
||||
file.updationTime = item["updationTime"];
|
||||
file.isEncrypted = true;
|
||||
file.fileDecryptionParams =
|
||||
DecryptionParams.fromMap(item["file"]["decryptionParams"]);
|
||||
file.thumbnailDecryptionParams = DecryptionParams.fromMap(
|
||||
item["thumbnail"]["decryptionParams"]);
|
||||
file.metadataDecryptionParams = DecryptionParams.fromMap(
|
||||
item["metadata"]["decryptionParams"]);
|
||||
final metadataDecryptionKey = await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(file.metadataDecryptionParams.encryptedKey),
|
||||
Configuration.instance.getKey(),
|
||||
Sodium.base642bin(
|
||||
file.metadataDecryptionParams.keyDecryptionNonce));
|
||||
final encodedMetadata = await CryptoUtil.decrypt(
|
||||
file.encryptedKey = item["encryptedKey"];
|
||||
file.keyDecryptionNonce = item["keyDecryptionNonce"];
|
||||
file.fileDecryptionHeader = item["file"]["decryptionHeader"];
|
||||
file.thumbnailDecryptionHeader =
|
||||
item["thumbnail"]["decryptionHeader"];
|
||||
file.metadataDecryptionHeader =
|
||||
item["metadata"]["decryptionHeader"];
|
||||
final encodedMetadata = CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(item["metadata"]["encryptedData"]),
|
||||
metadataDecryptionKey,
|
||||
Sodium.base642bin(file.metadataDecryptionParams.nonce),
|
||||
decryptFileKey(file),
|
||||
Sodium.base642bin(file.metadataDecryptionHeader),
|
||||
);
|
||||
Map<String, dynamic> metadata =
|
||||
jsonDecode(utf8.decode(encodedMetadata));
|
||||
|
@ -63,25 +64,4 @@ class DiffFetcher {
|
|||
return files;
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<File>> getFilesDiff(int lastSyncTime, int limit) async {
|
||||
Response response = await _dio.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/files/diff",
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
queryParameters: {
|
||||
"sinceTime": lastSyncTime,
|
||||
"limit": limit,
|
||||
},
|
||||
).catchError((e) => _logger.severe(e));
|
||||
if (response != null) {
|
||||
Bus.instance.fire(RemoteSyncEvent(true));
|
||||
return (response.data["diff"] as List)
|
||||
.map((file) => new File.fromJson(file))
|
||||
.toList();
|
||||
} else {
|
||||
Bus.instance.fire(RemoteSyncEvent(false));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io' as io;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/models/decryption_params.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/upload_url.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_name_util.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
|
@ -18,8 +19,7 @@ class FileUploader {
|
|||
Future<UploadURL> getUploadURL() {
|
||||
return Dio()
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() +
|
||||
"/encrypted-files/upload-url",
|
||||
Configuration.instance.getHttpEndpoint() + "/files/upload-url",
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
|
@ -44,6 +44,10 @@ class FileUploader {
|
|||
Future<File> encryptAndUploadFile(File file) async {
|
||||
_logger.info("Uploading " + file.toString());
|
||||
|
||||
file.collectionID = (await CollectionsService.instance
|
||||
.getOrCreateForPath(file.deviceFolder))
|
||||
.id;
|
||||
|
||||
final encryptedFileName = file.generatedID.toString() + ".encrypted";
|
||||
final tempDirectory = Configuration.instance.getTempDirectory();
|
||||
final encryptedFilePath = tempDirectory + encryptedFileName;
|
||||
|
@ -56,16 +60,6 @@ class FileUploader {
|
|||
final fileUploadURL = await getUploadURL();
|
||||
String fileObjectKey = await putFile(fileUploadURL, encryptedFile);
|
||||
|
||||
final encryptedFileKey = await CryptoUtil.encrypt(
|
||||
fileAttributes.key.bytes,
|
||||
key: Configuration.instance.getKey(),
|
||||
);
|
||||
final fileDecryptionParams = DecryptionParams(
|
||||
encryptedKey: encryptedFileKey.encryptedData.base64,
|
||||
keyDecryptionNonce: encryptedFileKey.nonce.base64,
|
||||
header: fileAttributes.header.base64,
|
||||
);
|
||||
|
||||
final thumbnailData = (await (await file.getAsset()).thumbDataWithSize(
|
||||
THUMBNAIL_LARGE_SIZE,
|
||||
THUMBNAIL_LARGE_SIZE,
|
||||
|
@ -74,52 +68,53 @@ class FileUploader {
|
|||
final encryptedThumbnailName =
|
||||
file.generatedID.toString() + "_thumbnail.encrypted";
|
||||
final encryptedThumbnailPath = tempDirectory + encryptedThumbnailName;
|
||||
final encryptedThumbnail = await CryptoUtil.encrypt(thumbnailData);
|
||||
final encryptedThumbnail =
|
||||
CryptoUtil.encryptChaCha(thumbnailData, fileAttributes.key);
|
||||
io.File(encryptedThumbnailPath)
|
||||
.writeAsBytesSync(encryptedThumbnail.encryptedData.bytes);
|
||||
.writeAsBytesSync(encryptedThumbnail.encryptedData);
|
||||
|
||||
final thumbnailUploadURL = await getUploadURL();
|
||||
String thumbnailObjectKey =
|
||||
await putFile(thumbnailUploadURL, io.File(encryptedThumbnailPath));
|
||||
|
||||
final encryptedThumbnailKey = await CryptoUtil.encrypt(
|
||||
encryptedThumbnail.key.bytes,
|
||||
key: Configuration.instance.getKey(),
|
||||
);
|
||||
final thumbnailDecryptionParams = DecryptionParams(
|
||||
encryptedKey: encryptedThumbnailKey.encryptedData.base64,
|
||||
keyDecryptionNonce: encryptedThumbnailKey.nonce.base64,
|
||||
nonce: encryptedThumbnail.nonce.base64,
|
||||
final encryptedMetadataData = CryptoUtil.encryptChaCha(
|
||||
utf8.encode(jsonEncode(file.getMetadata())), fileAttributes.key);
|
||||
|
||||
final encryptedFileKeyData = CryptoUtil.encryptSync(
|
||||
fileAttributes.key,
|
||||
CollectionsService.instance.getCollectionKey(file.collectionID),
|
||||
);
|
||||
|
||||
final metadata = jsonEncode(file.getMetadata());
|
||||
final encryptedMetadata = await CryptoUtil.encrypt(utf8.encode(metadata));
|
||||
final encryptedMetadataKey = await CryptoUtil.encrypt(
|
||||
encryptedMetadata.key.bytes,
|
||||
key: Configuration.instance.getKey(),
|
||||
);
|
||||
final metadataDecryptionParams = DecryptionParams(
|
||||
encryptedKey: encryptedMetadataKey.encryptedData.base64,
|
||||
keyDecryptionNonce: encryptedMetadataKey.nonce.base64,
|
||||
nonce: encryptedMetadata.nonce.base64,
|
||||
);
|
||||
final encryptedKey = Sodium.bin2base64(encryptedFileKeyData.encryptedData);
|
||||
final keyDecryptionNonce = Sodium.bin2base64(encryptedFileKeyData.nonce);
|
||||
final fileDecryptionHeader = Sodium.bin2base64(fileAttributes.header);
|
||||
final thumbnailDecryptionHeader =
|
||||
Sodium.bin2base64(encryptedThumbnail.header);
|
||||
final encryptedMetadata =
|
||||
Sodium.bin2base64(encryptedMetadataData.encryptedData);
|
||||
final metadataDecryptionHeader =
|
||||
Sodium.bin2base64(encryptedMetadataData.header);
|
||||
|
||||
final data = {
|
||||
"collectionID": file.collectionID,
|
||||
"encryptedKey": encryptedKey,
|
||||
"keyDecryptionNonce": keyDecryptionNonce,
|
||||
"file": {
|
||||
"objectKey": fileObjectKey,
|
||||
"decryptionParams": fileDecryptionParams.toMap(),
|
||||
"decryptionHeader": fileDecryptionHeader,
|
||||
},
|
||||
"thumbnail": {
|
||||
"objectKey": thumbnailObjectKey,
|
||||
"decryptionParams": thumbnailDecryptionParams.toMap(),
|
||||
"decryptionHeader": thumbnailDecryptionHeader,
|
||||
},
|
||||
"metadata": {
|
||||
"encryptedData": encryptedMetadata.encryptedData.base64,
|
||||
"decryptionParams": metadataDecryptionParams.toMap(),
|
||||
"encryptedData": encryptedMetadata,
|
||||
"decryptionHeader": metadataDecryptionHeader,
|
||||
}
|
||||
};
|
||||
return _dio
|
||||
.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/encrypted-files",
|
||||
Configuration.instance.getHttpEndpoint() + "/files",
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
data: data,
|
||||
|
@ -131,9 +126,11 @@ class FileUploader {
|
|||
file.uploadedFileID = data["id"];
|
||||
file.updationTime = data["updationTime"];
|
||||
file.ownerID = data["ownerID"];
|
||||
file.fileDecryptionParams = fileDecryptionParams;
|
||||
file.thumbnailDecryptionParams = thumbnailDecryptionParams;
|
||||
file.metadataDecryptionParams = metadataDecryptionParams;
|
||||
file.encryptedKey = encryptedKey;
|
||||
file.keyDecryptionNonce = keyDecryptionNonce;
|
||||
file.fileDecryptionHeader = fileDecryptionHeader;
|
||||
file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
|
||||
file.metadataDecryptionHeader = metadataDecryptionHeader;
|
||||
return file;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,10 +15,9 @@ import 'package:photos/core/cache/video_cache_manager.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/models/encrypted_file_attributes.dart';
|
||||
import 'package:photos/models/encryption_attribute.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
|
||||
import 'crypto_util.dart';
|
||||
|
||||
|
@ -176,17 +175,8 @@ Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
|
|||
return null;
|
||||
}
|
||||
logger.info("File downloaded: " + file.uploadedFileID.toString());
|
||||
var attributes = ChaChaAttributes(
|
||||
EncryptionAttribute(
|
||||
bytes: await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(file.fileDecryptionParams.encryptedKey),
|
||||
Configuration.instance.getKey(),
|
||||
Sodium.base642bin(file.fileDecryptionParams.keyDecryptionNonce),
|
||||
)),
|
||||
EncryptionAttribute(base64: file.fileDecryptionParams.header),
|
||||
);
|
||||
await CryptoUtil.decryptFile(
|
||||
encryptedFilePath, decryptedFilePath, attributes);
|
||||
await CryptoUtil.decryptFile(encryptedFilePath, decryptedFilePath,
|
||||
Sodium.base642bin(file.fileDecryptionHeader), decryptFileKey(file));
|
||||
logger.info("File decrypted: " + file.uploadedFileID.toString());
|
||||
io.File(encryptedFilePath).deleteSync();
|
||||
final fileExtension = extension(file.title).substring(1).toLowerCase();
|
||||
|
@ -200,6 +190,8 @@ Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
|
|||
decryptedFile.deleteSync();
|
||||
downloadsInProgress.remove(file.uploadedFileID);
|
||||
return cachedFile;
|
||||
}).catchError((e) {
|
||||
downloadsInProgress.remove(file.uploadedFileID);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -209,14 +201,11 @@ Future<io.File> _downloadAndDecryptThumbnail(File file) async {
|
|||
"_thumbnail.decrypted";
|
||||
return Dio().download(file.getThumbnailUrl(), temporaryPath).then((_) async {
|
||||
final encryptedFile = io.File(temporaryPath);
|
||||
final thumbnailDecryptionKey = await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(file.thumbnailDecryptionParams.encryptedKey),
|
||||
Configuration.instance.getKey(),
|
||||
Sodium.base642bin(file.thumbnailDecryptionParams.keyDecryptionNonce));
|
||||
final data = await CryptoUtil.decrypt(
|
||||
final thumbnailDecryptionKey = decryptFileKey(file);
|
||||
final data = CryptoUtil.decryptChaCha(
|
||||
encryptedFile.readAsBytesSync(),
|
||||
thumbnailDecryptionKey,
|
||||
Sodium.base642bin(file.thumbnailDecryptionParams.nonce),
|
||||
Sodium.base642bin(file.thumbnailDecryptionHeader),
|
||||
);
|
||||
encryptedFile.deleteSync();
|
||||
return ThumbnailCacheManager().putFile(
|
||||
|
@ -227,3 +216,11 @@ Future<io.File> _downloadAndDecryptThumbnail(File file) async {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
Uint8List decryptFileKey(File file) {
|
||||
final encryptedKey = Sodium.base642bin(file.encryptedKey);
|
||||
final nonce = Sodium.base642bin(file.keyDecryptionNonce);
|
||||
final collectionKey =
|
||||
CollectionsService.instance.getCollectionKey(file.collectionID);
|
||||
return CryptoUtil.decryptSync(encryptedKey, collectionKey, nonce);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ Future<void> _shareVideo(ProgressDialog dialog, File file) async {
|
|||
return ShareExtend.share(path, "image");
|
||||
}
|
||||
|
||||
Future<void> shareText(String text) async {
|
||||
return ShareExtend.share(text, "text");
|
||||
}
|
||||
|
||||
Future<void> _shareImage(ProgressDialog dialog, File file) async {
|
||||
await dialog.show();
|
||||
final bytes = await getBytes(file);
|
||||
|
|
58
pubspec.lock
58
pubspec.lock
|
@ -28,14 +28,14 @@ packages:
|
|||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.5.0-nullsafety.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0-nullsafety.1"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -49,14 +49,14 @@ packages:
|
|||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.1.0-nullsafety.3"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
version: "1.2.0-nullsafety.1"
|
||||
chewie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -70,14 +70,14 @@ packages:
|
|||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.1.0-nullsafety.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.13"
|
||||
version: "1.15.0-nullsafety.3"
|
||||
computer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -196,7 +196,7 @@ packages:
|
|||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0-nullsafety.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -345,13 +345,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
like_button:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -359,13 +352,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
local_image_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_image_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -379,14 +365,14 @@ packages:
|
|||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.8"
|
||||
version: "0.12.10-nullsafety.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.8"
|
||||
version: "1.3.0-nullsafety.3"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -414,7 +400,7 @@ packages:
|
|||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.8.0-nullsafety.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -444,12 +430,12 @@ packages:
|
|||
source: hosted
|
||||
version: "1.0.2"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.2"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -526,7 +512,7 @@ packages:
|
|||
name: pull_to_refresh
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.1"
|
||||
version: "1.6.2"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -608,7 +594,7 @@ packages:
|
|||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
version: "1.8.0-nullsafety.2"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -629,21 +615,21 @@ packages:
|
|||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.5"
|
||||
version: "1.10.0-nullsafety.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0-nullsafety.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.1.0-nullsafety.1"
|
||||
super_logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -664,21 +650,21 @@ packages:
|
|||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0-nullsafety.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.17"
|
||||
version: "0.2.19-nullsafety.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0-nullsafety.3"
|
||||
uni_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -741,7 +727,7 @@ packages:
|
|||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
version: "2.1.0-nullsafety.3"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -799,5 +785,5 @@ packages:
|
|||
source: hosted
|
||||
version: "2.2.1"
|
||||
sdks:
|
||||
dart: ">=2.9.0-14.0.dev <3.0.0"
|
||||
dart: ">=2.10.0-110 <2.11.0"
|
||||
flutter: ">=1.19.0-2.0.pre <2.0.0"
|
||||
|
|
|
@ -29,7 +29,6 @@ dependencies:
|
|||
path_provider: ^1.6.5
|
||||
shared_preferences: ^0.5.6
|
||||
dio: ^3.0.9
|
||||
local_image_provider: ^1.0.0
|
||||
image: ^2.1.4
|
||||
esys_flutter_share: ^1.0.2
|
||||
share_extend: ^1.1.9
|
||||
|
@ -47,7 +46,7 @@ dependencies:
|
|||
logging: ^0.11.4
|
||||
flutter_image_compress: ^0.6.5+1
|
||||
flutter_typeahead: ^1.8.1
|
||||
pull_to_refresh: ^1.5.7
|
||||
pull_to_refresh: ^1.6.2
|
||||
fluttertoast: ^4.0.1
|
||||
extended_image: ^0.9.0
|
||||
video_player: ^0.10.11+1
|
||||
|
@ -61,6 +60,7 @@ dependencies:
|
|||
uni_links: ^0.4.0
|
||||
crisp: ^0.1.3
|
||||
flutter_sodium: ^0.1.8
|
||||
pedantic: ^1.9.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Add table
Reference in a new issue