diff --git a/lib/events/collection_updated_event.dart b/lib/events/collection_updated_event.dart new file mode 100644 index 000000000..d70cb6cc2 --- /dev/null +++ b/lib/events/collection_updated_event.dart @@ -0,0 +1,7 @@ +import 'package:photos/events/event.dart'; + +class CollectionUpdatedEvent extends Event { + final int collectionID; + + CollectionUpdatedEvent(this.collectionID); +} diff --git a/lib/models/selected_files.dart b/lib/models/selected_files.dart index d16791711..7f391d780 100644 --- a/lib/models/selected_files.dart +++ b/lib/models/selected_files.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/foundation.dart'; import 'package:photos/models/file.dart'; diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index ac438766b..d813a912e 100644 --- a/lib/services/collections_service.dart +++ b/lib/services/collections_service.dart @@ -7,7 +7,9 @@ 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/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'; @@ -178,6 +180,22 @@ class CollectionsService { return _collectionIDToOwnedCollections[collectionID]; } + Future 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 getOrCreateForPath(String path) async { if (_localCollections.containsKey(path)) { return _localCollections[path]; @@ -217,12 +235,15 @@ class CollectionsService { Sodium.bin2base64(encryptedKeyData.nonce), ).toMap()); } - return Dio().post( - Configuration.instance.getHttpEndpoint() + "/collections/add-files", - data: params, - options: - Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}), - ); + return Dio() + .post( + Configuration.instance.getHttpEndpoint() + "/collections/add-files", + data: params, + options: Options( + headers: {"X-Auth-Token": Configuration.instance.getToken()}), + ) + .then( + (value) => Bus.instance.fire(CollectionUpdatedEvent(collectionID))); } Future removeFromCollection(int collectionID, List files) { @@ -234,12 +255,17 @@ class CollectionsService { } params["fileIDs"].add(file.uploadedFileID); } - return Dio().post( - Configuration.instance.getHttpEndpoint() + "/collections/remove-files", - data: params, - options: - Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}), - ); + return Dio() + .post( + Configuration.instance.getHttpEndpoint() + + "/collections/remove-files", + data: params, + options: Options( + headers: {"X-Auth-Token": Configuration.instance.getToken()}), + ) + .then( + (value) => Bus.instance.fire(CollectionUpdatedEvent(collectionID))); + ; } Future createAndCacheCollection(Collection collection) async { diff --git a/lib/services/favorites_service.dart b/lib/services/favorites_service.dart index 2074b06fa..d7216cb8d 100644 --- a/lib/services/favorites_service.dart +++ b/lib/services/favorites_service.dart @@ -1,8 +1,6 @@ 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'; @@ -55,7 +53,6 @@ class FavoritesService { } else { await _collectionsService.addToCollection(collectionID, [file]); _cachedFavoriteFiles.add(file); - Bus.instance.fire(LocalPhotosUpdatedEvent()); } } @@ -67,7 +64,6 @@ class FavoritesService { } else { await _collectionsService.removeFromCollection(collectionID, [file]); _cachedFavoriteFiles.remove(file); - Bus.instance.fire(LocalPhotosUpdatedEvent()); } } diff --git a/lib/ui/collections_gallery_widget.dart b/lib/ui/collections_gallery_widget.dart index c98412c02..4f2fe085d 100644 --- a/lib/ui/collections_gallery_widget.dart +++ b/lib/ui/collections_gallery_widget.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:fluttertoast/fluttertoast.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/events/tab_changed_event.dart'; import 'package:photos/models/collection.dart'; @@ -29,11 +29,17 @@ class CollectionsGalleryWidget extends StatefulWidget { } class _CollectionsGalleryWidgetState extends State { - StreamSubscription _subscription; + StreamSubscription _localFilesSubscription; + StreamSubscription _collectionUpdatesSubscription; @override void initState() { - _subscription = Bus.instance.on().listen((event) { + _localFilesSubscription = + Bus.instance.on().listen((event) { + setState(() {}); + }); + _collectionUpdatesSubscription = + Bus.instance.on().listen((event) { setState(() {}); }); super.initState(); @@ -232,7 +238,8 @@ class _CollectionsGalleryWidgetState extends State { @override void dispose() { - _subscription.cancel(); + _localFilesSubscription.cancel(); + _collectionUpdatesSubscription.cancel(); super.dispose(); } } diff --git a/lib/ui/create_collection_page.dart b/lib/ui/create_collection_page.dart new file mode 100644 index 000000000..58bf7eb4a --- /dev/null +++ b/lib/ui/create_collection_page.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/db/files_db.dart'; +import 'package:photos/models/collection.dart'; +import 'package:photos/models/file.dart'; +import 'package:photos/services/collections_service.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 List files; + const CreateCollectionPage(this.files, {Key key}) : super(key: key); + + @override + _CreateCollectionPageState createState() => _CreateCollectionPageState(); +} + +class _CreateCollectionPageState extends State { + 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 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(); + }, + ), + ), + ), + ], + ); + } + + 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 { + final collection = await _createAlbum(_albumName); + if (collection != null) { + await _addToCollection(collection.id); + } + }, + ), + ], + ); + + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); + } + + Future _addToCollection(int collectionID) async { + final dialog = createProgressDialog(context, "Uploading files to album..."); + await dialog.show(); + final files = List(); + for (final file in widget.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); + Navigator.pop(context); + Navigator.pop(context); + showToast("Album '" + _albumName + "' created."); + } catch (e, s) { + _logger.severe(e, s); + await dialog.hide(); + showGenericErrorDialog(context); + } finally { + await dialog.hide(); + } + } + + Future _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; + } +} diff --git a/lib/ui/gallery_app_bar_widget.dart b/lib/ui/gallery_app_bar_widget.dart index cfb95ca07..522442bde 100644 --- a/lib/ui/gallery_app_bar_widget.dart +++ b/lib/ui/gallery_app_bar_widget.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.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/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'; @@ -153,11 +155,22 @@ class _GalleryAppBarWidgetState extends State { ); } + Future _createAlbum() async { + Navigator.push( + context, + PageTransition( + type: PageTransitionType.bottomToTop, + child: CreateCollectionPage( + widget.selectedFiles.files.toList(), + ))); + } + List _getActions(BuildContext context) { List actions = List(); if (widget.selectedFiles.files.isNotEmpty) { - if (widget.type != GalleryAppBarType.shared_collection && - widget.type != GalleryAppBarType.search_results) { + if (widget.type == GalleryAppBarType.homepage || + widget.type == GalleryAppBarType.local_folder || + widget.type == GalleryAppBarType.collection) { actions.add(IconButton( icon: Icon(Icons.delete), onPressed: () { @@ -165,6 +178,12 @@ class _GalleryAppBarWidgetState extends State { }, )); } + actions.add(IconButton( + icon: Icon(Icons.add), + onPressed: () { + _createAlbum(); + }, + )); actions.add(IconButton( icon: Icon(Icons.share), onPressed: () { diff --git a/pubspec.lock b/pubspec.lock index 60687ed0c..d992cd059 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -415,6 +415,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: diff --git a/pubspec.yaml b/pubspec.yaml index 74fdde7f9..0362f9f8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,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: