diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index cb0a82f96..daf1f7a90 100644 --- a/lib/services/collections_service.dart +++ b/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 move( + int toCollectionID, int fromCollectionID, List files) { + _validateMoveRequest(toCollectionID, fromCollectionID, files); + final params = {}; + 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 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 removeFromCollection(int collectionID, List files) async { final params = {}; params["collectionID"] = collectionID; diff --git a/lib/ui/create_collection_page.dart b/lib/ui/create_collection_page.dart index e87528136..d432fde72 100644 --- a/lib/ui/create_collection_page.dart +++ b/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 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 { 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 { ], ), 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 { 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 { ))); } + Future _addOrMoveToCollection(int collectionID) async { + return widget.actionType == CollectionActionType.addFiles + ? _addToCollection(collectionID) + : _moveFilesToCollection(collectionID); + } + + Future _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 _addToCollection(int collectionID) async { final dialog = createProgressDialog(context, "uploading files to album..."); await dialog.show(); diff --git a/lib/ui/gallery_app_bar_widget.dart b/lib/ui/gallery_app_bar_widget.dart index 68a3639bd..fdcac574f 100644 --- a/lib/ui/gallery_app_bar_widget.dart +++ b/lib/ui/gallery_app_bar_widget.dart @@ -164,6 +164,18 @@ class _GalleryAppBarWidgetState extends State { ))); } + Future _moveFiles() async { + Navigator.push( + context, + PageTransition( + type: PageTransitionType.bottomToTop, + child: CreateCollectionPage( + widget.selectedFiles, + null, + actionType: CollectionActionType.moveFiles, + ))); + } + List _getActions(BuildContext context) { List actions = []; // skip add button for incoming collection till this feature is implemented @@ -177,6 +189,17 @@ class _GalleryAppBarWidgetState extends State { }, )); } + 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), diff --git a/lib/utils/dialog_util.dart b/lib/utils/dialog_util.dart index b7b42b711..7d5682836 100644 --- a/lib/utils/dialog_util.dart +++ b/lib/utils/dialog_util.dart @@ -25,13 +25,19 @@ ProgressDialog createProgressDialog(BuildContext context, String message) { return dialog; } -Future showErrorDialog(BuildContext context, String title, String content) { +Future 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'); },