Просмотр исходного кода

Update shared collections interface

Vishnu Mohandas 4 лет назад
Родитель
Сommit
96dba3f905

+ 11 - 14
lib/db/collections_db.dart

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

+ 53 - 18
lib/models/collection.dart

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

+ 7 - 0
lib/models/collection_items.dart

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

+ 19 - 15
lib/services/collections_service.dart

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

+ 1 - 0
lib/services/favorites_service.dart

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

+ 3 - 2
lib/ui/collection_page.dart

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

+ 5 - 5
lib/ui/share_collection_widget.dart

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

+ 143 - 37
lib/ui/shared_collections_gallery.dart

@@ -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;
-          }
-          final thumbnail =
-              await FilesDB.instance.getLatestFileInCollection(collection.id);
-          if (thumbnail == null) {
-            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 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);
+        });
+        incoming.sort((first, second) {
           return second.lastUpdatedFile.updationTime
               .compareTo(first.lastUpdatedFile.updationTime);
         });
-        return c;
+        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 _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 _buildCollection(BuildContext context, CollectionWithThumbnail c) {
+  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();