Update shared collections interface

This commit is contained in:
Vishnu Mohandas 2020-11-02 20:08:59 +05:30
parent 989594f1ef
commit 96dba3f905
8 changed files with 243 additions and 92 deletions

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart';
@ -12,15 +13,14 @@ class CollectionsDB {
static final collectionsTable = 'collections';
static final columnID = 'collection_id';
static final columnOwnerID = 'owner_id';
static final columnOwnerEmail = 'owner_email';
static final columnOwnerName = 'owner_name';
static final columnOwner = 'owner';
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 columnSharees = 'sharees';
static final columnUpdationTime = 'updation_time';
CollectionsDB._privateConstructor();
@ -47,15 +47,14 @@ class CollectionsDB {
await db.execute('''
CREATE TABLE $collectionsTable (
$columnID INTEGER PRIMARY KEY NOT NULL,
$columnOwnerID INTEGER NOT NULL,
$columnOwnerEmail TEXT,
$columnOwnerName TEXT,
$columnOwner TEXT NOT NULL,
$columnEncryptedKey TEXT NOT NULL,
$columnKeyDecryptionNonce TEXT,
$columnName TEXT NOT NULL,
$columnType TEXT NOT NULL,
$columnEncryptedPath TEXT,
$columnPathDecryptionNonce TEXT,
$columnSharees TEXT,
$columnUpdationTime TEXT NOT NULL
)
''');
@ -107,15 +106,15 @@ class CollectionsDB {
Map<String, dynamic> _getRowForCollection(Collection collection) {
var row = new Map<String, dynamic>();
row[columnID] = collection.id;
row[columnOwnerID] = collection.owner.id;
row[columnOwnerEmail] = collection.owner.email;
row[columnOwnerName] = collection.owner.name;
row[columnOwner] = collection.owner.toJson();
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[columnSharees] =
json.encode(collection.sharees?.map((x) => x?.toMap())?.toList());
row[columnUpdationTime] = collection.updationTime;
return row;
}
@ -123,11 +122,7 @@ class CollectionsDB {
Collection _convertToCollection(Map<String, dynamic> row) {
return Collection(
row[columnID],
CollectionOwner(
id: row[columnOwnerID],
email: row[columnOwnerEmail],
name: row[columnOwnerName],
),
User.fromJson(row[columnOwner]),
row[columnEncryptedKey],
row[columnKeyDecryptionNonce],
row[columnName],
@ -135,6 +130,8 @@ class CollectionsDB {
CollectionAttributes(
encryptedPath: row[columnEncryptedPath],
pathDecryptionNonce: row[columnPathDecryptionNonce]),
List<User>.from((json.decode(row[columnSharees]) as List)
.map((x) => User.fromMap(x))),
int.parse(row[columnUpdationTime]),
);
}

View file

@ -1,13 +1,16 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
class Collection {
final int id;
final CollectionOwner owner;
final User owner;
final String encryptedKey;
final String keyDecryptionNonce;
final String name;
final CollectionType type;
final CollectionAttributes attributes;
final List<User> sharees;
final int updationTime;
final bool isDeleted;
@ -19,6 +22,7 @@ class Collection {
this.name,
this.type,
this.attributes,
this.sharees,
this.updationTime, {
this.isDeleted = false,
});
@ -44,6 +48,32 @@ class Collection {
}
}
Collection copyWith({
int id,
User owner,
String encryptedKey,
String keyDecryptionNonce,
String name,
CollectionType type,
CollectionAttributes attributes,
List<User> sharees,
int updationTime,
bool isDeleted,
}) {
return Collection(
id ?? this.id,
owner ?? this.owner,
encryptedKey ?? this.encryptedKey,
keyDecryptionNonce ?? this.keyDecryptionNonce,
name ?? this.name,
type ?? this.type,
attributes ?? this.attributes,
sharees ?? this.sharees,
updationTime ?? this.updationTime,
isDeleted: isDeleted ?? this.isDeleted,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
@ -53,21 +83,26 @@ class Collection {
'name': name,
'type': typeToString(type),
'attributes': attributes?.toMap(),
'sharees': sharees?.map((x) => x?.toMap())?.toList(),
'updationTime': updationTime,
'isDeleted': isDeleted,
};
}
factory Collection.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
final sharees = (map['sharees'] == null || map['sharees'].length == 0)
? List<User>()
: List<User>.from(map['sharees'].map((x) => User.fromMap(x)));
return Collection(
map['id'],
CollectionOwner.fromMap(map['owner']),
User.fromMap(map['owner']),
map['encryptedKey'],
map['keyDecryptionNonce'],
map['name'],
typeFromString(map['type']),
CollectionAttributes.fromMap(map['attributes']),
sharees,
map['updationTime'],
isDeleted: map['isDeleted'] ?? false,
);
@ -80,7 +115,7 @@ class Collection {
@override
String toString() {
return 'Collection(id: $id, owner: ${owner.toString()} encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, type: $type, attributes: $attributes, creationTime: $updationTime)';
return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, type: $type, attributes: $attributes, sharees: $sharees, updationTime: $updationTime, isDeleted: $isDeleted)';
}
@override
@ -95,7 +130,9 @@ class Collection {
o.name == name &&
o.type == type &&
o.attributes == attributes &&
o.updationTime == updationTime;
listEquals(o.sharees, sharees) &&
o.updationTime == updationTime &&
o.isDeleted == isDeleted;
}
@override
@ -107,7 +144,9 @@ class Collection {
name.hashCode ^
type.hashCode ^
attributes.hashCode ^
updationTime.hashCode;
sharees.hashCode ^
updationTime.hashCode ^
isDeleted.hashCode;
}
}
@ -178,23 +217,23 @@ class CollectionAttributes {
int get hashCode => encryptedPath.hashCode ^ pathDecryptionNonce.hashCode;
}
class CollectionOwner {
class User {
int id;
String email;
String name;
CollectionOwner({
User({
this.id,
this.email,
this.name,
});
CollectionOwner copyWith({
User copyWith({
int id,
String email,
String name,
}) {
return CollectionOwner(
return User(
id: id ?? this.id,
email: email ?? this.email,
name: name ?? this.name,
@ -209,10 +248,10 @@ class CollectionOwner {
};
}
factory CollectionOwner.fromMap(Map<String, dynamic> map) {
factory User.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return CollectionOwner(
return User(
id: map['id'],
email: map['email'],
name: map['name'],
@ -221,8 +260,7 @@ class CollectionOwner {
String toJson() => json.encode(toMap());
factory CollectionOwner.fromJson(String source) =>
CollectionOwner.fromMap(json.decode(source));
factory User.fromJson(String source) => User.fromMap(json.decode(source));
@override
String toString() => 'CollectionOwner(id: $id, email: $email, name: $name)';
@ -231,10 +269,7 @@ class CollectionOwner {
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is CollectionOwner &&
o.id == id &&
o.email == email &&
o.name == name;
return o is User && o.id == id && o.email == email && o.name == name;
}
@override

View file

@ -20,3 +20,10 @@ class CollectionWithThumbnail {
this.lastUpdatedFile,
);
}
class SharedCollections {
final List<CollectionWithThumbnail> outgoing;
final List<CollectionWithThumbnail> incoming;
SharedCollections(this.outgoing, this.incoming);
}

View file

@ -78,7 +78,7 @@ class CollectionsService {
return _collectionIDToCollections.values.toList();
}
Future<List<String>> getSharees(int collectionID) {
Future<List<User>> getSharees(int collectionID) {
return Dio()
.get(
Configuration.instance.getHttpEndpoint() + "/collections/sharees",
@ -90,27 +90,29 @@ class CollectionsService {
)
.then((response) {
_logger.info(response.toString());
final emails = List<String>();
for (final email in response.data["emails"]) {
emails.add(email);
final sharees = List<User>();
for (final user in response.data["sharees"]) {
sharees.add(User.fromMap(user));
}
return emails;
return sharees;
});
}
Future<void> share(int collectionID, String email, String publicKey) {
final encryptedKey = CryptoUtil.sealSync(
getCollectionKey(collectionID), Sodium.base642bin(publicKey));
return Dio().post(
Configuration.instance.getHttpEndpoint() + "/collections/share",
data: {
"collectionID": collectionID,
"email": email,
"encryptedKey": Sodium.bin2base64(encryptedKey),
},
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
);
return Dio()
.post(
Configuration.instance.getHttpEndpoint() + "/collections/share",
data: {
"collectionID": collectionID,
"email": email,
"encryptedKey": Sodium.bin2base64(encryptedKey),
},
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((value) => sync());
}
Future<void> unshare(int collectionID, String email) {
@ -183,6 +185,7 @@ class CollectionsService {
CollectionType.album,
CollectionAttributes(),
null,
null,
));
return collection;
}
@ -206,6 +209,7 @@ class CollectionsService {
encryptedPath: Sodium.bin2base64(encryptedPath.encryptedData),
pathDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce)),
null,
null,
));
return collection;
}

View file

@ -82,6 +82,7 @@ class FavoritesService {
CollectionType.favorites,
CollectionAttributes(),
null,
null,
));
_cachedFavoritesCollectionID = collection.id;
return collection.id;

View file

@ -11,8 +11,9 @@ import 'gallery_app_bar_widget.dart';
class CollectionPage extends StatefulWidget {
final Collection collection;
final String tagPrefix;
const CollectionPage(this.collection, {Key key}) : super(key: key);
const CollectionPage(this.collection, {this.tagPrefix = "collection", Key key}) : super(key: key);
@override
_CollectionPageState createState() => _CollectionPageState();
@ -34,7 +35,7 @@ class _CollectionPageState extends State<CollectionPage> {
reloadEvent: Bus.instance
.on<CollectionUpdatedEvent>()
.where((event) => event.collectionID == widget.collection.id),
tagPrefix: "collection",
tagPrefix: widget.tagPrefix,
selectedFiles: _selectedFiles,
);
return Scaffold(

View file

@ -19,7 +19,7 @@ import 'package:photos/utils/toast_util.dart';
class SharingDialog extends StatefulWidget {
final Collection collection;
final List<String> sharees;
final List<User> sharees;
SharingDialog(this.collection, this.sharees, {Key key}) : super(key: key);
@ -29,7 +29,7 @@ class SharingDialog extends StatefulWidget {
class _SharingDialogState extends State<SharingDialog> {
bool _showEntryField = false;
List<String> _sharees;
List<User> _sharees;
String _email;
@override
@ -42,8 +42,8 @@ class _SharingDialogState extends State<SharingDialog> {
Collection.typeToString(widget.collection.type) +
"."));
} else {
for (final email in _sharees) {
children.add(EmailItemWidget(widget.collection.id, email));
for (final user in _sharees) {
children.add(EmailItemWidget(widget.collection.id, user.email));
}
}
if (_showEntryField) {
@ -186,7 +186,7 @@ class _SharingDialogState extends State<SharingDialog> {
await dialog.hide();
showToast("Shared successfully!");
setState(() {
_sharees.add(email);
_sharees.add(User(email: email));
_showEntryField = false;
});
} catch (e) {

View file

@ -8,7 +8,10 @@ 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_items.dart';
import 'package:photos/ui/collection_page.dart';
import 'package:photos/ui/collections_gallery_widget.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/ui/shared_collection_page.dart';
@ -36,40 +39,42 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
@override
Widget build(BuildContext context) {
return FutureBuilder<List<CollectionWithThumbnail>>(
return FutureBuilder<SharedCollections>(
future:
CollectionsDB.instance.getAllCollections().then((collections) async {
final c = List<CollectionWithThumbnail>();
final outgoing = List<CollectionWithThumbnail>();
final incoming = List<CollectionWithThumbnail>();
for (final collection in collections) {
if (collection.owner.id == Configuration.instance.getUserID()) {
continue;
if (collection.sharees.length > 0) {
final withThumbnail =
await _getCollectionWithThumbnail(collection);
if (withThumbnail.thumbnail != null) {
outgoing.add(withThumbnail);
}
} else {
continue;
}
} else {
final withThumbnail = await _getCollectionWithThumbnail(collection);
if (withThumbnail.thumbnail != null) {
incoming.add(withThumbnail);
}
}
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) {
outgoing.sort((first, second) {
return second.lastUpdatedFile.updationTime
.compareTo(first.lastUpdatedFile.updationTime);
});
return c;
incoming.sort((first, second) {
return second.lastUpdatedFile.updationTime
.compareTo(first.lastUpdatedFile.updationTime);
});
return SharedCollections(outgoing, incoming);
}),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data.isEmpty) {
return nothingToSeeHere;
} else {
return _getSharedCollectionsGallery(snapshot.data);
}
return _getSharedCollectionsGallery(snapshot.data);
} else if (snapshot.hasError) {
_logger.shout(snapshot.error);
return Center(child: Text(snapshot.error.toString()));
@ -80,26 +85,114 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
);
}
Widget _getSharedCollectionsGallery(
List<CollectionWithThumbnail> collections) {
return Container(
margin: EdgeInsets.only(top: 24),
child: GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.only(bottom: 12),
physics: ScrollPhysics(), // to disable GridView's scrolling
itemBuilder: (context, index) {
return _buildCollection(context, collections[index]);
},
itemCount: collections.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
Widget _getSharedCollectionsGallery(SharedCollections collections) {
return SingleChildScrollView(
child: Column(
children: [
SectionTitle("INCOMING"),
Padding(padding: EdgeInsets.all(8)),
collections.incoming.length > 0
? GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _buildIncomingCollection(
context, collections.incoming[index]);
},
itemCount: collections.incoming.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
)
: nothingToSeeHere,
Padding(padding: EdgeInsets.all(8)),
Divider(height: 16),
SectionTitle("OUTGOING"),
Padding(padding: EdgeInsets.all(8)),
collections.outgoing.length > 0
? ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.only(bottom: 12),
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _buildOutgoingCollection(
context, collections.outgoing[index]);
},
itemCount: collections.outgoing.length,
)
: nothingToSeeHere,
],
),
);
}
Widget _buildCollection(BuildContext context, CollectionWithThumbnail c) {
Widget _buildOutgoingCollection(
BuildContext context, CollectionWithThumbnail c) {
return GestureDetector(
child: Container(
margin: EdgeInsets.fromLTRB(16, 4, 8, 12),
child: Row(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(2.0),
child: Container(
child: Hero(
tag: "outgoing_collection" + c.thumbnail.tag(),
child: ThumbnailWidget(
c.thumbnail,
)),
height: 60,
width: 60,
),
),
Padding(padding: EdgeInsets.all(8)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
c.collection.name,
style: TextStyle(
fontSize: 16,
),
),
Padding(
padding: EdgeInsets.fromLTRB(0, 4, 0, 0),
child: Text(
"Shared with " +
c.collection.sharees
.map((u) => u.name.split(" ")[0])
.join(", "),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).primaryColorLight,
),
textAlign: TextAlign.left,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
onTap: () {
final page = CollectionPage(
c.collection,
tagPrefix: "outgoing_collection",
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return page;
},
),
);
},
);
}
Widget _buildIncomingCollection(
BuildContext context, CollectionWithThumbnail c) {
return GestureDetector(
child: Column(
children: <Widget>[
@ -156,6 +249,19 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
);
}
Future<CollectionWithThumbnail> _getCollectionWithThumbnail(
Collection collection) async {
final thumbnail =
await FilesDB.instance.getLatestFileInCollection(collection.id);
final lastUpdatedFile =
await FilesDB.instance.getLastModifiedFileInCollection(collection.id);
return CollectionWithThumbnail(
collection,
thumbnail,
lastUpdatedFile,
);
}
@override
void dispose() {
_subscription.cancel();