Browse Source

Merge pull request #41 from ente-io/move_files

Support for moving files between albums
Neeraj Gupta 3 years ago
parent
commit
c416ca1e94

+ 56 - 0
lib/services/collections_service.dart

@@ -12,6 +12,7 @@ import 'package:photos/core/network.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/events/files_updated_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_file_item.dart';
@@ -339,6 +340,61 @@ class CollectionsService {
     });
   }
 
+  Future<void> move(
+      int toCollectionID, int fromCollectionID, List<File> files) {
+    _validateMoveRequest(toCollectionID, fromCollectionID, files);
+    final params = <String, dynamic>{};
+    params["toCollectionID"] = toCollectionID;
+    params["fromCollectionID"] = fromCollectionID;
+    params["files"] = [];
+    for (final file in files) {
+      final fileKey = decryptFileKey(file);
+      file.generatedID = null; // So that a new entry is created in the FilesDB
+      file.collectionID = toCollectionID;
+      final encryptedKeyData =
+          CryptoUtil.encryptSync(fileKey, getCollectionKey(toCollectionID));
+      file.encryptedKey = Sodium.bin2base64(encryptedKeyData.encryptedData);
+      file.keyDecryptionNonce = Sodium.bin2base64(encryptedKeyData.nonce);
+      params["files"].add(CollectionFileItem(
+              file.uploadedFileID, file.encryptedKey, file.keyDecryptionNonce)
+          .toMap());
+    }
+    return _dio
+        .post(
+      Configuration.instance.getHttpEndpoint() + "/collections/move-files",
+      data: params,
+      options:
+          Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
+    )
+        .then((value) async {
+      // insert files to new collection
+      await _filesDB.insertMultiple(files);
+      Bus.instance.fire(CollectionUpdatedEvent(toCollectionID, files));
+      //  todo: remove files from existing collection locally.
+      //  Ideally, remoteSync should take care of it.
+      Bus.instance.fire(CollectionUpdatedEvent(fromCollectionID, files,
+          type: EventType.deleted));
+    });
+  }
+
+  void _validateMoveRequest(
+      int toCollectionID, int fromCollectionID, List<File> files) {
+    if (toCollectionID == fromCollectionID) {
+      throw AssertionError("can't move to same album");
+    }
+    for (final file in files) {
+      if (file.uploadedFileID == null) {
+        throw AssertionError("can only move uploaded memories");
+      }
+      if (file.collectionID != fromCollectionID) {
+        throw AssertionError("all memories should belong to the same album");
+      }
+      if (file.ownerID != Configuration.instance.getUserID()) {
+        throw AssertionError("can only move memories uploaded by you");
+      }
+    }
+  }
+
   Future<void> removeFromCollection(int collectionID, List<File> files) async {
     final params = <String, dynamic>{};
     params["collectionID"] = collectionID;

+ 44 - 5
lib/ui/create_collection_page.dart

@@ -19,10 +19,15 @@ import 'package:photos/utils/share_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 
+enum CollectionActionType { addFiles, moveFiles }
+
 class CreateCollectionPage extends StatefulWidget {
   final SelectedFiles selectedFiles;
   final List<SharedMediaFile> sharedFiles;
-  const CreateCollectionPage(this.selectedFiles, this.sharedFiles, {Key key})
+  final CollectionActionType actionType;
+
+  const CreateCollectionPage(this.selectedFiles, this.sharedFiles,
+      {Key key, this.actionType = CollectionActionType.addFiles})
       : super(key: key);
 
   @override
@@ -37,7 +42,9 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(
-        title: Text("add files"),
+        title: Text(widget.actionType == CollectionActionType.addFiles
+            ? "add files"
+            : "move files"),
       ),
       body: _getBody(context),
     );
@@ -140,8 +147,10 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
           ],
         ),
         onTap: () async {
-          if (await _addToCollection(item.collection.id)) {
-            showToast("added successfully to '" + item.collection.name);
+          if (await _addOrMoveToCollection(item.collection.id)) {
+            showToast(widget.actionType == CollectionActionType.addFiles
+                ? "added successfully to " + item.collection.name
+                : "moved successfully to " + item.collection.name);
             _navigateToCollection(item.collection);
           }
         },
@@ -191,7 +200,7 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
             Navigator.of(context, rootNavigator: true).pop('dialog');
             final collection = await _createAlbum(_albumName);
             if (collection != null) {
-              if (await _addToCollection(collection.id)) {
+              if (await _addOrMoveToCollection(collection.id)) {
                 showToast("album '" + _albumName + "' created.");
                 _navigateToCollection(collection);
               }
@@ -220,6 +229,36 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
             )));
   }
 
+  Future<bool> _addOrMoveToCollection(int collectionID) async {
+    return widget.actionType == CollectionActionType.addFiles
+        ? _addToCollection(collectionID)
+        : _moveFilesToCollection(collectionID);
+  }
+
+  Future<bool> _moveFilesToCollection(int toCollectionID) async {
+    final dialog = createProgressDialog(context, "moving files to album...");
+    await dialog.show();
+    try {
+      int fromCollectionID = widget.selectedFiles.files?.first?.collectionID;
+      await CollectionsService.instance.move(
+          toCollectionID,
+          fromCollectionID,
+          widget.selectedFiles.files?.toList());
+      RemoteSyncService.instance.sync(silently: true);
+      widget.selectedFiles?.clearAll();
+      await dialog.hide();
+      return true;
+    } on AssertionError catch (e, s) {
+      await dialog.hide();
+      showErrorDialog(context, "oops", e.message);
+      return false;
+    } catch (e, s) {
+      _logger.severe("Could not move to album", e, s);
+      showGenericErrorDialog(context);
+      return false;
+    }
+  }
+
   Future<bool> _addToCollection(int collectionID) async {
     final dialog = createProgressDialog(context, "uploading files to album...");
     await dialog.show();

+ 23 - 0
lib/ui/gallery_app_bar_widget.dart

@@ -164,6 +164,18 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
             )));
   }
 
+  Future<void> _moveFiles() async {
+    Navigator.push(
+        context,
+        PageTransition(
+            type: PageTransitionType.bottomToTop,
+            child: CreateCollectionPage(
+              widget.selectedFiles,
+              null,
+              actionType: CollectionActionType.moveFiles,
+            )));
+  }
+
   List<Widget> _getActions(BuildContext context) {
     List<Widget> actions = <Widget>[];
     // skip add button for incoming collection till this feature is implemented
@@ -177,6 +189,17 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
         },
       ));
     }
+    if (Configuration.instance.hasConfiguredAccount() &&
+        widget.type == GalleryAppBarType.owned_collection) {
+      actions.add(IconButton(
+        icon: Icon(Platform.isAndroid
+            ? Icons.arrow_right_alt_rounded
+            : CupertinoIcons.arrow_right),
+        onPressed: () {
+          _moveFiles();
+        },
+      ));
+    }
     actions.add(IconButton(
       icon: Icon(
           Platform.isAndroid ? Icons.share_outlined : CupertinoIcons.share),

+ 8 - 2
lib/utils/dialog_util.dart

@@ -25,13 +25,19 @@ ProgressDialog createProgressDialog(BuildContext context, String message) {
   return dialog;
 }
 
-Future<dynamic> showErrorDialog(BuildContext context, String title, String content) {
+Future<dynamic> showErrorDialog(
+    BuildContext context, String title, String content) {
   AlertDialog alert = AlertDialog(
     title: Text(title),
     content: Text(content),
     actions: [
       TextButton(
-        child: Text("ok"),
+        child: Text(
+          "ok",
+          style: TextStyle(
+            color: Colors.white,
+          ),
+        ),
         onPressed: () {
           Navigator.of(context, rootNavigator: true).pop('dialog');
         },