commit
79a195abb1
46 changed files with 1311 additions and 1053 deletions
|
@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
In most cases you can leave this as-is, but you if you want to provide
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application android:name="io.flutter.app.FlutterApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true">
|
||||
<application android:name="io.flutter.app.FlutterApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true">
|
||||
|
||||
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
|
|
1
android/settings_aar.gradle
Normal file
1
android/settings_aar.gradle
Normal file
|
@ -0,0 +1 @@
|
|||
include ':app'
|
|
@ -20,6 +20,7 @@ class Configuration {
|
|||
static const endpointKey = "endpoint";
|
||||
static const userIDKey = "user_id";
|
||||
static const emailKey = "email";
|
||||
static const nameKey = "name";
|
||||
static const tokenKey = "token";
|
||||
static const hasOptedForE2EKey = "has_opted_for_e2e_encryption";
|
||||
static const foldersToBackUpKey = "folders_to_back_up";
|
||||
|
@ -105,7 +106,7 @@ class Configuration {
|
|||
if (kDebugMode) {
|
||||
return "http://192.168.0.100";
|
||||
}
|
||||
return "https://api.staging.ente.io";
|
||||
return "https://api.ente.io";
|
||||
}
|
||||
|
||||
Future<void> setEndpoint(String endpoint) async {
|
||||
|
@ -128,6 +129,14 @@ class Configuration {
|
|||
await _preferences.setString(emailKey, email);
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return _preferences.getString(nameKey);
|
||||
}
|
||||
|
||||
Future<void> setName(String name) async {
|
||||
await _preferences.setString(nameKey, name);
|
||||
}
|
||||
|
||||
int getUserID() {
|
||||
return _preferences.getInt(userIDKey);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ 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 {
|
||||
|
@ -11,17 +10,18 @@ class CollectionsDB {
|
|||
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 columnOwnerEmail = 'owner_email';
|
||||
static final columnOwnerName = 'owner_name';
|
||||
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';
|
||||
static final columnUpdationTime = 'updation_time';
|
||||
|
||||
CollectionsDB._privateConstructor();
|
||||
static final CollectionsDB instance = CollectionsDB._privateConstructor();
|
||||
|
@ -48,24 +48,15 @@ class CollectionsDB {
|
|||
CREATE TABLE $collectionsTable (
|
||||
$columnID INTEGER PRIMARY KEY NOT NULL,
|
||||
$columnOwnerID INTEGER NOT NULL,
|
||||
$columnOwnerEmail TEXT,
|
||||
$columnOwnerName TEXT,
|
||||
$columnEncryptedKey TEXT NOT NULL,
|
||||
$columnKeyDecryptionNonce TEXT NOT NULL,
|
||||
$columnKeyDecryptionNonce TEXT,
|
||||
$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
|
||||
$columnUpdationTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
@ -80,18 +71,6 @@ class CollectionsDB {
|
|||
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);
|
||||
|
@ -102,62 +81,53 @@ class CollectionsDB {
|
|||
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 {
|
||||
Future<int> getLastCollectionUpdationTime() async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
collectionsTable,
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
orderBy: '$columnUpdationTime DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return int.parse(rows[0][columnCreationTime]);
|
||||
return int.parse(rows[0][columnUpdationTime]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getLastSharedCollectionCreationTime() async {
|
||||
Future<int> deleteCollection(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
sharedCollectionsTable,
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: 1,
|
||||
return db.delete(
|
||||
collectionsTable,
|
||||
where: '$columnID = ?',
|
||||
whereArgs: [collectionID],
|
||||
);
|
||||
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[columnOwnerID] = collection.owner.id;
|
||||
row[columnOwnerEmail] = collection.owner.email;
|
||||
row[columnOwnerName] = collection.owner.name;
|
||||
row[columnEncryptedKey] = collection.encryptedKey;
|
||||
row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce;
|
||||
row[columnName] = collection.name;
|
||||
row[columnType] = Collection.typeToString(collection.type);
|
||||
row[columnEncryptedPath] = collection.attributes.encryptedPath;
|
||||
row[columnPathDecryptionNonce] = collection.attributes.pathDecryptionNonce;
|
||||
row[columnCreationTime] = collection.creationTime;
|
||||
row[columnUpdationTime] = collection.updationTime;
|
||||
return row;
|
||||
}
|
||||
|
||||
Collection _convertToCollection(Map<String, dynamic> row) {
|
||||
return Collection(
|
||||
row[columnID],
|
||||
row[columnOwnerID],
|
||||
CollectionOwner(
|
||||
id: row[columnOwnerID],
|
||||
email: row[columnOwnerEmail],
|
||||
name: row[columnOwnerName],
|
||||
),
|
||||
row[columnEncryptedKey],
|
||||
row[columnKeyDecryptionNonce],
|
||||
row[columnName],
|
||||
|
@ -165,29 +135,7 @@ class CollectionsDB {
|
|||
CollectionAttributes(
|
||||
encryptedPath: row[columnEncryptedPath],
|
||||
pathDecryptionNonce: 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]),
|
||||
int.parse(row[columnUpdationTime]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ class FilesDB {
|
|||
static final columnLatitude = 'latitude';
|
||||
static final columnLongitude = 'longitude';
|
||||
static final columnFileType = 'file_type';
|
||||
static final columnRemoteFolderID = 'remote_folder_id';
|
||||
static final columnIsEncrypted = 'is_encrypted';
|
||||
static final columnIsDeleted = 'is_deleted';
|
||||
static final columnCreationTime = 'creation_time';
|
||||
|
@ -73,7 +72,6 @@ class FilesDB {
|
|||
$columnLatitude REAL,
|
||||
$columnLongitude REAL,
|
||||
$columnFileType INTEGER,
|
||||
$columnRemoteFolderID INTEGER,
|
||||
$columnIsEncrypted INTEGER DEFAULT 1,
|
||||
$columnModificationTime TEXT NOT NULL,
|
||||
$columnEncryptedKey TEXT,
|
||||
|
@ -85,7 +83,12 @@ class FilesDB {
|
|||
$columnCreationTime TEXT NOT NULL,
|
||||
$columnUpdationTime TEXT,
|
||||
UNIQUE($columnUploadedFileID, $columnCollectionID)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX collection_id_index ON $table($columnCollectionID);
|
||||
CREATE INDEX device_folder_index ON $table($columnDeviceFolder);
|
||||
CREATE INDEX creation_time_index ON $table($columnCreationTime);
|
||||
CREATE INDEX updation_time_index ON $table($columnUpdationTime);
|
||||
''');
|
||||
}
|
||||
|
||||
|
@ -120,6 +123,17 @@ class FilesDB {
|
|||
return _convertToFiles(results)[0];
|
||||
}
|
||||
|
||||
Future<List<File>> getDeduplicatedFiles() async {
|
||||
_logger.info("Getting files for collection");
|
||||
final db = await instance.database;
|
||||
final results = await db.query(table,
|
||||
where: '$columnIsDeleted = 0',
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
groupBy:
|
||||
'IFNULL($columnUploadedFileID, $columnGeneratedID), IFNULL($columnLocalID, $columnGeneratedID)');
|
||||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getFiles() async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
|
@ -141,20 +155,6 @@ class FilesDB {
|
|||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getAllInFolder(
|
||||
int folderID, int beforeCreationTime, int limit) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
table,
|
||||
where:
|
||||
'$columnRemoteFolderID = ? AND $columnIsDeleted = 0 AND $columnCreationTime < ?',
|
||||
whereArgs: [folderID, beforeCreationTime],
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: limit,
|
||||
);
|
||||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getAllInCollectionBeforeCreationTime(
|
||||
int collectionID, int beforeCreationTime, int limit) async {
|
||||
final db = await instance.database;
|
||||
|
@ -169,6 +169,21 @@ class FilesDB {
|
|||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getAllInPathBeforeCreationTime(
|
||||
String path, int beforeCreationTime, int limit) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
table,
|
||||
where:
|
||||
'$columnLocalID IS NOT NULL AND $columnDeviceFolder = ? AND $columnIsDeleted = 0 AND $columnCreationTime < ?',
|
||||
whereArgs: [path, beforeCreationTime],
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
groupBy: '$columnLocalID',
|
||||
limit: limit,
|
||||
);
|
||||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getAllInCollection(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
|
@ -193,14 +208,20 @@ class FilesDB {
|
|||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<List<File>> getAllDeleted() async {
|
||||
Future<List<int>> getDeletedFileIDs() async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
final rows = await db.query(
|
||||
table,
|
||||
columns: [columnUploadedFileID],
|
||||
distinct: true,
|
||||
where: '$columnIsDeleted = 1',
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
);
|
||||
return _convertToFiles(results);
|
||||
final result = List<int>();
|
||||
for (final row in rows) {
|
||||
result.add(row[columnUploadedFileID]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<File>> getFilesToBeUploadedWithinFolders(
|
||||
|
@ -220,17 +241,92 @@ class FilesDB {
|
|||
return _convertToFiles(results);
|
||||
}
|
||||
|
||||
Future<File> getMatchingFile(String localID, String title,
|
||||
String deviceFolder, int creationTime, int modificationTime,
|
||||
Future<Map<int, File>> getLastCreatedFilesInCollections(
|
||||
List<int> collectionIDs) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.rawQuery('''
|
||||
SELECT
|
||||
$columnGeneratedID,
|
||||
$columnLocalID,
|
||||
$columnUploadedFileID,
|
||||
$columnOwnerID,
|
||||
$columnCollectionID,
|
||||
$columnTitle,
|
||||
$columnDeviceFolder,
|
||||
$columnLatitude,
|
||||
$columnLongitude,
|
||||
$columnFileType,
|
||||
$columnIsEncrypted,
|
||||
$columnModificationTime,
|
||||
$columnEncryptedKey,
|
||||
$columnKeyDecryptionNonce,
|
||||
$columnFileDecryptionHeader,
|
||||
$columnThumbnailDecryptionHeader,
|
||||
$columnMetadataDecryptionHeader,
|
||||
$columnIsDeleted,
|
||||
$columnUpdationTime,
|
||||
MAX($columnCreationTime) as $columnCreationTime
|
||||
FROM $table
|
||||
WHERE $columnCollectionID IN (${collectionIDs.join(', ')}) AND $columnIsDeleted = 0
|
||||
GROUP BY $columnCollectionID
|
||||
ORDER BY $columnCreationTime DESC;
|
||||
''');
|
||||
final result = Map<int, File>();
|
||||
final files = _convertToFiles(rows);
|
||||
for (final file in files) {
|
||||
result[file.collectionID] = file;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<int, File>> getLastUpdatedFilesInCollections(
|
||||
List<int> collectionIDs) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.rawQuery('''
|
||||
SELECT
|
||||
$columnGeneratedID,
|
||||
$columnLocalID,
|
||||
$columnUploadedFileID,
|
||||
$columnOwnerID,
|
||||
$columnCollectionID,
|
||||
$columnTitle,
|
||||
$columnDeviceFolder,
|
||||
$columnLatitude,
|
||||
$columnLongitude,
|
||||
$columnFileType,
|
||||
$columnIsEncrypted,
|
||||
$columnModificationTime,
|
||||
$columnEncryptedKey,
|
||||
$columnKeyDecryptionNonce,
|
||||
$columnFileDecryptionHeader,
|
||||
$columnThumbnailDecryptionHeader,
|
||||
$columnMetadataDecryptionHeader,
|
||||
$columnIsDeleted,
|
||||
$columnCreationTime,
|
||||
MAX($columnUpdationTime) AS $columnUpdationTime
|
||||
FROM $table
|
||||
WHERE $columnCollectionID IN (${collectionIDs.join(', ')}) AND $columnIsDeleted = 0
|
||||
GROUP BY $columnCollectionID
|
||||
ORDER BY $columnUpdationTime DESC;
|
||||
''');
|
||||
final result = Map<int, File>();
|
||||
final files = _convertToFiles(rows);
|
||||
for (final file in files) {
|
||||
result[file.collectionID] = file;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<File>> getMatchingFiles(
|
||||
String title, String deviceFolder, int creationTime, int modificationTime,
|
||||
{String alternateTitle}) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
where: '''$columnLocalID=? AND ($columnTitle=? OR $columnTitle=?) AND
|
||||
where: '''($columnTitle=? OR $columnTitle=?) AND
|
||||
$columnDeviceFolder=? AND $columnCreationTime=? AND
|
||||
$columnModificationTime=?''',
|
||||
whereArgs: [
|
||||
localID,
|
||||
title,
|
||||
alternateTitle,
|
||||
deviceFolder,
|
||||
|
@ -239,9 +335,9 @@ class FilesDB {
|
|||
],
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return _getFileFromRow(rows[0]);
|
||||
return _convertToFiles(rows);
|
||||
} else {
|
||||
throw ("No matching file found");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,25 +365,24 @@ class FilesDB {
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: Remove deleted files on remote
|
||||
Future<int> markForDeletion(File file) async {
|
||||
Future<int> markForDeletion(int uploadedFileID) async {
|
||||
final db = await instance.database;
|
||||
final values = new Map<String, dynamic>();
|
||||
values[columnIsDeleted] = 1;
|
||||
return db.update(
|
||||
table,
|
||||
values,
|
||||
where: '$columnGeneratedID =?',
|
||||
whereArgs: [file.generatedID],
|
||||
where: '$columnUploadedFileID =?',
|
||||
whereArgs: [uploadedFileID],
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> delete(File file) async {
|
||||
Future<int> delete(int uploadedFileID) async {
|
||||
final db = await instance.database;
|
||||
return db.delete(
|
||||
table,
|
||||
where: '$columnGeneratedID =?',
|
||||
whereArgs: [file.generatedID],
|
||||
where: '$columnUploadedFileID =?',
|
||||
whereArgs: [uploadedFileID],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -300,12 +395,22 @@ class FilesDB {
|
|||
);
|
||||
}
|
||||
|
||||
Future<int> deleteFilesInRemoteFolder(int folderID) async {
|
||||
Future<int> deleteCollection(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
return db.delete(
|
||||
table,
|
||||
where: '$columnRemoteFolderID =?',
|
||||
whereArgs: [folderID],
|
||||
where: '$columnCollectionID = ?',
|
||||
whereArgs: [collectionID],
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeFromCollection(int collectionID, List<int> fileIDs) async {
|
||||
final db = await instance.database;
|
||||
return db.delete(
|
||||
table,
|
||||
where:
|
||||
'$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})',
|
||||
whereArgs: [collectionID],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -315,7 +420,6 @@ class FilesDB {
|
|||
table,
|
||||
columns: [columnDeviceFolder],
|
||||
distinct: true,
|
||||
where: '$columnRemoteFolderID IS NULL',
|
||||
);
|
||||
List<String> result = List<String>();
|
||||
for (final row in rows) {
|
||||
|
@ -324,43 +428,11 @@ class FilesDB {
|
|||
return result;
|
||||
}
|
||||
|
||||
Future<File> getLatestFileInPath(String path) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
where: '$columnDeviceFolder =?',
|
||||
whereArgs: [path],
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return _getFileFromRow(rows[0]);
|
||||
} else {
|
||||
throw ("No file found in path");
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> getLatestFileInRemoteFolder(int folderID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
where: '$columnRemoteFolderID =?',
|
||||
whereArgs: [folderID],
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return _getFileFromRow(rows[0]);
|
||||
} else {
|
||||
throw ("No file found in remote folder " + folderID.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> getLatestFileInCollection(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
where: '$columnCollectionID =?',
|
||||
where: '$columnCollectionID = ? AND $columnIsDeleted = 0',
|
||||
whereArgs: [collectionID],
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
limit: 1,
|
||||
|
@ -368,39 +440,36 @@ class FilesDB {
|
|||
if (rows.isNotEmpty) {
|
||||
return _getFileFromRow(rows[0]);
|
||||
} else {
|
||||
throw ("No file found in collection " + collectionID.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> getLastSyncedFileInRemoteFolder(int folderID) async {
|
||||
Future<File> getLastModifiedFileInCollection(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
where: '$columnRemoteFolderID =?',
|
||||
whereArgs: [folderID],
|
||||
where: '$columnCollectionID = ? AND $columnIsDeleted = 0',
|
||||
whereArgs: [collectionID],
|
||||
orderBy: '$columnUpdationTime DESC',
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return _getFileFromRow(rows[0]);
|
||||
} else {
|
||||
throw ("No file found in remote folder " + folderID.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> getLatestFileAmongGeneratedIDs(List<String> generatedIDs) async {
|
||||
Future<bool> doesFileExistInCollection(
|
||||
int uploadedFileID, int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final rows = await db.query(
|
||||
table,
|
||||
where: '$columnGeneratedID IN (${generatedIDs.join(",")})',
|
||||
orderBy: '$columnCreationTime DESC',
|
||||
where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
|
||||
whereArgs: [uploadedFileID, collectionID],
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isNotEmpty) {
|
||||
return _getFileFromRow(rows[0]);
|
||||
} else {
|
||||
throw ("No file found with ids " + generatedIDs.join(", ").toString());
|
||||
}
|
||||
return rows.isNotEmpty;
|
||||
}
|
||||
|
||||
List<File> _convertToFiles(List<Map<String, dynamic>> results) {
|
||||
|
@ -434,7 +503,6 @@ class FilesDB {
|
|||
row[columnFileType] = -1;
|
||||
}
|
||||
row[columnIsEncrypted] = file.isEncrypted ? 1 : 0;
|
||||
row[columnRemoteFolderID] = file.remoteFolderID;
|
||||
row[columnCreationTime] = file.creationTime;
|
||||
row[columnModificationTime] = file.modificationTime;
|
||||
row[columnUpdationTime] = file.updationTime;
|
||||
|
@ -459,7 +527,6 @@ class FilesDB {
|
|||
file.location = Location(row[columnLatitude], row[columnLongitude]);
|
||||
}
|
||||
file.fileType = getFileType(row[columnFileType]);
|
||||
file.remoteFolderID = row[columnRemoteFolderID];
|
||||
file.isEncrypted = row[columnIsEncrypted] == 1;
|
||||
file.creationTime = int.parse(row[columnCreationTime]);
|
||||
file.modificationTime = int.parse(row[columnModificationTime]);
|
||||
|
|
7
lib/events/collection_updated_event.dart
Normal file
7
lib/events/collection_updated_event.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'package:photos/events/event.dart';
|
||||
|
||||
class CollectionUpdatedEvent extends Event {
|
||||
final int collectionID;
|
||||
|
||||
CollectionUpdatedEvent({this.collectionID});
|
||||
}
|
7
lib/events/tab_changed_event.dart
Normal file
7
lib/events/tab_changed_event.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'package:sentry/sentry.dart';
|
||||
|
||||
class TabChangedEvent extends Event {
|
||||
final selectedIndex;
|
||||
|
||||
TabChangedEvent(this.selectedIndex);
|
||||
}
|
|
@ -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/collections_service.dart';
|
||||
import 'package:photos/services/favorites_service.dart';
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/ui/home_widget.dart';
|
||||
|
@ -34,7 +33,6 @@ void _main() async {
|
|||
await CollectionsService.instance.init();
|
||||
await SyncService.instance.init();
|
||||
await MemoriesService.instance.init();
|
||||
await FavoritesService.instance.init();
|
||||
_sync();
|
||||
|
||||
final SentryClient sentry = new SentryClient(dsn: SENTRY_DSN);
|
||||
|
|
|
@ -2,24 +2,26 @@ import 'dart:convert';
|
|||
|
||||
class Collection {
|
||||
final int id;
|
||||
final int ownerID;
|
||||
final CollectionOwner owner;
|
||||
final String encryptedKey;
|
||||
final String keyDecryptionNonce;
|
||||
final String name;
|
||||
final CollectionType type;
|
||||
final CollectionAttributes attributes;
|
||||
final int creationTime;
|
||||
final int updationTime;
|
||||
final bool isDeleted;
|
||||
|
||||
Collection(
|
||||
this.id,
|
||||
this.ownerID,
|
||||
this.owner,
|
||||
this.encryptedKey,
|
||||
this.keyDecryptionNonce,
|
||||
this.name,
|
||||
this.type,
|
||||
this.attributes,
|
||||
this.creationTime,
|
||||
);
|
||||
this.updationTime, {
|
||||
this.isDeleted = false,
|
||||
});
|
||||
|
||||
static CollectionType typeFromString(String type) {
|
||||
switch (type) {
|
||||
|
@ -42,38 +44,16 @@ class Collection {
|
|||
}
|
||||
}
|
||||
|
||||
Collection copyWith({
|
||||
int id,
|
||||
int ownerID,
|
||||
String encryptedKey,
|
||||
String keyDecryptionNonce,
|
||||
String name,
|
||||
CollectionType type,
|
||||
CollectionAttributes attributes,
|
||||
int creationTime,
|
||||
}) {
|
||||
return Collection(
|
||||
id ?? this.id,
|
||||
ownerID ?? this.ownerID,
|
||||
encryptedKey ?? this.encryptedKey,
|
||||
keyDecryptionNonce ?? this.keyDecryptionNonce,
|
||||
name ?? this.name,
|
||||
type ?? this.type,
|
||||
attributes ?? this.attributes,
|
||||
creationTime ?? this.creationTime,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'ownerID': ownerID,
|
||||
'owner': owner?.toMap(),
|
||||
'encryptedKey': encryptedKey,
|
||||
'keyDecryptionNonce': keyDecryptionNonce,
|
||||
'name': name,
|
||||
'type': typeToString(type),
|
||||
'attributes': attributes?.toMap(),
|
||||
'creationTime': creationTime,
|
||||
'updationTime': updationTime,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -82,13 +62,14 @@ class Collection {
|
|||
|
||||
return Collection(
|
||||
map['id'],
|
||||
map['ownerID'],
|
||||
CollectionOwner.fromMap(map['owner']),
|
||||
map['encryptedKey'],
|
||||
map['keyDecryptionNonce'],
|
||||
map['name'],
|
||||
typeFromString(map['type']),
|
||||
CollectionAttributes.fromMap(map['attributes']),
|
||||
map['creationTime'],
|
||||
map['updationTime'],
|
||||
isDeleted: map['isDeleted'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -99,7 +80,7 @@ class Collection {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Collection(id: $id, ownerID: $ownerID, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, type: $type, attributes: $attributes, creationTime: $creationTime)';
|
||||
return 'Collection(id: $id, owner: ${owner.toString()} encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, type: $type, attributes: $attributes, creationTime: $updationTime)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -108,25 +89,25 @@ class Collection {
|
|||
|
||||
return o is Collection &&
|
||||
o.id == id &&
|
||||
o.ownerID == ownerID &&
|
||||
o.owner == owner &&
|
||||
o.encryptedKey == encryptedKey &&
|
||||
o.keyDecryptionNonce == keyDecryptionNonce &&
|
||||
o.name == name &&
|
||||
o.type == type &&
|
||||
o.attributes == attributes &&
|
||||
o.creationTime == creationTime;
|
||||
o.updationTime == updationTime;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
ownerID.hashCode ^
|
||||
owner.hashCode ^
|
||||
encryptedKey.hashCode ^
|
||||
keyDecryptionNonce.hashCode ^
|
||||
name.hashCode ^
|
||||
type.hashCode ^
|
||||
attributes.hashCode ^
|
||||
creationTime.hashCode;
|
||||
updationTime.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,3 +177,66 @@ class CollectionAttributes {
|
|||
@override
|
||||
int get hashCode => encryptedPath.hashCode ^ pathDecryptionNonce.hashCode;
|
||||
}
|
||||
|
||||
class CollectionOwner {
|
||||
int id;
|
||||
String email;
|
||||
String name;
|
||||
|
||||
CollectionOwner({
|
||||
this.id,
|
||||
this.email,
|
||||
this.name,
|
||||
});
|
||||
|
||||
CollectionOwner copyWith({
|
||||
int id,
|
||||
String email,
|
||||
String name,
|
||||
}) {
|
||||
return CollectionOwner(
|
||||
id: id ?? this.id,
|
||||
email: email ?? this.email,
|
||||
name: name ?? this.name,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'email': email,
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
|
||||
factory CollectionOwner.fromMap(Map<String, dynamic> map) {
|
||||
if (map == null) return null;
|
||||
|
||||
return CollectionOwner(
|
||||
id: map['id'],
|
||||
email: map['email'],
|
||||
name: map['name'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory CollectionOwner.fromJson(String source) =>
|
||||
CollectionOwner.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() => 'CollectionOwner(id: $id, email: $email, name: $name)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object o) {
|
||||
if (identical(this, o)) return true;
|
||||
|
||||
return o is CollectionOwner &&
|
||||
o.id == id &&
|
||||
o.email == email &&
|
||||
o.name == name;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ email.hashCode ^ name.hashCode;
|
||||
}
|
||||
|
|
22
lib/models/collection_items.dart
Normal file
22
lib/models/collection_items.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/device_folder.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
|
||||
class CollectionItems {
|
||||
final List<DeviceFolder> folders;
|
||||
final List<CollectionWithThumbnail> collections;
|
||||
|
||||
CollectionItems(this.folders, this.collections);
|
||||
}
|
||||
|
||||
class CollectionWithThumbnail {
|
||||
final Collection collection;
|
||||
final File thumbnail;
|
||||
final File lastUpdatedFile;
|
||||
|
||||
CollectionWithThumbnail(
|
||||
this.collection,
|
||||
this.thumbnail,
|
||||
this.lastUpdatedFile,
|
||||
);
|
||||
}
|
|
@ -4,14 +4,12 @@ import 'package:photos/models/file.dart';
|
|||
class DeviceFolder {
|
||||
final String name;
|
||||
final String path;
|
||||
final List<File> Function() loader;
|
||||
final File thumbnail;
|
||||
final GalleryItemsFilter filter;
|
||||
|
||||
DeviceFolder(
|
||||
this.name,
|
||||
this.path,
|
||||
this.loader,
|
||||
this.thumbnail, {
|
||||
this.filter,
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ class File {
|
|||
String localID;
|
||||
String title;
|
||||
String deviceFolder;
|
||||
int remoteFolderID;
|
||||
bool isEncrypted;
|
||||
int creationTime;
|
||||
int modificationTime;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import 'package:photos/events/local_photos_updated_event.dart';
|
|||
import 'package:photos/models/file.dart';
|
||||
|
||||
class FileRepository {
|
||||
final _logger = Logger("PhotoRepository");
|
||||
final _logger = Logger("FileRepository");
|
||||
final _files = List<File>();
|
||||
|
||||
FileRepository._privateConstructor();
|
||||
|
@ -15,12 +15,23 @@ class FileRepository {
|
|||
return _files;
|
||||
}
|
||||
|
||||
Future<List<File>> loadFiles() async {
|
||||
var files = await FilesDB.instance.getFiles();
|
||||
_files.clear();
|
||||
_files.addAll(files);
|
||||
bool _hasLoadedFiles = false;
|
||||
|
||||
return _files;
|
||||
bool get hasLoadedFiles {
|
||||
return _hasLoadedFiles;
|
||||
}
|
||||
|
||||
Future<List<File>> _cachedFuture;
|
||||
|
||||
Future<List<File>> loadFiles() async {
|
||||
if (_cachedFuture == null) {
|
||||
_cachedFuture = _loadFiles().then((value) {
|
||||
_hasLoadedFiles = true;
|
||||
_cachedFuture = null;
|
||||
return value;
|
||||
});
|
||||
}
|
||||
return _cachedFuture;
|
||||
}
|
||||
|
||||
Future<void> reloadFiles() async {
|
||||
|
@ -28,4 +39,24 @@ class FileRepository {
|
|||
await loadFiles();
|
||||
Bus.instance.fire(LocalPhotosUpdatedEvent());
|
||||
}
|
||||
|
||||
Future<List<File>> _loadFiles() async {
|
||||
final files = await FilesDB.instance.getFiles();
|
||||
final deduplicatedFiles = List<File>();
|
||||
for (int index = 0; index < files.length; index++) {
|
||||
if (index != 0) {
|
||||
bool isSameUploadedFile = files[index].uploadedFileID != null &&
|
||||
(files[index].uploadedFileID == files[index - 1].uploadedFileID);
|
||||
bool isSameLocalFile = files[index].localID != null &&
|
||||
(files[index].localID == files[index - 1].localID);
|
||||
if (isSameUploadedFile || isSameLocalFile) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
deduplicatedFiles.add(files[index]);
|
||||
}
|
||||
_files.clear();
|
||||
_files.addAll(deduplicatedFiles);
|
||||
return _files;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,14 @@ import 'package:flutter_sodium/flutter_sodium.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:photos/core/configuration.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/collection_updated_event.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/collection_file_item.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/shared_collection.dart';
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
|
||||
|
@ -19,14 +22,15 @@ class CollectionsService {
|
|||
final _logger = Logger("CollectionsService");
|
||||
|
||||
CollectionsDB _db;
|
||||
FilesDB _filesDB;
|
||||
Configuration _config;
|
||||
final _localCollections = Map<String, Collection>();
|
||||
final _collectionIDToOwnedCollections = Map<int, Collection>();
|
||||
final _collectionIDToSharedCollections = Map<int, SharedCollection>();
|
||||
final _collectionIDToCollections = Map<int, Collection>();
|
||||
final _cachedKeys = Map<int, Uint8List>();
|
||||
|
||||
CollectionsService._privateConstructor() {
|
||||
_db = CollectionsDB.instance;
|
||||
_filesDB = FilesDB.instance;
|
||||
_config = Configuration.instance;
|
||||
}
|
||||
|
||||
|
@ -36,33 +40,33 @@ class CollectionsService {
|
|||
Future<void> init() async {
|
||||
final collections = await _db.getAllCollections();
|
||||
for (final collection in collections) {
|
||||
_cacheOwnedCollectionAttributes(collection);
|
||||
}
|
||||
final sharedCollections = await _db.getAllSharedCollections();
|
||||
for (final collection in sharedCollections) {
|
||||
_collectionIDToSharedCollections[collection.id] = collection;
|
||||
_cacheCollectionAttributes(collection);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
_cacheOwnedCollectionAttributes(collection);
|
||||
_logger.info("Syncing");
|
||||
final lastCollectionUpdationTime =
|
||||
await _db.getLastCollectionUpdationTime();
|
||||
final fetchedCollections =
|
||||
await _fetchCollections(lastCollectionUpdationTime ?? 0);
|
||||
final updatedCollections = List<Collection>();
|
||||
for (final collection in fetchedCollections) {
|
||||
if (collection.isDeleted) {
|
||||
await _filesDB.deleteCollection(collection.id);
|
||||
await _db.deleteCollection(collection.id);
|
||||
} else {
|
||||
updatedCollections.add(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;
|
||||
await _db.insert(updatedCollections);
|
||||
final collections = await _db.getAllCollections();
|
||||
for (final collection in collections) {
|
||||
_cacheCollectionAttributes(collection);
|
||||
}
|
||||
if (fetchedCollections.isNotEmpty) {
|
||||
_logger.info("Collections updated");
|
||||
Bus.instance.fire(CollectionUpdatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,8 +74,8 @@ class CollectionsService {
|
|||
return _localCollections[path];
|
||||
}
|
||||
|
||||
List<Collection> getOwnedCollections() {
|
||||
return _collectionIDToOwnedCollections.values.toList();
|
||||
List<Collection> getCollections() {
|
||||
return _collectionIDToCollections.values.toList();
|
||||
}
|
||||
|
||||
Future<List<String>> getSharees(int collectionID) {
|
||||
|
@ -109,16 +113,27 @@ class CollectionsService {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> unshare(int collectionID, String email) {
|
||||
return Dio().post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/unshare",
|
||||
data: {
|
||||
"collectionID": collectionID,
|
||||
"email": email,
|
||||
},
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List getCollectionKey(int collectionID) {
|
||||
if (!_cachedKeys.containsKey(collectionID)) {
|
||||
final collection = _collectionIDToCollections[collectionID];
|
||||
var key;
|
||||
if (_collectionIDToOwnedCollections.containsKey(collectionID)) {
|
||||
final collection = _collectionIDToOwnedCollections[collectionID];
|
||||
if (collection.owner.id == _config.getUserID()) {
|
||||
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,
|
||||
|
@ -130,10 +145,10 @@ class CollectionsService {
|
|||
return _cachedKeys[collectionID];
|
||||
}
|
||||
|
||||
Future<List<Collection>> _getOwnedCollections(int sinceTime) {
|
||||
Future<List<Collection>> _fetchCollections(int sinceTime) {
|
||||
return Dio()
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/owned",
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/",
|
||||
queryParameters: {
|
||||
"sinceTime": sinceTime,
|
||||
},
|
||||
|
@ -152,30 +167,24 @@ class CollectionsService {
|
|||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
Collection getCollectionByID(int collectionID) {
|
||||
return _collectionIDToCollections[collectionID];
|
||||
}
|
||||
|
||||
Collection getOwnedCollectionByID(int collectionID) {
|
||||
return _collectionIDToOwnedCollections[collectionID];
|
||||
Future<Collection> createAlbum(String albumName) async {
|
||||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
|
||||
final collection = await createAndCacheCollection(Collection(
|
||||
null,
|
||||
null,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
albumName,
|
||||
CollectionType.album,
|
||||
CollectionAttributes(),
|
||||
null,
|
||||
));
|
||||
return collection;
|
||||
}
|
||||
|
||||
Future<Collection> getOrCreateForPath(String path) async {
|
||||
|
@ -206,40 +215,50 @@ class CollectionsService {
|
|||
params["collectionID"] = collectionID;
|
||||
for (final file in files) {
|
||||
final key = decryptFileKey(file);
|
||||
file.collectionID = collectionID;
|
||||
final encryptedKeyData =
|
||||
CryptoUtil.encryptSync(key, getCollectionKey(collectionID));
|
||||
file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
|
||||
file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
|
||||
if (params["files"] == null) {
|
||||
params["files"] = [];
|
||||
}
|
||||
params["files"].add(CollectionFileItem(
|
||||
file.uploadedFileID,
|
||||
Sodium.bin2base64(encryptedKeyData.encryptedData),
|
||||
Sodium.bin2base64(encryptedKeyData.nonce),
|
||||
).toMap());
|
||||
file.uploadedFileID, file.encryptedKey, file.keyDecryptionNonce)
|
||||
.toMap());
|
||||
}
|
||||
return Dio().post(
|
||||
return Dio()
|
||||
.post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/add-files",
|
||||
data: params,
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
);
|
||||
)
|
||||
.then((value) async {
|
||||
await _filesDB.insertMultiple(files);
|
||||
Bus.instance.fire(CollectionUpdatedEvent(collectionID: collectionID));
|
||||
SyncService.instance.syncWithRemote();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> removeFromCollection(int collectionID, List<File> files) {
|
||||
Future<void> removeFromCollection(int collectionID, List<File> files) async {
|
||||
final params = Map<String, dynamic>();
|
||||
params["collectionID"] = collectionID;
|
||||
for (final file in files) {
|
||||
if (params["fileIDs"] == null) {
|
||||
params["fileIDs"] = [];
|
||||
params["fileIDs"] = List<int>();
|
||||
}
|
||||
params["fileIDs"].add(file.uploadedFileID);
|
||||
}
|
||||
return Dio().post(
|
||||
await Dio().post(
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/remove-files",
|
||||
data: params,
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
);
|
||||
await _filesDB.removeFromCollection(collectionID, params["fileIDs"]);
|
||||
Bus.instance.fire(CollectionUpdatedEvent(collectionID: collectionID));
|
||||
SyncService.instance.syncWithRemote();
|
||||
}
|
||||
|
||||
Future<Collection> createAndCacheCollection(Collection collection) async {
|
||||
|
@ -252,21 +271,23 @@ class CollectionsService {
|
|||
)
|
||||
.then((response) {
|
||||
final collection = Collection.fromMap(response.data["collection"]);
|
||||
_cacheOwnedCollectionAttributes(collection);
|
||||
_cacheCollectionAttributes(collection);
|
||||
return collection;
|
||||
});
|
||||
}
|
||||
|
||||
void _cacheOwnedCollectionAttributes(Collection collection) {
|
||||
void _cacheCollectionAttributes(Collection collection) {
|
||||
if (collection.attributes.encryptedPath != null) {
|
||||
var path = utf8.decode(CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(collection.attributes.encryptedPath),
|
||||
_config.getKey(),
|
||||
Sodium.base642bin(collection.attributes.pathDecryptionNonce)));
|
||||
_localCollections[path] = collection;
|
||||
_localCollections[decryptCollectionPath(collection)] = collection;
|
||||
}
|
||||
_collectionIDToOwnedCollections[collection.id] = collection;
|
||||
getCollectionKey(collection.id);
|
||||
_collectionIDToCollections[collection.id] = collection;
|
||||
}
|
||||
|
||||
String decryptCollectionPath(Collection collection) {
|
||||
return utf8.decode(CryptoUtil.decryptSync(
|
||||
Sodium.base642bin(collection.attributes.encryptedPath),
|
||||
_config.getKey(),
|
||||
Sodium.base642bin(collection.attributes.pathDecryptionNonce)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'package:photos/models/face.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/utils/file_name_util.dart';
|
||||
|
||||
class FaceSearchService {
|
||||
final _logger = Logger("FaceSearchManager");
|
||||
final _dio = Dio();
|
||||
|
||||
FaceSearchService._privateConstructor();
|
||||
static final FaceSearchService instance =
|
||||
FaceSearchService._privateConstructor();
|
||||
|
||||
Future<List<Face>> getFaces() {
|
||||
return _dio
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/photos/faces",
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) => (response.data["faces"] as List)
|
||||
.map((face) => new Face.fromJson(face))
|
||||
.toList())
|
||||
.catchError(_onError);
|
||||
}
|
||||
|
||||
Future<List<File>> getFaceSearchResults(
|
||||
Face face, int beforeCreationTime, int limit) async {
|
||||
_logger.info("Fetching since creation " + beforeCreationTime.toString());
|
||||
final result = await _dio
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() +
|
||||
"/search/face/" +
|
||||
face.id.toString(),
|
||||
queryParameters: {
|
||||
"limit": limit,
|
||||
"beforeCreationTime": beforeCreationTime,
|
||||
},
|
||||
options:
|
||||
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
.then((response) {
|
||||
return (response.data["result"] as List)
|
||||
.map((p) => File.fromJson(p))
|
||||
.toList();
|
||||
}).catchError(_onError);
|
||||
final files = List<File>();
|
||||
if (result == null) {
|
||||
return throw ("Oops. Could not fetch search results.");
|
||||
}
|
||||
for (File file in result) {
|
||||
try {
|
||||
files.add(await FilesDB.instance.getMatchingFile(
|
||||
file.localID,
|
||||
file.title,
|
||||
file.deviceFolder,
|
||||
file.creationTime,
|
||||
file.modificationTime,
|
||||
alternateTitle: getHEICFileNameForJPG(file)));
|
||||
} catch (e) {
|
||||
// Not available locally
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
files.sort((first, second) {
|
||||
return second.creationTime.compareTo(first.creationTime);
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
void _onError(error) {
|
||||
_logger.severe(error);
|
||||
}
|
||||
}
|
|
@ -1,23 +1,18 @@
|
|||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class FavoritesService {
|
||||
static final _favoritesCollectionIDKey = "favorites_collection_id";
|
||||
|
||||
final _cachedFavoriteFiles = Set<File>();
|
||||
Configuration _config;
|
||||
CollectionsService _collectionsService;
|
||||
FileUploader _fileUploader;
|
||||
FilesDB _filesDB;
|
||||
int _cachedFavoritesCollectionID;
|
||||
|
||||
FavoritesService._privateConstructor() {
|
||||
_config = Configuration.instance;
|
||||
|
@ -27,23 +22,13 @@ class FavoritesService {
|
|||
}
|
||||
static FavoritesService instance = FavoritesService._privateConstructor();
|
||||
|
||||
SharedPreferences _preferences;
|
||||
|
||||
Future<void> init() async {
|
||||
_preferences = await SharedPreferences.getInstance();
|
||||
if (_preferences.containsKey(_favoritesCollectionIDKey)) {
|
||||
final collectionID = _preferences.getInt(_favoritesCollectionIDKey);
|
||||
_cachedFavoriteFiles
|
||||
.addAll((await _filesDB.getAllInCollection(collectionID)).toSet());
|
||||
Future<bool> isFavorite(File file) async {
|
||||
final collection = await _getFavoritesCollection();
|
||||
if (collection == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Set<File> getFavoriteFiles() {
|
||||
return _cachedFavoriteFiles;
|
||||
}
|
||||
|
||||
bool isLiked(File file) {
|
||||
return _cachedFavoriteFiles.contains(file);
|
||||
return _filesDB.doesFileExistInCollection(
|
||||
file.uploadedFileID, collection.id);
|
||||
}
|
||||
|
||||
Future<void> addToFavorites(File file) async {
|
||||
|
@ -54,8 +39,6 @@ class FavoritesService {
|
|||
await _filesDB.update(uploadedFile);
|
||||
} else {
|
||||
await _collectionsService.addToCollection(collectionID, [file]);
|
||||
_cachedFavoriteFiles.add(file);
|
||||
Bus.instance.fire(LocalPhotosUpdatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,29 +49,26 @@ class FavoritesService {
|
|||
// Do nothing, ignore
|
||||
} else {
|
||||
await _collectionsService.removeFromCollection(collectionID, [file]);
|
||||
_cachedFavoriteFiles.remove(file);
|
||||
Bus.instance.fire(LocalPhotosUpdatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Collection> getFavoritesCollection() async {
|
||||
if (!_preferences.containsKey(_favoritesCollectionIDKey)) {
|
||||
final collections = _collectionsService.getOwnedCollections();
|
||||
Future<Collection> _getFavoritesCollection() async {
|
||||
if (_cachedFavoritesCollectionID == null) {
|
||||
final collections = _collectionsService.getCollections();
|
||||
for (final collection in collections) {
|
||||
if (collection.type == CollectionType.favorites) {
|
||||
await _preferences.setInt(_favoritesCollectionIDKey, collection.id);
|
||||
_cachedFavoritesCollectionID = collection.id;
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return _collectionsService
|
||||
.getOwnedCollectionByID(_preferences.getInt(_favoritesCollectionIDKey));
|
||||
return _collectionsService.getCollectionByID(_cachedFavoritesCollectionID);
|
||||
}
|
||||
|
||||
Future<int> _getOrCreateFavoriteCollectionID() async {
|
||||
if (_preferences.containsKey(_favoritesCollectionIDKey)) {
|
||||
return _preferences.getInt(_favoritesCollectionIDKey);
|
||||
if (_cachedFavoritesCollectionID != null) {
|
||||
return _cachedFavoritesCollectionID;
|
||||
}
|
||||
final key = CryptoUtil.generateKey();
|
||||
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
|
||||
|
@ -103,7 +83,7 @@ class FavoritesService {
|
|||
CollectionAttributes(),
|
||||
null,
|
||||
));
|
||||
await _preferences.setInt(_favoritesCollectionIDKey, collection.id);
|
||||
_cachedFavoritesCollectionID = collection.id;
|
||||
return collection.id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:math';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/collection_updated_event.dart';
|
||||
import 'package:photos/events/photo_upload_event.dart';
|
||||
import 'package:photos/events/user_authenticated_event.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
|
@ -24,13 +25,14 @@ class SyncService {
|
|||
final _dio = Dio();
|
||||
final _db = FilesDB.instance;
|
||||
final _uploader = FileUploader.instance;
|
||||
final _collectionsService = CollectionsService.instance;
|
||||
final _downloader = DiffFetcher();
|
||||
bool _isSyncInProgress = false;
|
||||
bool _syncStopRequested = false;
|
||||
Future<void> _existingSync;
|
||||
SharedPreferences _prefs;
|
||||
|
||||
static final _encryptedFilesSyncTimeKey = "encrypted_files_sync_time";
|
||||
static final _collectionSyncTimeKeyPrefix = "collection_sync_time_";
|
||||
static final _dbUpdationTimeKey = "db_updation_time";
|
||||
static final _diffLimit = 100;
|
||||
|
||||
|
@ -57,8 +59,8 @@ class SyncService {
|
|||
_logger.info("Syncing...");
|
||||
try {
|
||||
await _doSync();
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
} finally {
|
||||
_isSyncInProgress = false;
|
||||
}
|
||||
|
@ -106,10 +108,11 @@ class SyncService {
|
|||
}
|
||||
files.sort(
|
||||
(first, second) => first.creationTime.compareTo(second.creationTime));
|
||||
|
||||
await _insertFilesToDB(files, syncStartTime);
|
||||
await FileRepository.instance.reloadFiles();
|
||||
await _syncWithRemote();
|
||||
if (files.isNotEmpty) {
|
||||
await _insertFilesToDB(files, syncStartTime);
|
||||
await FileRepository.instance.reloadFiles();
|
||||
}
|
||||
await syncWithRemote();
|
||||
}
|
||||
|
||||
Future<List<AssetPathEntity>> _getGalleryList(
|
||||
|
@ -117,7 +120,7 @@ class SyncService {
|
|||
final filterOptionGroup = FilterOptionGroup();
|
||||
filterOptionGroup.setOption(AssetType.image, FilterOption(needTitle: true));
|
||||
filterOptionGroup.setOption(AssetType.video, FilterOption(needTitle: true));
|
||||
filterOptionGroup.dateTimeCond = DateTimeCond(
|
||||
filterOptionGroup.createTimeCond = DateTimeCond(
|
||||
min: DateTime.fromMicrosecondsSinceEpoch(fromTimestamp),
|
||||
max: DateTime.fromMicrosecondsSinceEpoch(toTimestamp),
|
||||
);
|
||||
|
@ -153,36 +156,49 @@ class SyncService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _syncWithRemote() async {
|
||||
Future<void> syncWithRemote() async {
|
||||
if (!Configuration.instance.hasConfiguredAccount()) {
|
||||
return Future.error("Account not configured yet");
|
||||
}
|
||||
await CollectionsService.instance.sync();
|
||||
await _persistEncryptedFilesDiff();
|
||||
await _collectionsService.sync();
|
||||
final collections = _collectionsService.getCollections();
|
||||
for (final collection in collections) {
|
||||
await _fetchEncryptedFilesDiff(collection.id);
|
||||
}
|
||||
await _uploadDiff();
|
||||
await _deletePhotosOnServer();
|
||||
await deleteFilesOnServer();
|
||||
}
|
||||
|
||||
Future<void> _persistEncryptedFilesDiff() async {
|
||||
Future<void> _fetchEncryptedFilesDiff(int collectionID) async {
|
||||
final diff = await _downloader.getEncryptedFilesDiff(
|
||||
_getEncryptedFilesSyncTime(), _diffLimit);
|
||||
collectionID,
|
||||
_getCollectionSyncTime(collectionID),
|
||||
_diffLimit,
|
||||
);
|
||||
if (diff.isNotEmpty) {
|
||||
await _storeDiff(diff, _encryptedFilesSyncTimeKey);
|
||||
await _storeDiff(diff, collectionID);
|
||||
FileRepository.instance.reloadFiles();
|
||||
Bus.instance.fire(CollectionUpdatedEvent(collectionID: collectionID));
|
||||
if (diff.length == _diffLimit) {
|
||||
return await _persistEncryptedFilesDiff();
|
||||
return await _fetchEncryptedFilesDiff(collectionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _getEncryptedFilesSyncTime() {
|
||||
var syncTime = _prefs.getInt(_encryptedFilesSyncTimeKey);
|
||||
int _getCollectionSyncTime(int collectionID) {
|
||||
var syncTime =
|
||||
_prefs.getInt(_collectionSyncTimeKeyPrefix + collectionID.toString());
|
||||
if (syncTime == null) {
|
||||
syncTime = 0;
|
||||
}
|
||||
return syncTime;
|
||||
}
|
||||
|
||||
Future<void> _setCollectionSyncTime(int collectionID, int time) async {
|
||||
return _prefs.setInt(
|
||||
_collectionSyncTimeKeyPrefix + collectionID.toString(), time);
|
||||
}
|
||||
|
||||
Future<void> _uploadDiff() async {
|
||||
final foldersToBackUp = Configuration.instance.getPathsToBackUp();
|
||||
List<File> filesToBeUploaded =
|
||||
|
@ -218,6 +234,8 @@ class SyncService {
|
|||
final uploadedFile = await _uploader.encryptAndUploadFile(file);
|
||||
await _db.update(uploadedFile);
|
||||
}
|
||||
Bus.instance
|
||||
.fire(CollectionUpdatedEvent(collectionID: file.collectionID));
|
||||
Bus.instance.fire(PhotoUploadEvent(
|
||||
completed: i + 1, total: filesToBeUploaded.length));
|
||||
} catch (e) {
|
||||
|
@ -227,45 +245,60 @@ class SyncService {
|
|||
}
|
||||
}
|
||||
|
||||
Future _storeDiff(List<File> diff, String prefKey) async {
|
||||
Future _storeDiff(List<File> diff, int collectionID) async {
|
||||
for (File file in diff) {
|
||||
try {
|
||||
final existingFile = await _db.getMatchingFile(file.localID, file.title,
|
||||
file.deviceFolder, file.creationTime, file.modificationTime,
|
||||
alternateTitle: getHEICFileNameForJPG(file));
|
||||
file.localID = existingFile.localID;
|
||||
if (existingFile.collectionID == null ||
|
||||
existingFile.collectionID == file.collectionID) {
|
||||
// Uploaded for the first time || updated within the same collection
|
||||
file.generatedID = existingFile.generatedID;
|
||||
final existingFiles = await _db.getMatchingFiles(file.title,
|
||||
file.deviceFolder, file.creationTime, file.modificationTime,
|
||||
alternateTitle: getHEICFileNameForJPG(file));
|
||||
if (existingFiles == null) {
|
||||
// File uploaded from a different device
|
||||
file.localID = null;
|
||||
await _db.insert(file);
|
||||
} else {
|
||||
// File exists on device
|
||||
bool wasUploadedOnAPreviousInstallation =
|
||||
existingFiles.length == 1 && existingFiles[0].collectionID == null;
|
||||
file.localID = existingFiles[0]
|
||||
.localID; // File should ideally have the same localID
|
||||
if (wasUploadedOnAPreviousInstallation) {
|
||||
file.generatedID = existingFiles[0].generatedID;
|
||||
await _db.update(file);
|
||||
} else {
|
||||
// If an existing file was added to a collection
|
||||
await _db.insert(file);
|
||||
bool wasUpdatedInExistingCollection = false;
|
||||
for (final existingFile in existingFiles) {
|
||||
if (file.collectionID == existingFile.collectionID) {
|
||||
file.generatedID = existingFile.generatedID;
|
||||
wasUpdatedInExistingCollection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (wasUpdatedInExistingCollection) {
|
||||
await _db.update(file);
|
||||
} else {
|
||||
// Added to a new collection
|
||||
await _db.insert(file);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
file.localID = null; // File uploaded from a different device
|
||||
await _db.insert(file);
|
||||
}
|
||||
await _prefs.setInt(prefKey, file.updationTime);
|
||||
await _setCollectionSyncTime(collectionID, file.updationTime);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePhotosOnServer() async {
|
||||
return _db.getAllDeleted().then((deletedPhotos) async {
|
||||
for (File deletedPhoto in deletedPhotos) {
|
||||
await _deleteFileOnServer(deletedPhoto);
|
||||
await _db.delete(deletedPhoto);
|
||||
Future<void> deleteFilesOnServer() async {
|
||||
return _db.getDeletedFileIDs().then((ids) async {
|
||||
for (int id in ids) {
|
||||
await _deleteFileOnServer(id);
|
||||
await _db.delete(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteFileOnServer(File file) async {
|
||||
Future<void> _deleteFileOnServer(int fileID) async {
|
||||
return _dio
|
||||
.delete(
|
||||
Configuration.instance.getHttpEndpoint() +
|
||||
"/files/" +
|
||||
file.uploadedFileID.toString(),
|
||||
fileID.toString(),
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
|
@ -108,14 +107,18 @@ class UserService {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> setupKey(BuildContext context, String passphrase) async {
|
||||
Future<void> setupAttributes(BuildContext context, String passphrase) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
final result = await _config.generateKey(passphrase);
|
||||
final name = _config.getName();
|
||||
await _dio
|
||||
.put(
|
||||
_config.getHttpEndpoint() + "/users/key-attributes",
|
||||
data: result.keyAttributes.toMap(),
|
||||
_config.getHttpEndpoint() + "/users/attributes",
|
||||
data: {
|
||||
"name": name,
|
||||
"keyAttributes": result.keyAttributes.toMap(),
|
||||
},
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/collection_updated_event.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
|
||||
|
@ -29,6 +31,9 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
? DateTime.now().microsecondsSinceEpoch
|
||||
: lastFile.creationTime,
|
||||
limit),
|
||||
reloadEvent: Bus.instance
|
||||
.on<CollectionUpdatedEvent>()
|
||||
.where((event) => event.collectionID == widget.collection.id),
|
||||
tagPrefix: "collection",
|
||||
selectedFiles: _selectedFiles,
|
||||
);
|
||||
|
@ -37,6 +42,7 @@ class _CollectionPageState extends State<CollectionPage> {
|
|||
GalleryAppBarType.collection,
|
||||
widget.collection.name,
|
||||
_selectedFiles,
|
||||
collection: widget.collection,
|
||||
),
|
||||
body: gallery,
|
||||
);
|
||||
|
|
|
@ -1,20 +1,27 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/collection_updated_event.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/events/tab_changed_event.dart';
|
||||
import 'package:photos/models/collection_items.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/repositories/file_repository.dart';
|
||||
import 'package:photos/services/favorites_service.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/models/device_folder.dart';
|
||||
import 'package:photos/ui/collection_page.dart';
|
||||
import 'package:photos/ui/device_folder_page.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/ui/thumbnail_widget.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
||||
class CollectionsGalleryWidget extends StatefulWidget {
|
||||
const CollectionsGalleryWidget({Key key}) : super(key: key);
|
||||
|
@ -25,11 +32,18 @@ class CollectionsGalleryWidget extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget> {
|
||||
StreamSubscription<LocalPhotosUpdatedEvent> _subscription;
|
||||
final _logger = Logger("CollectionsGallery");
|
||||
StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
|
||||
StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_subscription = Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
|
||||
_localFilesSubscription =
|
||||
Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
_collectionUpdatesSubscription =
|
||||
Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
|
@ -73,16 +87,16 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget> {
|
|||
),
|
||||
),
|
||||
Divider(height: 12),
|
||||
SectionTitle("Collections"),
|
||||
SectionTitle("Saved Collections"),
|
||||
Padding(padding: EdgeInsets.all(6)),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
physics: ScrollPhysics(), // to disable GridView's scrolling
|
||||
itemBuilder: (context, index) {
|
||||
return _buildCollection(context, items.collections[index]);
|
||||
return _buildCollection(context, items.collections, index);
|
||||
},
|
||||
itemCount: items.collections.length,
|
||||
itemCount: items.collections.length + 1, // To include the + button
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
|
@ -93,34 +107,52 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget> {
|
|||
}
|
||||
|
||||
Future<CollectionItems> _getCollections() async {
|
||||
final paths = await FilesDB.instance.getLocalPaths();
|
||||
final filesDB = FilesDB.instance;
|
||||
final filesRepo = FileRepository.instance;
|
||||
final collectionsService = CollectionsService.instance;
|
||||
final userID = Configuration.instance.getUserID();
|
||||
final folders = List<DeviceFolder>();
|
||||
for (final path in paths) {
|
||||
final files = List<File>();
|
||||
for (File file in FileRepository.instance.files) {
|
||||
if (file.deviceFolder == path) {
|
||||
files.add(file);
|
||||
final files = filesRepo.hasLoadedFiles
|
||||
? filesRepo.files
|
||||
: await filesRepo.loadFiles();
|
||||
final filePathMap = LinkedHashMap<String, List<File>>();
|
||||
final thumbnailPathMap = Map<String, File>();
|
||||
for (final file in files) {
|
||||
final path = file.deviceFolder;
|
||||
if (filePathMap[path] == null) {
|
||||
filePathMap[path] = List<File>();
|
||||
}
|
||||
if (file.localID != null) {
|
||||
filePathMap[path].add(file);
|
||||
if (thumbnailPathMap[path] == null) {
|
||||
thumbnailPathMap[path] = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final path in thumbnailPathMap.keys) {
|
||||
final folderName = p.basename(path);
|
||||
folders.add(DeviceFolder(folderName, path, () => files, files[0]));
|
||||
folders.add(DeviceFolder(folderName, path, thumbnailPathMap[path]));
|
||||
}
|
||||
final collectionsWithThumbnail = List<CollectionWithThumbnail>();
|
||||
final collections = collectionsService.getCollections();
|
||||
final ownedCollectionIDs = List<int>();
|
||||
for (final c in collections) {
|
||||
if (c.owner.id == userID) {
|
||||
ownedCollectionIDs.add(c.id);
|
||||
}
|
||||
}
|
||||
final thumbnails =
|
||||
await filesDB.getLastCreatedFilesInCollections(ownedCollectionIDs);
|
||||
final lastUpdatedFiles =
|
||||
await filesDB.getLastUpdatedFilesInCollections(ownedCollectionIDs);
|
||||
for (final collectionID in lastUpdatedFiles.keys) {
|
||||
collectionsWithThumbnail.add(CollectionWithThumbnail(
|
||||
collectionsService.getCollectionByID(collectionID),
|
||||
thumbnails[collectionID],
|
||||
lastUpdatedFiles[collectionID]));
|
||||
}
|
||||
folders.sort((first, second) {
|
||||
return second.thumbnail.creationTime
|
||||
.compareTo(first.thumbnail.creationTime);
|
||||
});
|
||||
|
||||
final collections = List<CollectionWithThumbnail>();
|
||||
final favorites = FavoritesService.instance.getFavoriteFiles().toList();
|
||||
favorites.sort((first, second) {
|
||||
return second.creationTime.compareTo(first.creationTime);
|
||||
});
|
||||
if (favorites.length > 0) {
|
||||
collections.add(CollectionWithThumbnail(
|
||||
await FavoritesService.instance.getFavoritesCollection(),
|
||||
favorites[0]));
|
||||
}
|
||||
return CollectionItems(folders, collections);
|
||||
return CollectionItems(folders, collectionsWithThumbnail);
|
||||
}
|
||||
|
||||
Widget _buildFolder(BuildContext context, DeviceFolder folder) {
|
||||
|
@ -165,19 +197,34 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildCollection(BuildContext context, CollectionWithThumbnail c) {
|
||||
Widget _buildCollection(BuildContext context,
|
||||
List<CollectionWithThumbnail> collections, int index) {
|
||||
if (index == collections.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(28, 12, 28, 46),
|
||||
child: OutlineButton(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
),
|
||||
onPressed: () async {
|
||||
await showToast(
|
||||
"Long press to select photos and click + to create an album.",
|
||||
toastLength: Toast.LENGTH_LONG);
|
||||
Bus.instance.fire(TabChangedEvent(0));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
final c = collections[index];
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
child: Container(
|
||||
child: c.thumbnail ==
|
||||
null // When the user has shared a folder without photos
|
||||
? Icon(Icons.error)
|
||||
: Hero(
|
||||
tag: "collection" + c.thumbnail.tag(),
|
||||
child: ThumbnailWidget(c.thumbnail)),
|
||||
child: Hero(
|
||||
tag: "collection" + c.thumbnail.tag(),
|
||||
child: ThumbnailWidget(c.thumbnail)),
|
||||
height: 150,
|
||||
width: 150,
|
||||
),
|
||||
|
@ -208,7 +255,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
_localFilesSubscription.cancel();
|
||||
_collectionUpdatesSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -235,17 +283,3 @@ class SectionTitle extends StatelessWidget {
|
|||
]));
|
||||
}
|
||||
}
|
||||
|
||||
class CollectionItems {
|
||||
final List<DeviceFolder> folders;
|
||||
final List<CollectionWithThumbnail> collections;
|
||||
|
||||
CollectionItems(this.folders, this.collections);
|
||||
}
|
||||
|
||||
class CollectionWithThumbnail {
|
||||
final Collection collection;
|
||||
final File thumbnail;
|
||||
|
||||
CollectionWithThumbnail(this.collection, this.thumbnail);
|
||||
}
|
||||
|
|
262
lib/ui/create_collection_page.dart
Normal file
262
lib/ui/create_collection_page.dart
Normal file
|
@ -0,0 +1,262 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/collection_items.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/ui/collection_page.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/ui/thumbnail_widget.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
||||
class CreateCollectionPage extends StatefulWidget {
|
||||
final SelectedFiles selectedFiles;
|
||||
const CreateCollectionPage(this.selectedFiles, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CreateCollectionPageState createState() => _CreateCollectionPageState();
|
||||
}
|
||||
|
||||
class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
||||
final _logger = Logger("CreateCollectionPage");
|
||||
String _albumName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Create album"),
|
||||
),
|
||||
body: _getBody(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: OutlineButton(
|
||||
child: Text(
|
||||
"Create a new album",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
onPressed: () {
|
||||
_showNameAlbumDialog();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 12, 8, 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Add to an existing collection",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColorLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_getExistingCollectionsWidget(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getExistingCollectionsWidget() {
|
||||
return FutureBuilder<List<CollectionWithThumbnail>>(
|
||||
future: _getCollectionsWithThumbnail(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
} else if (snapshot.hasData) {
|
||||
return Flexible(
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return _buildCollectionItem(snapshot.data[index]);
|
||||
},
|
||||
itemCount: snapshot.data.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return loadWidget;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollectionItem(CollectionWithThumbnail item) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: GestureDetector(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2.0),
|
||||
child: Container(
|
||||
child: ThumbnailWidget(item.thumbnail),
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.collection.name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
if (await _addToCollection(item.collection.id)) {
|
||||
showToast("Added successfully to '" + item.collection.name);
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CollectionPage(
|
||||
item.collection,
|
||||
)));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
|
||||
final collectionsWithThumbnail = List<CollectionWithThumbnail>();
|
||||
final collections = CollectionsService.instance.getCollections();
|
||||
for (final c in collections) {
|
||||
if (c.owner.id != Configuration.instance.getUserID()) {
|
||||
continue;
|
||||
}
|
||||
var thumbnail = await FilesDB.instance.getLatestFileInCollection(c.id);
|
||||
if (thumbnail == null) {
|
||||
continue;
|
||||
}
|
||||
final lastUpdatedFile =
|
||||
await FilesDB.instance.getLastModifiedFileInCollection(c.id);
|
||||
collectionsWithThumbnail.add(CollectionWithThumbnail(
|
||||
c,
|
||||
thumbnail,
|
||||
lastUpdatedFile,
|
||||
));
|
||||
}
|
||||
collectionsWithThumbnail.sort((first, second) {
|
||||
return second.lastUpdatedFile.updationTime
|
||||
.compareTo(first.lastUpdatedFile.updationTime);
|
||||
});
|
||||
return collectionsWithThumbnail;
|
||||
}
|
||||
|
||||
void _showNameAlbumDialog() async {
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text("Album title"),
|
||||
content: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Christmas 21 / Dinner at Bob's",
|
||||
contentPadding: EdgeInsets.all(8),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_albumName = value;
|
||||
});
|
||||
},
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.text,
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text("OK"),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
final collection = await _createAlbum(_albumName);
|
||||
if (collection != null) {
|
||||
if (await _addToCollection(collection.id)) {
|
||||
showToast("Album '" + _albumName + "' created.");
|
||||
Navigator.pop(context);
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CollectionPage(
|
||||
collection,
|
||||
)));
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _addToCollection(int collectionID) async {
|
||||
final dialog = createProgressDialog(context, "Uploading files to album...");
|
||||
await dialog.show();
|
||||
final files = List<File>();
|
||||
for (final file in widget.selectedFiles.files) {
|
||||
if (file.uploadedFileID == null) {
|
||||
file.collectionID = collectionID;
|
||||
final uploadedFile =
|
||||
(await FileUploader.instance.encryptAndUploadFile(file));
|
||||
await FilesDB.instance.update(uploadedFile);
|
||||
files.add(uploadedFile);
|
||||
} else {
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await CollectionsService.instance.addToCollection(collectionID, files);
|
||||
await dialog.hide();
|
||||
widget.selectedFiles.clearAll();
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
await dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<Collection> _createAlbum(String albumName) async {
|
||||
var collection;
|
||||
final dialog = createProgressDialog(context, "Creating album...");
|
||||
await dialog.show();
|
||||
try {
|
||||
collection = await CollectionsService.instance.createAlbum(albumName);
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
await dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
|
@ -115,9 +115,7 @@ class _DetailPageState extends State<DetailPage> {
|
|||
|
||||
AppBar _buildAppBar() {
|
||||
final actions = List<Widget>();
|
||||
if (_files[_selectedIndex].localID != null) {
|
||||
actions.add(_getFavoriteButton());
|
||||
}
|
||||
actions.add(_getFavoriteButton());
|
||||
actions.add(PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
|
@ -178,8 +176,22 @@ class _DetailPageState extends State<DetailPage> {
|
|||
|
||||
Widget _getFavoriteButton() {
|
||||
final file = _files[_selectedIndex];
|
||||
|
||||
return FutureBuilder(
|
||||
future: FavoritesService.instance.isFavorite(file),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return _getLikeButton(file, snapshot.data);
|
||||
} else {
|
||||
return _getLikeButton(file, false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getLikeButton(File file, bool isLiked) {
|
||||
return LikeButton(
|
||||
isLiked: FavoritesService.instance.isLiked(file),
|
||||
isLiked: isLiked,
|
||||
onTap: (oldValue) async {
|
||||
final isLiked = !oldValue;
|
||||
bool hasError = false;
|
||||
|
@ -250,22 +262,24 @@ class _DetailPageState extends State<DetailPage> {
|
|||
),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
];
|
||||
if (file.fileType == FileType.image) {
|
||||
items.add(Row(
|
||||
children: [
|
||||
Icon(Icons.photo_size_select_actual),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Text(asset.width.toString() + " x " + asset.height.toString()),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
items.add(Row(
|
||||
children: [
|
||||
Icon(Icons.timer),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Text(asset.videoDuration.toString()),
|
||||
],
|
||||
));
|
||||
if (asset != null) {
|
||||
if (file.fileType == FileType.image) {
|
||||
items.add(Row(
|
||||
children: [
|
||||
Icon(Icons.photo_size_select_actual),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Text(asset.width.toString() + " x " + asset.height.toString()),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
items.add(Row(
|
||||
children: [
|
||||
Icon(Icons.timer),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Text(asset.videoDuration.toString()),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
return AlertDialog(
|
||||
title: Text(file.title),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/models/device_folder.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
|
@ -21,7 +22,13 @@ class _DeviceFolderPageState extends State<DeviceFolderPage> {
|
|||
@override
|
||||
Widget build(Object context) {
|
||||
var gallery = Gallery(
|
||||
syncLoader: widget.folder.loader,
|
||||
asyncLoader: (lastFile, limit) => FilesDB.instance
|
||||
.getAllInPathBeforeCreationTime(
|
||||
widget.folder.path,
|
||||
lastFile == null
|
||||
? DateTime.now().microsecondsSinceEpoch
|
||||
: lastFile.creationTime,
|
||||
limit),
|
||||
reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
|
||||
tagPrefix: "device_folder:" + widget.folder.path,
|
||||
selectedFiles: _selectedFiles,
|
||||
|
@ -31,7 +38,7 @@ class _DeviceFolderPageState extends State<DeviceFolderPage> {
|
|||
GalleryAppBarType.local_folder,
|
||||
widget.folder.name,
|
||||
_selectedFiles,
|
||||
widget.folder.thumbnail.deviceFolder,
|
||||
path: widget.folder.thumbnail.deviceFolder,
|
||||
),
|
||||
body: gallery,
|
||||
);
|
||||
|
|
|
@ -4,6 +4,8 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/ui/common_elements.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/email_util.dart';
|
||||
|
||||
class EmailEntryPage extends StatefulWidget {
|
||||
EmailEntryPage({Key key}) : super(key: key);
|
||||
|
@ -13,12 +15,14 @@ class EmailEntryPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
final _config = Configuration.instance;
|
||||
TextEditingController _emailController;
|
||||
TextEditingController _nameController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_emailController =
|
||||
TextEditingController(text: Configuration.instance.getEmail());
|
||||
_emailController = TextEditingController(text: _config.getEmail());
|
||||
_nameController = TextEditingController(text: _config.getName());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -44,23 +48,39 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
|
|||
height: 200,
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(12)),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'your name',
|
||||
contentPadding: EdgeInsets.all(12),
|
||||
),
|
||||
controller: _nameController,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.name,
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'you@email.com',
|
||||
contentPadding: EdgeInsets.all(20),
|
||||
contentPadding: EdgeInsets.all(12),
|
||||
),
|
||||
controller: _emailController,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Padding(padding: EdgeInsets.all(12)),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
child: button("Sign In", onPressed: () {
|
||||
final email = _emailController.text;
|
||||
Configuration.instance.setEmail(email);
|
||||
if (!isValidEmail(email)) {
|
||||
showErrorDialog(context, "Invalid email address",
|
||||
"Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
_config.setEmail(email);
|
||||
_config.setName(_nameController.text);
|
||||
UserService.instance.getOtt(context, email);
|
||||
}),
|
||||
),
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/services/face_search_service.dart';
|
||||
import 'package:photos/models/face.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/ui/gallery.dart';
|
||||
import 'package:photos/ui/gallery_app_bar_widget.dart';
|
||||
|
||||
class FaceSearchResultsPage extends StatelessWidget {
|
||||
final FaceSearchService _faceSearchManager = FaceSearchService.instance;
|
||||
final Face face;
|
||||
final selectedFiles = SelectedFiles();
|
||||
|
||||
FaceSearchResultsPage(this.face, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var gallery = Gallery(
|
||||
asyncLoader: (lastFile, limit) => _faceSearchManager.getFaceSearchResults(
|
||||
face,
|
||||
lastFile == null
|
||||
? DateTime.now().microsecondsSinceEpoch
|
||||
: lastFile.creationTime,
|
||||
limit),
|
||||
tagPrefix: "face_search_results",
|
||||
selectedFiles: selectedFiles,
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: GalleryAppBarWidget(
|
||||
GalleryAppBarType.search_results,
|
||||
"Search results",
|
||||
selectedFiles,
|
||||
),
|
||||
body: Container(
|
||||
child: gallery,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -62,9 +62,11 @@ class _GalleryState extends State<Gallery> {
|
|||
_requiresLoad = true;
|
||||
if (widget.reloadEvent != null) {
|
||||
widget.reloadEvent.listen((event) {
|
||||
setState(() {
|
||||
_requiresLoad = true;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_requiresLoad = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
widget.selectedFiles.addListener(() {
|
||||
|
@ -72,6 +74,9 @@ class _GalleryState extends State<Gallery> {
|
|||
_saveScrollPosition();
|
||||
});
|
||||
});
|
||||
if (widget.asyncLoader == null) {
|
||||
_hasLoadedAll = true;
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -103,7 +108,6 @@ class _GalleryState extends State<Gallery> {
|
|||
}
|
||||
|
||||
Widget _onDataLoaded() {
|
||||
_logger.info("Loaded " + _files.length.toString());
|
||||
if (_files.isEmpty) {
|
||||
return nothingToSeeHere;
|
||||
}
|
||||
|
@ -146,18 +150,23 @@ class _GalleryState extends State<Gallery> {
|
|||
// Eagerly load next batch
|
||||
_loadNextItems();
|
||||
}
|
||||
if (index == _collatedFiles.length) {
|
||||
if (_hasLoadedAll || widget.asyncLoader == null) {
|
||||
return Container();
|
||||
}
|
||||
return loadWidget;
|
||||
}
|
||||
var fileIndex = index;
|
||||
var fileIndex;
|
||||
if (widget.headerWidget != null) {
|
||||
if (index == 0) {
|
||||
return widget.headerWidget;
|
||||
}
|
||||
fileIndex--;
|
||||
fileIndex = index - 1;
|
||||
} else {
|
||||
fileIndex = index;
|
||||
}
|
||||
if (fileIndex == _collatedFiles.length) {
|
||||
if (widget.asyncLoader != null) {
|
||||
if (!_hasLoadedAll) {
|
||||
return loadWidget;
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fileIndex < 0 || fileIndex >= _collatedFiles.length) {
|
||||
return Container();
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/user_authenticated_event.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/ui/create_collection_page.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';
|
||||
import 'package:photos/ui/settings_page.dart';
|
||||
import 'package:photos/ui/share_folder_widget.dart';
|
||||
import 'package:photos/ui/share_collection_widget.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import 'package:photos/utils/share_util.dart';
|
||||
|
@ -29,13 +33,15 @@ class GalleryAppBarWidget extends StatefulWidget
|
|||
final String title;
|
||||
final SelectedFiles selectedFiles;
|
||||
final String path;
|
||||
final Collection collection;
|
||||
|
||||
GalleryAppBarWidget(
|
||||
this.type,
|
||||
this.title,
|
||||
this.selectedFiles, [
|
||||
this.selectedFiles, {
|
||||
this.path,
|
||||
]);
|
||||
this.collection,
|
||||
});
|
||||
|
||||
@override
|
||||
_GalleryAppBarWidgetState createState() => _GalleryAppBarWidgetState();
|
||||
|
@ -45,7 +51,10 @@ class GalleryAppBarWidget extends StatefulWidget
|
|||
}
|
||||
|
||||
class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||
final _logger = Logger("GalleryAppBar");
|
||||
|
||||
StreamSubscription _userAuthEventSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.selectedFiles.addListener(() {
|
||||
|
@ -101,9 +110,10 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
);
|
||||
},
|
||||
));
|
||||
} else if (widget.type == GalleryAppBarType.local_folder) {
|
||||
} else if (widget.type == GalleryAppBarType.local_folder ||
|
||||
widget.type == GalleryAppBarType.collection) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.share),
|
||||
icon: Icon(Icons.person_add),
|
||||
onPressed: () {
|
||||
_showShareCollectionDialog();
|
||||
},
|
||||
|
@ -140,35 +150,116 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
}
|
||||
|
||||
Future<void> _showShareCollectionDialog() async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ShareFolderWidget(
|
||||
widget.title,
|
||||
widget.path,
|
||||
collection:
|
||||
CollectionsService.instance.getCollectionForPath(widget.path),
|
||||
);
|
||||
},
|
||||
);
|
||||
var collection = widget.collection;
|
||||
if (collection == null) {
|
||||
if (widget.type == GalleryAppBarType.local_folder) {
|
||||
collection =
|
||||
CollectionsService.instance.getCollectionForPath(widget.path);
|
||||
if (collection == null) {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
collection = await CollectionsService.instance
|
||||
.getOrCreateForPath(widget.path);
|
||||
await dialog.hide();
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
await dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
"Cannot create a collection of type" + widget.type.toString());
|
||||
}
|
||||
}
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final sharees =
|
||||
await CollectionsService.instance.getSharees(widget.collection.id);
|
||||
await dialog.hide();
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SharingDialog(collection, sharees);
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
await dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createAlbum() async {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.bottomToTop,
|
||||
child: CreateCollectionPage(
|
||||
widget.selectedFiles,
|
||||
)));
|
||||
}
|
||||
|
||||
List<Widget> _getActions(BuildContext context) {
|
||||
List<Widget> actions = List<Widget>();
|
||||
if (widget.selectedFiles.files.isNotEmpty) {
|
||||
if (widget.type != GalleryAppBarType.shared_collection &&
|
||||
widget.type != GalleryAppBarType.search_results) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
_showDeleteSheet(context);
|
||||
},
|
||||
));
|
||||
}
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.add),
|
||||
onPressed: () {
|
||||
_createAlbum();
|
||||
},
|
||||
));
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.share),
|
||||
onPressed: () {
|
||||
_shareSelected(context);
|
||||
},
|
||||
));
|
||||
if (widget.type == GalleryAppBarType.homepage ||
|
||||
widget.type == GalleryAppBarType.local_folder) {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(Icons.share),
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
_shareSelected(context);
|
||||
_showDeleteSheet(context);
|
||||
},
|
||||
));
|
||||
} else if (widget.type == GalleryAppBarType.collection) {
|
||||
actions.add(PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.remove_circle),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Remove"),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
),
|
||||
Text("Delete"),
|
||||
],
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 1) {
|
||||
_showRemoveFromCollectionSheet(context);
|
||||
} else if (value == 2) {
|
||||
_showDeleteSheet(context);
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -179,9 +270,55 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
|||
shareMultiple(context, widget.selectedFiles.files.toList());
|
||||
}
|
||||
|
||||
void _showDeleteSheet(BuildContext context) {
|
||||
void _showRemoveFromCollectionSheet(BuildContext context) {
|
||||
final count = widget.selectedFiles.files.length;
|
||||
final action = CupertinoActionSheet(
|
||||
title: Text("Delete file?"),
|
||||
title: Text("Remove " +
|
||||
count.toString() +
|
||||
" file" +
|
||||
(count == 1 ? "" : "s") +
|
||||
" from " +
|
||||
widget.collection.name +
|
||||
"?"),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Remove"),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () async {
|
||||
final dialog = createProgressDialog(context, "Removing files...");
|
||||
await dialog.show();
|
||||
try {
|
||||
CollectionsService.instance.removeFromCollection(
|
||||
widget.collection.id, widget.selectedFiles.files.toList());
|
||||
await dialog.hide();
|
||||
widget.selectedFiles.clearAll();
|
||||
Navigator.of(context).pop();
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
await dialog.hide();
|
||||
Navigator.of(context).pop();
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: Text("Cancel"),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
showCupertinoModalPopup(context: context, builder: (_) => action);
|
||||
}
|
||||
|
||||
void _showDeleteSheet(BuildContext context) {
|
||||
final count = widget.selectedFiles.files.length;
|
||||
final action = CupertinoActionSheet(
|
||||
title: Text("Permanently delete " +
|
||||
count.toString() +
|
||||
" file" +
|
||||
(count == 1 ? "?" : "s?")),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: Text("Delete"),
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import 'package:photos/events/tab_changed_event.dart';
|
||||
import 'package:photos/models/filters/important_items_filter.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/repositories/file_repository.dart';
|
||||
|
@ -21,8 +22,6 @@ import 'package:photos/ui/memories_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';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
|
||||
|
@ -43,23 +42,22 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
final _selectedFiles = SelectedFiles();
|
||||
final _memoriesWidget = MemoriesWidget();
|
||||
|
||||
ShakeDetector _detector;
|
||||
int _selectedNavBarItem = 0;
|
||||
StreamSubscription<LocalPhotosUpdatedEvent>
|
||||
_localPhotosUpdatedEventSubscription;
|
||||
StreamSubscription<LocalPhotosUpdatedEvent> _photosUpdatedEvent;
|
||||
StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_detector = ShakeDetector.autoStart(
|
||||
shakeThresholdGravity: 3,
|
||||
onPhoneShake: () {
|
||||
_logger.info("Emailing logs");
|
||||
LoggingUtil.instance.emailLogs();
|
||||
});
|
||||
_localPhotosUpdatedEventSubscription =
|
||||
_photosUpdatedEvent =
|
||||
Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
_tabChangedEventSubscription =
|
||||
Bus.instance.on<TabChangedEvent>().listen((event) {
|
||||
setState(() {
|
||||
_selectedNavBarItem = event.selectedIndex;
|
||||
});
|
||||
});
|
||||
_initDeepLinks();
|
||||
super.initState();
|
||||
}
|
||||
|
@ -71,7 +69,6 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
GalleryAppBarType.homepage,
|
||||
widget.title,
|
||||
_selectedFiles,
|
||||
"/",
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNavigationBar(),
|
||||
body: IndexedStack(
|
||||
|
@ -145,9 +142,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
|
||||
Widget _getMainGalleryWidget() {
|
||||
return FutureBuilder(
|
||||
future: FileRepository.instance.loadFiles().then((files) {
|
||||
return _getFilteredPhotos(files);
|
||||
}),
|
||||
future: FileRepository.instance.loadFiles(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Gallery(
|
||||
|
@ -209,8 +204,8 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_detector.stopListening();
|
||||
_localPhotosUpdatedEventSubscription.cancel();
|
||||
_tabChangedEventSubscription.cancel();
|
||||
_photosUpdatedEvent.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
Widget _getBody() {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(8, 40, 8, 8),
|
||||
padding: EdgeInsets.fromLTRB(8, 8, 8, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
@ -38,8 +38,8 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
|||
children: [
|
||||
Image.asset(
|
||||
"assets/email_sent.png",
|
||||
width: 256,
|
||||
height: 256,
|
||||
width: 220,
|
||||
height: 220,
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(12)),
|
||||
Text.rich(
|
||||
|
|
|
@ -139,7 +139,7 @@ class _PassphraseEntryPageState extends State<PassphraseEntryPage> {
|
|||
child: Text("Confirm"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
UserService.instance.setupKey(context, _passphraseController1.text);
|
||||
UserService.instance.setupAttributes(context, _passphraseController1.text);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/services/face_search_service.dart';
|
||||
import 'package:photos/models/face.dart';
|
||||
import 'package:photos/ui/circular_network_image_widget.dart';
|
||||
import 'package:photos/ui/face_search_results_page.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/ui/location_search_widget.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
|
@ -12,9 +7,6 @@ class SearchPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
final FaceSearchService _faceSearchManager = FaceSearchService.instance;
|
||||
String _searchString = "";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -27,53 +19,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||
)
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
child: _searchString.isEmpty ? _getSearchSuggestions() : Container(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getSearchSuggestions() {
|
||||
return FutureBuilder<List<Face>>(
|
||||
future: _faceSearchManager.getFaces(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Container(
|
||||
height: 60,
|
||||
margin: EdgeInsets.only(top: 4, left: 4),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildItem(context, snapshot.data[index]);
|
||||
}),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text("Error: " + snapshot.error.toString()));
|
||||
} else {
|
||||
return Center(child: loadWidget);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, Face face) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_routeToSearchResults(face, context);
|
||||
},
|
||||
child: CircularNetworkImageWidget(face.getThumbnailUrl(), 60),
|
||||
);
|
||||
}
|
||||
|
||||
void _routeToSearchResults(Face face, BuildContext context) {
|
||||
final page = FaceSearchResultsPage(face);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
body: Container(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:crisp/crisp.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
@ -6,10 +9,12 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:flutter_email_sender/flutter_email_sender.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({Key key}) : super(key: key);
|
||||
|
@ -235,8 +240,20 @@ class SupportSectionWidget extends StatelessWidget {
|
|||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
final dialog = createProgressDialog(context, "Preparing logs...");
|
||||
await dialog.show();
|
||||
final tempPath = (await getTemporaryDirectory()).path;
|
||||
final zipFilePath = tempPath + "/logs.zip";
|
||||
final logsDirectory = Directory(tempPath + "/logs");
|
||||
var encoder = ZipFileEncoder();
|
||||
encoder.create(zipFilePath);
|
||||
encoder.addDirectory(logsDirectory);
|
||||
encoder.close();
|
||||
await dialog.hide();
|
||||
final Email email = Email(
|
||||
recipients: ['support@ente.io'],
|
||||
cc: ['vishnu@ente.io'],
|
||||
attachmentPaths: [zipFilePath],
|
||||
isHTML: false,
|
||||
);
|
||||
await FlutterEmailSender.send(email);
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/db/public_keys_db.dart';
|
||||
import 'package:photos/models/collection.dart';
|
||||
|
@ -16,49 +17,11 @@ 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);
|
||||
|
||||
@override
|
||||
_ShareFolderWidgetState createState() => _ShareFolderWidgetState();
|
||||
}
|
||||
|
||||
class _ShareFolderWidgetState extends State<ShareFolderWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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 SharingDialog(widget.collection, snapshot.data, widget.path);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
} else {
|
||||
return loadWidget;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SharingDialog extends StatefulWidget {
|
||||
final Collection collection;
|
||||
final List<String> sharees;
|
||||
final String path;
|
||||
|
||||
SharingDialog(this.collection, this.sharees, this.path, {Key key})
|
||||
: super(key: key);
|
||||
SharingDialog(this.collection, this.sharees, {Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SharingDialogState createState() => _SharingDialogState();
|
||||
|
@ -75,10 +38,12 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
final children = List<Widget>();
|
||||
if (!_showEntryField &&
|
||||
(widget.collection == null || _sharees.length == 0)) {
|
||||
children.add(Text("Click the + button to share this folder."));
|
||||
children.add(Text("Click the + button to share this " +
|
||||
Collection.typeToString(widget.collection.type) +
|
||||
"."));
|
||||
} else {
|
||||
for (final email in _sharees) {
|
||||
children.add(EmailItemWidget(email));
|
||||
children.add(EmailItemWidget(widget.collection.id, email));
|
||||
}
|
||||
}
|
||||
if (_showEntryField) {
|
||||
|
@ -170,13 +135,13 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
"Please enter a valid email address.");
|
||||
return;
|
||||
} else if (email == Configuration.instance.getEmail()) {
|
||||
showErrorDialog(
|
||||
context, "Oops", "You cannot share the album with yourself.");
|
||||
showErrorDialog(context, "Oops", "You cannot share with yourself.");
|
||||
return;
|
||||
}
|
||||
if (publicKey == null) {
|
||||
final dialog = createProgressDialog(context, "Searching for user...");
|
||||
await dialog.show();
|
||||
|
||||
publicKey = await UserService.instance.getPublicKey(email);
|
||||
await dialog.hide();
|
||||
}
|
||||
|
@ -192,7 +157,7 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
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.");
|
||||
"Hey, I have some really nice photos to share. Please install ente.io so that I can share them privately.");
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -206,20 +171,20 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
} else {
|
||||
final dialog = createProgressDialog(context, "Sharing...");
|
||||
await dialog.show();
|
||||
var collectionID;
|
||||
if (widget.collection != null) {
|
||||
collectionID = widget.collection.id;
|
||||
} else {
|
||||
collectionID =
|
||||
(await CollectionsService.instance.getOrCreateForPath(widget.path))
|
||||
.id;
|
||||
await Configuration.instance.addPathToFoldersToBeBackedUp(widget.path);
|
||||
SyncService.instance.sync();
|
||||
final collection = widget.collection;
|
||||
if (collection.type == CollectionType.folder) {
|
||||
final path =
|
||||
CollectionsService.instance.decryptCollectionPath(collection);
|
||||
if (!Configuration.instance.getPathsToBackUp().contains(path)) {
|
||||
await Configuration.instance.addPathToFoldersToBeBackedUp(path);
|
||||
SyncService.instance.sync();
|
||||
}
|
||||
}
|
||||
try {
|
||||
await CollectionsService.instance.share(collectionID, email, publicKey);
|
||||
await CollectionsService.instance
|
||||
.share(widget.collection.id, email, publicKey);
|
||||
await dialog.hide();
|
||||
showToast("Folder shared successfully!");
|
||||
showToast("Shared successfully!");
|
||||
setState(() {
|
||||
_sharees.add(email);
|
||||
_showEntryField = false;
|
||||
|
@ -233,8 +198,11 @@ class _SharingDialogState extends State<SharingDialog> {
|
|||
}
|
||||
|
||||
class EmailItemWidget extends StatelessWidget {
|
||||
final int collectionID;
|
||||
final String email;
|
||||
|
||||
const EmailItemWidget(
|
||||
this.collectionID,
|
||||
this.email, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
@ -252,9 +220,24 @@ class EmailItemWidget extends StatelessWidget {
|
|||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.delete_forever,
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_forever),
|
||||
color: Colors.redAccent,
|
||||
onPressed: () async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
await CollectionsService.instance
|
||||
.unshare(collectionID, email);
|
||||
await dialog.hide();
|
||||
showToast("Stopped sharing with " + email + ".");
|
||||
Navigator.of(context).pop();
|
||||
} catch (e, s) {
|
||||
Logger("EmailItemWidget").severe(e, s);
|
||||
await dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
|
@ -1,12 +1,12 @@
|
|||
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/models/collection.dart';
|
||||
import 'package:photos/ui/gallery.dart';
|
||||
import 'package:photos/ui/gallery_app_bar_widget.dart';
|
||||
|
||||
class SharedCollectionPage extends StatefulWidget {
|
||||
final SharedCollection collection;
|
||||
final Collection collection;
|
||||
|
||||
const SharedCollectionPage(this.collection, {Key key}) : super(key: key);
|
||||
|
||||
|
|
|
@ -3,12 +3,12 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.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/events/collection_updated_event.dart';
|
||||
import 'package:photos/models/collection_items.dart';
|
||||
import 'package:photos/ui/common_elements.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/ui/shared_collection_page.dart';
|
||||
|
@ -24,35 +24,43 @@ class SharedCollectionGallery extends StatefulWidget {
|
|||
|
||||
class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
|
||||
Logger _logger = Logger("SharedCollectionGallery");
|
||||
StreamSubscription<RemoteSyncEvent> _subscription;
|
||||
StreamSubscription<CollectionUpdatedEvent> _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_subscription = Bus.instance.on<RemoteSyncEvent>().listen((event) {
|
||||
if (event.success) {
|
||||
setState(() {});
|
||||
}
|
||||
_subscription = Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<SharedCollectionWithThumbnail>>(
|
||||
future: CollectionsDB.instance
|
||||
.getAllSharedCollections()
|
||||
.then((collections) async {
|
||||
final c = List<SharedCollectionWithThumbnail>();
|
||||
return FutureBuilder<List<CollectionWithThumbnail>>(
|
||||
future:
|
||||
CollectionsDB.instance.getAllCollections().then((collections) async {
|
||||
final c = List<CollectionWithThumbnail>();
|
||||
for (final collection in collections) {
|
||||
var thumbnail;
|
||||
try {
|
||||
thumbnail =
|
||||
await FilesDB.instance.getLatestFileInCollection(collection.id);
|
||||
} catch (e) {
|
||||
_logger.warning(e.toString());
|
||||
if (collection.owner.id == Configuration.instance.getUserID()) {
|
||||
continue;
|
||||
}
|
||||
c.add(SharedCollectionWithThumbnail(collection, thumbnail));
|
||||
final thumbnail =
|
||||
await FilesDB.instance.getLatestFileInCollection(collection.id);
|
||||
if (thumbnail == null) {
|
||||
continue;
|
||||
}
|
||||
final lastUpdatedFile = await FilesDB.instance
|
||||
.getLastModifiedFileInCollection(collection.id);
|
||||
c.add(CollectionWithThumbnail(
|
||||
collection,
|
||||
thumbnail,
|
||||
lastUpdatedFile,
|
||||
));
|
||||
}
|
||||
c.sort((first, second) {
|
||||
return second.lastUpdatedFile.updationTime
|
||||
.compareTo(first.lastUpdatedFile.updationTime);
|
||||
});
|
||||
return c;
|
||||
}),
|
||||
builder: (context, snapshot) {
|
||||
|
@ -73,7 +81,7 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
|
|||
}
|
||||
|
||||
Widget _getSharedCollectionsGallery(
|
||||
List<SharedCollectionWithThumbnail> collections) {
|
||||
List<CollectionWithThumbnail> collections) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 24),
|
||||
child: GridView.builder(
|
||||
|
@ -91,21 +99,35 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildCollection(
|
||||
BuildContext context, SharedCollectionWithThumbnail c) {
|
||||
_logger.info("Building collection " + c.collection.toString());
|
||||
Widget _buildCollection(BuildContext context, CollectionWithThumbnail c) {
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
child: Container(
|
||||
child: c.thumbnail ==
|
||||
null // When the user has shared a folder without photos
|
||||
? Icon(Icons.error)
|
||||
: Hero(
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: "shared_collection" + c.thumbnail.tag(),
|
||||
child: ThumbnailWidget(c.thumbnail)),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Container(
|
||||
child: Text(
|
||||
c.collection.owner.name.substring(0, 1),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
padding: EdgeInsets.all(8),
|
||||
margin: EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).accentColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
height: 150,
|
||||
width: 150,
|
||||
),
|
||||
|
@ -140,10 +162,3 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
|
|||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SharedCollectionWithThumbnail {
|
||||
final SharedCollection collection;
|
||||
final File thumbnail;
|
||||
|
||||
SharedCollectionWithThumbnail(this.collection, this.thumbnail);
|
||||
}
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity/connectivity.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class EndpointFinder {
|
||||
final _dio = Dio();
|
||||
final logger = Logger("EndpointFinder");
|
||||
|
||||
EndpointFinder._privateConstructor() {
|
||||
_dio.options = BaseOptions(connectTimeout: 250);
|
||||
}
|
||||
|
||||
static final EndpointFinder instance = EndpointFinder._privateConstructor();
|
||||
|
||||
bool _shouldContinueSearch;
|
||||
|
||||
Future<String> findEndpoint() {
|
||||
_shouldContinueSearch = true;
|
||||
return (Connectivity().getWifiIP()).then((ip) async {
|
||||
logger.info(ip);
|
||||
final ipSplit = ip.split(".");
|
||||
var prefix = "";
|
||||
for (int index = 0; index < ipSplit.length; index++) {
|
||||
if (index != ipSplit.length - 1) {
|
||||
prefix += ipSplit[index] + ".";
|
||||
}
|
||||
}
|
||||
logger.info(prefix + "*");
|
||||
|
||||
for (int i = 1; i <= 255 && _shouldContinueSearch; i++) {
|
||||
var endpoint = prefix + i.toString();
|
||||
try {
|
||||
final success = await ping(endpoint);
|
||||
if (success) {
|
||||
return endpoint;
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
if (_shouldContinueSearch) {
|
||||
throw TimeoutException("Could not find a valid endpoint");
|
||||
} else {
|
||||
// Exit gracefully
|
||||
return Future.value(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void cancelSearch() {
|
||||
_shouldContinueSearch = false;
|
||||
}
|
||||
|
||||
Future<bool> ping(String endpoint) async {
|
||||
return _dio.get("http://" + endpoint + ":8080/ping").then((response) {
|
||||
if (response.data["message"] == "pong") {
|
||||
logger.info("Found " + endpoint);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/collection_updated_event.dart';
|
||||
import 'package:photos/events/remote_sync_event.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
|
@ -15,14 +16,16 @@ class DiffFetcher {
|
|||
final _logger = Logger("FileDownloader");
|
||||
final _dio = Dio();
|
||||
|
||||
Future<List<File>> getEncryptedFilesDiff(int lastSyncTime, int limit) async {
|
||||
Future<List<File>> getEncryptedFilesDiff(
|
||||
int collectionID, int sinceTime, int limit) async {
|
||||
return _dio
|
||||
.get(
|
||||
Configuration.instance.getHttpEndpoint() + "/files/diff",
|
||||
Configuration.instance.getHttpEndpoint() + "/collections/diff",
|
||||
options: Options(
|
||||
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
|
||||
queryParameters: {
|
||||
"sinceTime": lastSyncTime,
|
||||
"collectionID": collectionID,
|
||||
"sinceTime": sinceTime,
|
||||
"limit": limit,
|
||||
},
|
||||
)
|
||||
|
@ -39,6 +42,8 @@ class DiffFetcher {
|
|||
if (item["isDeleted"]) {
|
||||
await FilesDB.instance.deleteFromCollection(
|
||||
file.uploadedFileID, file.collectionID);
|
||||
Bus.instance.fire(
|
||||
CollectionUpdatedEvent(collectionID: file.collectionID));
|
||||
continue;
|
||||
}
|
||||
file.ownerID = item["ownerID"];
|
||||
|
|
|
@ -14,7 +14,9 @@ import 'package:photos/core/cache/thumbnail_cache_manager.dart';
|
|||
import 'package:photos/core/cache/video_cache_manager.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/collection_updated_event.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/file_type.dart';
|
||||
import 'package:photos/repositories/file_repository.dart';
|
||||
|
@ -26,13 +28,24 @@ import 'crypto_util.dart';
|
|||
final logger = Logger("FileUtil");
|
||||
|
||||
Future<void> deleteFiles(List<File> files) async {
|
||||
await PhotoManager.editor
|
||||
.deleteWithIds(files.map((file) => file.localID).toList());
|
||||
for (File file in files) {
|
||||
await FilesDB.instance.markForDeletion(file);
|
||||
final localIDs = List<String>();
|
||||
bool hasUploadedFiles = false;
|
||||
for (final file in files) {
|
||||
if (file.localID != null) {
|
||||
localIDs.add(file.localID);
|
||||
}
|
||||
if (file.uploadedFileID != null) {
|
||||
hasUploadedFiles = true;
|
||||
await FilesDB.instance.markForDeletion(file.uploadedFileID);
|
||||
}
|
||||
}
|
||||
await PhotoManager.editor.deleteWithIds(localIDs);
|
||||
await FileRepository.instance.reloadFiles();
|
||||
SyncService.instance.sync();
|
||||
if (hasUploadedFiles) {
|
||||
Bus.instance.fire(CollectionUpdatedEvent());
|
||||
// TODO: Blocking call?
|
||||
SyncService.instance.deleteFilesOnServer();
|
||||
}
|
||||
}
|
||||
|
||||
void preloadFile(File file) {
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:flutter_email_sender/flutter_email_sender.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class LoggingUtil {
|
||||
LoggingUtil._privateConstructor();
|
||||
|
||||
static final LoggingUtil instance = LoggingUtil._privateConstructor();
|
||||
|
||||
bool _isInProgress = false;
|
||||
Future<void> emailLogs() async {
|
||||
if (_isInProgress) {
|
||||
return;
|
||||
}
|
||||
_isInProgress = true;
|
||||
final tempPath = (await getTemporaryDirectory()).path;
|
||||
final zipFilePath = tempPath + "/logs.zip";
|
||||
Directory logsDirectory = Directory(tempPath + "/logs");
|
||||
var encoder = ZipFileEncoder();
|
||||
encoder.create(zipFilePath);
|
||||
encoder.addDirectory(logsDirectory);
|
||||
encoder.close();
|
||||
final Email email = Email(
|
||||
body: 'Logs attached.',
|
||||
subject: 'Error, error, share the terror.',
|
||||
recipients: ['android-support@ente.io'],
|
||||
cc: ['vishnumohandas@gmail.com'],
|
||||
attachmentPaths: [zipFilePath],
|
||||
isHTML: false,
|
||||
);
|
||||
await FlutterEmailSender.send(email);
|
||||
_isInProgress = false;
|
||||
}
|
||||
}
|
|
@ -23,10 +23,11 @@ Future<void> shareMultiple(BuildContext context, List<File> files) async {
|
|||
}
|
||||
final dialog = createProgressDialog(context, "Preparing...");
|
||||
await dialog.show();
|
||||
final pathList = List<String>();
|
||||
final pathFutures = List<Future<String>>();
|
||||
for (File file in files) {
|
||||
pathList.add((await getNativeFile(file)).path);
|
||||
pathFutures.add(getNativeFile(file).then((file) => file.path));
|
||||
}
|
||||
final pathList = await Future.wait(pathFutures);
|
||||
await dialog.hide();
|
||||
return ShareExtend.shareMultiple(pathList, "image");
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
void showToast(String message, {toastLength: Toast.LENGTH_SHORT}) {
|
||||
Fluttertoast.showToast(
|
||||
Future<void> showToast(String message, {toastLength: Toast.LENGTH_SHORT}) {
|
||||
return Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: toastLength,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
timeInSecForIosWeb: 1,
|
||||
backgroundColor: Colors.grey[850],
|
||||
backgroundColor: Colors.grey[800],
|
||||
textColor: Colors.white,
|
||||
fontSize: 16.0);
|
||||
}
|
||||
|
|
72
pubspec.lock
72
pubspec.lock
|
@ -85,34 +85,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
connectivity:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.9+5"
|
||||
connectivity_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_for_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1+2"
|
||||
connectivity_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0+5"
|
||||
connectivity_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -183,20 +155,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
extended_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
extended_image_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: extended_image_library
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -317,13 +275,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.2"
|
||||
http_client_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_client_helper
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -415,6 +366,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
page_transition:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: page_transition
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.7+2"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -477,7 +435,7 @@ packages:
|
|||
name: photo_manager
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.8"
|
||||
version: "0.6.0-dev.5"
|
||||
photo_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -548,13 +506,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.24.1"
|
||||
sensors:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sensors
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2+6"
|
||||
sentry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -562,13 +513,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
shake:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shake
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
share_extend:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -23,7 +23,7 @@ dependencies:
|
|||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^0.1.2
|
||||
photo_manager: ^0.5.8
|
||||
photo_manager: ^0.6.0-dev.5
|
||||
provider: ^3.1.0
|
||||
sqflite: ^1.3.0
|
||||
path_provider: ^1.6.5
|
||||
|
@ -35,11 +35,9 @@ dependencies:
|
|||
draggable_scrollbar: ^0.0.4
|
||||
photo_view: ^0.9.2
|
||||
visibility_detector: ^0.1.5
|
||||
connectivity: ^0.4.8+2
|
||||
event_bus: ^1.1.1
|
||||
sentry: ">=3.0.0 <4.0.0"
|
||||
super_logging: ^1.0.0
|
||||
shake: ^0.1.0
|
||||
archive: ^2.0.11
|
||||
flutter_email_sender: ^3.0.1
|
||||
like_button: ^0.2.0
|
||||
|
@ -48,7 +46,6 @@ dependencies:
|
|||
flutter_typeahead: ^1.8.1
|
||||
pull_to_refresh: ^1.6.2
|
||||
fluttertoast: ^4.0.1
|
||||
extended_image: ^0.9.0
|
||||
video_player: ^0.10.11+1
|
||||
chewie: ^0.9.10
|
||||
cached_network_image: ^2.3.0-beta
|
||||
|
@ -61,6 +58,7 @@ dependencies:
|
|||
crisp: ^0.1.3
|
||||
flutter_sodium: ^0.1.8
|
||||
pedantic: ^1.9.2
|
||||
page_transition: "^1.1.7+2"
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Add table
Reference in a new issue