Merge pull request #5 from ente-io/albums

Introduce albums
This commit is contained in:
Vishnu Mohandas 2020-11-01 12:37:26 +05:30 committed by GitHub
commit 79a195abb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1311 additions and 1053 deletions

View file

@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 28
compileSdkVersion 29
sourceSets {
main.java.srcDirs += 'src/main/kotlin'

View file

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

View file

@ -0,0 +1 @@
include ':app'

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
import 'package:photos/events/event.dart';
class CollectionUpdatedEvent extends Event {
final int collectionID;
CollectionUpdatedEvent({this.collectionID});
}

View file

@ -0,0 +1,7 @@
import 'package:sentry/sentry.dart';
class TabChangedEvent extends Event {
final selectedIndex;
TabChangedEvent(this.selectedIndex);
}

View file

@ -6,7 +6,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/services/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);

View file

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

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

View file

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

View file

@ -12,7 +12,6 @@ class File {
String localID;
String title;
String deviceFolder;
int remoteFolderID;
bool isEncrypted;
int creationTime;
int modificationTime;

View file

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:photos/models/file.dart';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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