diff --git a/lib/models/collection.dart b/lib/models/collection.dart index 3c2ef34a1..e1b6cf1f1 100644 --- a/lib/models/collection.dart +++ b/lib/models/collection.dart @@ -81,6 +81,8 @@ class Collection { return (owner?.id ?? 0) == userID; } + String get collectionName => name ?? "Unnamed collection"; + void updateSharees(List newSharees) { sharees?.clear(); sharees?.addAll(newSharees); diff --git a/lib/ui/components/album_list_item_widget.dart b/lib/ui/components/album_list_item_widget.dart new file mode 100644 index 000000000..da8352692 --- /dev/null +++ b/lib/ui/components/album_list_item_widget.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:photos/db/files_db.dart'; +import 'package:photos/models/collection_items.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart'; +import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; + +///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=7480%3A33462&t=H5AvR79OYDnB9ekw-4 +class AlbumListItemWidget extends StatelessWidget { + final CollectionWithThumbnail item; + const AlbumListItemWidget( + this.item, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final textTheme = getEnteTextTheme(context); + final colorScheme = getEnteColorScheme(context); + const sideOfThumbnail = 60.0; + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + alignment: Alignment.center, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(4), + ), + child: SizedBox( + height: sideOfThumbnail, + width: sideOfThumbnail, + child: item.thumbnail != null + ? ThumbnailWidget( + item.thumbnail, + showFavForAlbumOnly: true, + ) + : const NoThumbnailWidget( + addBorder: false, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.collection.collectionName), + FutureBuilder( + future: FilesDB.instance.collectionFileCount( + item.collection.id, + ), + builder: (context, snapshot) { + if (snapshot.hasData) { + final text = + snapshot.data == 1 ? " memory" : " memories"; + return Text( + snapshot.data.toString() + text, + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } else { + if (snapshot.hasError) { + Logger("AlbumListItemWidget").severe( + "Failed to fetch file count of collection", + snapshot.error, + ); + } + return Text( + "", + style: textTheme.small.copyWith( + color: colorScheme.textMuted, + ), + ); + } + }, + ) + ], + ), + ), + ], + ), + IgnorePointer( + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + border: Border.all( + color: colorScheme.strokeFainter, + ), + ), + height: sideOfThumbnail, + width: constraints.maxWidth, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/components/bottom_of_title_bar_widget.dart b/lib/ui/components/bottom_of_title_bar_widget.dart new file mode 100644 index 000000000..762a3dc65 --- /dev/null +++ b/lib/ui/components/bottom_of_title_bar_widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/components/title_bar_title_widget.dart'; + +///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=7309%3A29088&t=ReeZ2Big8xSsemZb-4 +class BottomOfTitleBarWidget extends StatelessWidget { + final TitleBarTitleWidget? title; + final String? caption; + const BottomOfTitleBarWidget({this.title, this.caption, super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title ?? const SizedBox.shrink(), + caption != null + ? Text( + caption!, + style: getEnteTextTheme(context).small.copyWith( + color: getEnteColorScheme(context).textMuted, + ), + ) + : const SizedBox.shrink(), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui/components/new_album_list_widget.dart b/lib/ui/components/new_album_list_widget.dart new file mode 100644 index 000000000..c089c8cbc --- /dev/null +++ b/lib/ui/components/new_album_list_widget.dart @@ -0,0 +1,73 @@ +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:photos/theme/ente_theme.dart'; + +///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=10854%3A57947&t=H5AvR79OYDnB9ekw-4 +class NewAlbumListItemWidget extends StatelessWidget { + const NewAlbumListItemWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final textTheme = getEnteTextTheme(context); + final colorScheme = getEnteColorScheme(context); + const sideOfThumbnail = 60.0; + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + alignment: Alignment.center, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(4), + ), + child: SizedBox( + height: sideOfThumbnail, + width: sideOfThumbnail, + child: Icon( + Icons.add_outlined, + color: colorScheme.strokeMuted, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 12), + child: Text( + "New album", + style: + textTheme.body.copyWith(color: colorScheme.textMuted), + ), + ), + ], + ), + IgnorePointer( + child: DottedBorder( + dashPattern: const [4], + color: colorScheme.strokeFainter, + strokeWidth: 1, + padding: const EdgeInsets.all(0), + borderType: BorderType.RRect, + radius: const Radius.circular(4), + child: SizedBox( + //Have to decrease the height and width by 1 pt as the stroke + //dotted border gives is of strokeAlign.center, so 0.5 inside and + // outside. Here for the row, stroke should be inside so we + //decrease the size of this sizedBox by 1 (so it shrinks 0.5 from + //every side) so that the strokeAlign.center of this sizedBox + //looks like a strokeAlign.inside in the row. + height: sideOfThumbnail - 1, + //This width will work for this only if the row widget takes up the + //full size it's parent (stack). + width: constraints.maxWidth - 1, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/create_collection_page.dart b/lib/ui/create_collection_page.dart deleted file mode 100644 index b0f25a2cd..000000000 --- a/lib/ui/create_collection_page.dart +++ /dev/null @@ -1,416 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:photos/db/files_db.dart'; -import 'package:photos/ente_theme_data.dart'; -import 'package:photos/models/collection.dart'; -import 'package:photos/models/collection_items.dart'; -import 'package:photos/models/file.dart'; -import 'package:photos/models/selected_files.dart'; -import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/ignored_files_service.dart'; -import 'package:photos/services/remote_sync_service.dart'; -import 'package:photos/ui/common/gradient_button.dart'; -import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart'; -import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; -import 'package:photos/ui/viewer/gallery/collection_page.dart'; -import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/navigation_util.dart'; -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, restoreFiles, unHide } - -String _actionName(CollectionActionType type, bool plural) { - final titleSuffix = (plural ? "s" : ""); - String text = ""; - switch (type) { - case CollectionActionType.addFiles: - text = "Add file"; - break; - case CollectionActionType.moveFiles: - text = "Move file"; - break; - case CollectionActionType.restoreFiles: - text = "Restore file"; - break; - case CollectionActionType.unHide: - text = "Unhide file"; - break; - } - return text + titleSuffix; -} - -class CreateCollectionPage extends StatefulWidget { - final SelectedFiles? selectedFiles; - final List? sharedFiles; - final CollectionActionType actionType; - - const CreateCollectionPage( - this.selectedFiles, - this.sharedFiles, { - Key? key, - this.actionType = CollectionActionType.addFiles, - }) : super(key: key); - - @override - State createState() => _CreateCollectionPageState(); -} - -class _CreateCollectionPageState extends State { - final _logger = Logger((_CreateCollectionPageState).toString()); - late String _albumName; - - @override - Widget build(BuildContext context) { - final filesCount = widget.sharedFiles != null - ? widget.sharedFiles!.length - : widget.selectedFiles!.files.length; - return Scaffold( - appBar: AppBar( - title: Text(_actionName(widget.actionType, filesCount > 1)), - ), - body: _getBody(context), - ); - } - - Widget _getBody(BuildContext context) { - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only( - top: 30, - bottom: 12, - left: 40, - right: 40, - ), - child: GradientButton( - onTap: () async { - _showNameAlbumDialog(); - }, - iconData: Icons.create_new_folder_outlined, - text: "To a new album", - ), - ), - ), - ], - ), - const Padding( - padding: EdgeInsets.fromLTRB(40, 24, 40, 20), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - "To an existing album", - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 4, 20, 0), - child: _getExistingCollectionsWidget(), - ), - ], - ), - ); - } - - Widget _getExistingCollectionsWidget() { - return FutureBuilder>( - future: _getCollectionsWithThumbnail(), - builder: (context, snapshot) { - if (snapshot.hasError) { - return Text(snapshot.error.toString()); - } else if (snapshot.hasData) { - return ListView.builder( - itemBuilder: (context, index) { - return _buildCollectionItem(snapshot.data![index]); - }, - itemCount: snapshot.data!.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - ); - } else { - return const EnteLoadingWidget(); - } - }, - ); - } - - Widget _buildCollectionItem(CollectionWithThumbnail item) { - return Container( - padding: const EdgeInsets.only(left: 24, bottom: 16), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(2.0), - child: SizedBox( - height: 64, - width: 64, - key: Key("collection_item:" + (item.thumbnail?.tag ?? "")), - child: item.thumbnail != null - ? ThumbnailWidget( - item.thumbnail, - showFavForAlbumOnly: true, - ) - : const NoThumbnailWidget(), - ), - ), - const Padding(padding: EdgeInsets.all(8)), - Expanded( - child: Text( - item.collection.name!, - style: const TextStyle( - fontSize: 16, - ), - ), - ), - ], - ), - onTap: () async { - if (await _runCollectionAction(item.collection.id)) { - showShortToast( - context, - widget.actionType == CollectionActionType.addFiles - ? "Added successfully to " + item.collection.name! - : "Moved successfully to " + item.collection.name!, - ); - _navigateToCollection(item.collection); - } - }, - ), - ); - } - - Future> _getCollectionsWithThumbnail() async { - final List collectionsWithThumbnail = - await CollectionsService.instance.getCollectionsWithThumbnails( - // in collections where user is a collaborator, only addTo and remove - // action can to be performed - includeCollabCollections: - widget.actionType == CollectionActionType.addFiles, - ); - collectionsWithThumbnail.removeWhere( - (element) => (element.collection.type == CollectionType.favorites || - element.collection.type == CollectionType.uncategorized || - element.collection.isSharedFilesCollection()), - ); - collectionsWithThumbnail.sort((first, second) { - return compareAsciiLowerCaseNatural( - first.collection.name ?? "", - second.collection.name ?? "", - ); - }); - return collectionsWithThumbnail; - } - - void _showNameAlbumDialog() async { - final AlertDialog alert = AlertDialog( - title: const Text("Album title"), - content: TextFormField( - decoration: const InputDecoration( - hintText: "Christmas 2020 / Dinner at Alice's", - contentPadding: EdgeInsets.all(8), - ), - onChanged: (value) { - setState(() { - _albumName = value; - }); - }, - autofocus: true, - keyboardType: TextInputType.text, - textCapitalization: TextCapitalization.words, - ), - actions: [ - TextButton( - child: Text( - "Ok", - style: TextStyle( - color: Theme.of(context).colorScheme.greenAlternative, - ), - ), - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop('dialog'); - final collection = await _createAlbum(_albumName); - if (collection != null) { - if (await _runCollectionAction(collection.id)) { - if (widget.actionType == CollectionActionType.restoreFiles) { - showShortToast( - context, - 'Restored files to album ' + _albumName, - ); - } else { - showShortToast( - context, - "Album '" + _albumName + "' created.", - ); - } - _navigateToCollection(collection); - } - } - }, - ), - ], - ); - - showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - ); - } - - void _navigateToCollection(Collection collection) { - Navigator.pop(context); - routeToPage( - context, - CollectionPage( - CollectionWithThumbnail(collection, null), - ), - ); - } - - Future _runCollectionAction(int collectionID) async { - switch (widget.actionType) { - case CollectionActionType.addFiles: - return _addToCollection(collectionID); - case CollectionActionType.moveFiles: - return _moveFilesToCollection(collectionID); - case CollectionActionType.unHide: - return _moveFilesToCollection(collectionID); - case CollectionActionType.restoreFiles: - return _restoreFilesToCollection(collectionID); - } - } - - Future _moveFilesToCollection(int toCollectionID) async { - final String message = widget.actionType == CollectionActionType.moveFiles - ? "Moving files to album..." - : "Unhiding files to album"; - final dialog = createProgressDialog(context, message); - await dialog.show(); - try { - final int fromCollectionID = - widget.selectedFiles!.files.first.collectionID!; - await CollectionsService.instance.move( - toCollectionID, - fromCollectionID, - widget.selectedFiles!.files.toList(), - ); - await dialog.hide(); - RemoteSyncService.instance.sync(silently: true); - widget.selectedFiles?.clearAll(); - - return true; - } on AssertionError catch (e) { - await dialog.hide(); - showErrorDialog(context, "Oops", e.message as String?); - return false; - } catch (e, s) { - _logger.severe("Could not move to album", e, s); - await dialog.hide(); - showGenericErrorDialog(context: context); - return false; - } - } - - Future _restoreFilesToCollection(int toCollectionID) async { - final dialog = createProgressDialog(context, "Restoring files..."); - await dialog.show(); - try { - await CollectionsService.instance - .restore(toCollectionID, widget.selectedFiles!.files.toList()); - RemoteSyncService.instance.sync(silently: true); - widget.selectedFiles?.clearAll(); - await dialog.hide(); - return true; - } on AssertionError catch (e) { - await dialog.hide(); - showErrorDialog(context, "Oops", e.message as String?); - return false; - } catch (e, s) { - _logger.severe("Could not move to album", e, s); - await dialog.hide(); - showGenericErrorDialog(context: context); - return false; - } - } - - Future _addToCollection(int collectionID) async { - final dialog = createProgressDialog(context, "Uploading files to album..."); - await dialog.show(); - try { - final List files = []; - final List filesPendingUpload = []; - if (widget.sharedFiles != null) { - filesPendingUpload.addAll( - await convertIncomingSharedMediaToFile( - widget.sharedFiles!, - collectionID, - ), - ); - } else { - for (final file in widget.selectedFiles!.files) { - final File? currentFile = - await (FilesDB.instance.getFile(file.generatedID!)); - if (currentFile == null) { - _logger.severe("Failed to find fileBy genID"); - continue; - } - if (currentFile.uploadedFileID == null) { - currentFile.collectionID = collectionID; - filesPendingUpload.add(currentFile); - } else { - files.add(currentFile); - } - } - } - if (filesPendingUpload.isNotEmpty) { - // filesPendingUpload might be getting ignored during auto-upload - // because the user deleted these files from ente in the past. - await IgnoredFilesService.instance - .removeIgnoredMappings(filesPendingUpload); - await FilesDB.instance.insertMultiple(filesPendingUpload); - } - if (files.isNotEmpty) { - await CollectionsService.instance.addToCollection(collectionID, files); - } - RemoteSyncService.instance.sync(silently: true); - await dialog.hide(); - widget.selectedFiles?.clearAll(); - return true; - } catch (e, s) { - _logger.severe("Could not add to album", e, s); - await dialog.hide(); - showGenericErrorDialog(context: context); - } - return false; - } - - Future _createAlbum(String albumName) async { - Collection? 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: context); - } finally { - await dialog.hide(); - } - return collection; - } -} diff --git a/lib/ui/create_collection_sheet.dart b/lib/ui/create_collection_sheet.dart new file mode 100644 index 000000000..32220f3e9 --- /dev/null +++ b/lib/ui/create_collection_sheet.dart @@ -0,0 +1,465 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:photos/db/files_db.dart'; +import 'package:photos/models/collection.dart'; +import 'package:photos/models/collection_items.dart'; +import 'package:photos/models/file.dart'; +import 'package:photos/models/selected_files.dart'; +import 'package:photos/services/collections_service.dart'; +import 'package:photos/services/ignored_files_service.dart'; +import 'package:photos/services/remote_sync_service.dart'; +import 'package:photos/theme/colors.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/common/loading_widget.dart'; +import 'package:photos/ui/components/album_list_item_widget.dart'; +import 'package:photos/ui/components/bottom_of_title_bar_widget.dart'; +import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/models/button_type.dart'; +import 'package:photos/ui/components/new_album_list_widget.dart'; +import 'package:photos/ui/components/title_bar_title_widget.dart'; +import 'package:photos/ui/viewer/gallery/collection_page.dart'; +import 'package:photos/utils/dialog_util.dart'; +import 'package:photos/utils/navigation_util.dart'; +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, restoreFiles, unHide } + +String _actionName(CollectionActionType type, bool plural) { + bool addTitleSuffix = false; + final titleSuffix = (plural ? "s" : ""); + String text = ""; + switch (type) { + case CollectionActionType.addFiles: + text = "Add item"; + addTitleSuffix = true; + break; + case CollectionActionType.moveFiles: + text = "Move item"; + addTitleSuffix = true; + break; + case CollectionActionType.restoreFiles: + text = "Restore to album"; + break; + case CollectionActionType.unHide: + text = "Unhide to album"; + break; + } + return addTitleSuffix ? text + titleSuffix : text; +} + +void createCollectionSheet( + SelectedFiles? selectedFiles, + List? sharedFiles, + BuildContext context, { + CollectionActionType actionType = CollectionActionType.addFiles, + bool showOptionToCreateNewAlbum = true, +}) { + showBarModalBottomSheet( + context: context, + builder: (context) { + return CreateCollectionSheet( + selectedFiles: selectedFiles, + sharedFiles: sharedFiles, + actionType: actionType, + showOptionToCreateNewAlbum: showOptionToCreateNewAlbum, + ); + }, + shape: const RoundedRectangleBorder( + side: BorderSide(width: 0), + borderRadius: BorderRadius.vertical( + top: Radius.circular(5), + ), + ), + topControl: const SizedBox.shrink(), + backgroundColor: getEnteColorScheme(context).backgroundElevated, + barrierColor: backdropFaintDark, + enableDrag: false, + ); +} + +class CreateCollectionSheet extends StatefulWidget { + final SelectedFiles? selectedFiles; + final List? sharedFiles; + final CollectionActionType actionType; + final bool showOptionToCreateNewAlbum; + const CreateCollectionSheet({ + required this.selectedFiles, + required this.sharedFiles, + required this.actionType, + required this.showOptionToCreateNewAlbum, + super.key, + }); + + @override + State createState() => _CreateCollectionSheetState(); +} + +class _CreateCollectionSheetState extends State { + final _logger = Logger((_CreateCollectionSheetState).toString()); + + @override + Widget build(BuildContext context) { + final filesCount = widget.sharedFiles != null + ? widget.sharedFiles!.length + : widget.selectedFiles!.files.length; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: min(428, MediaQuery.of(context).size.width), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 32, 0, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomOfTitleBarWidget( + title: TitleBarTitleWidget( + title: _actionName(widget.actionType, filesCount > 1), + ), + caption: "Create or select album", + ), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 4, 0), + child: Scrollbar( + radius: const Radius.circular(2), + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: FutureBuilder( + future: _getCollectionsWithThumbnail(), + builder: (context, snapshot) { + if (snapshot.hasError) { + //Need to show an error on the UI here + return const SizedBox.shrink(); + } else if (snapshot.hasData) { + final collectionsWithThumbnail = snapshot + .data as List; + return ListView.separated( + itemBuilder: (context, index) { + if (index == 0 && + widget.showOptionToCreateNewAlbum) { + return GestureDetector( + onTap: () { + _showNameAlbumDialog(); + }, + behavior: HitTestBehavior.opaque, + child: + const NewAlbumListItemWidget(), + ); + } + final item = collectionsWithThumbnail[ + index - + (widget.showOptionToCreateNewAlbum + ? 1 + : 0)]; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => + _albumListItemOnTap(item), + child: AlbumListItemWidget( + item, + ), + ); + }, + separatorBuilder: (context, index) => + const SizedBox( + height: 8, + ), + itemCount: + collectionsWithThumbnail.length + + (widget.showOptionToCreateNewAlbum + ? 1 + : 0), + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + ); + } else { + return const EnteLoadingWidget(); + } + }, + ), + ), + ), + ), + ), + SafeArea( + child: Container( + //inner stroke of 1pt + 15 pts of top padding = 16 pts + padding: const EdgeInsets.fromLTRB(16, 15, 16, 8), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: getEnteColorScheme(context).strokeFaint, + ), + ), + ), + child: const ButtonWidget( + buttonType: ButtonType.secondary, + buttonAction: ButtonAction.cancel, + isInAlert: true, + labelText: "Cancel", + ), + ), + ) + ], + ), + ), + ], + ), + ), + ), + ], + ); + } + + void _showNameAlbumDialog() async { + String? albumName; + final AlertDialog alert = AlertDialog( + title: const Text("Album title"), + content: TextFormField( + decoration: const InputDecoration( + hintText: "Christmas 2020 / Dinner at Alice's", + contentPadding: EdgeInsets.all(8), + ), + onChanged: (value) { + albumName = value; + }, + autofocus: true, + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.words, + ), + actions: [ + TextButton( + child: Text( + "Ok", + style: TextStyle( + color: getEnteColorScheme(context).primary500, + ), + ), + onPressed: () async { + if (albumName != null && albumName!.isNotEmpty) { + Navigator.of(context, rootNavigator: true).pop('dialog'); + final collection = await _createAlbum(albumName!); + if (collection != null) { + if (await _runCollectionAction(collection.id)) { + if (widget.actionType == CollectionActionType.restoreFiles) { + showShortToast( + context, + 'Restored files to album ' + albumName!, + ); + } else { + showShortToast( + context, + "Album '" + albumName! + "' created.", + ); + } + _navigateToCollection(collection); + } + } + } + }, + ), + ], + ); + + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); + } + + Future _createAlbum(String albumName) async { + Collection? 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: context); + } finally { + await dialog.hide(); + } + return collection; + } + + Future _albumListItemOnTap(CollectionWithThumbnail item) async { + if (await _runCollectionAction( + item.collection.id, + )) { + showShortToast( + context, + widget.actionType == CollectionActionType.addFiles + ? "Added successfully to " + item.collection.name! + : "Moved successfully to " + item.collection.name!, + ); + _navigateToCollection( + item.collection, + ); + } + } + + Future> _getCollectionsWithThumbnail() async { + final List collectionsWithThumbnail = + await CollectionsService.instance.getCollectionsWithThumbnails( + // in collections where user is a collaborator, only addTo and remove + // action can to be performed + includeCollabCollections: + widget.actionType == CollectionActionType.addFiles, + ); + collectionsWithThumbnail.removeWhere( + (element) => (element.collection.type == CollectionType.favorites || + element.collection.type == CollectionType.uncategorized || + element.collection.isSharedFilesCollection()), + ); + collectionsWithThumbnail.sort((first, second) { + return compareAsciiLowerCaseNatural( + first.collection.name ?? "", + second.collection.name ?? "", + ); + }); + return collectionsWithThumbnail; + } + + void _navigateToCollection(Collection collection) { + Navigator.pop(context); + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail(collection, null), + ), + ); + } + + Future _runCollectionAction(int collectionID) async { + switch (widget.actionType) { + case CollectionActionType.addFiles: + return _addToCollection(collectionID); + case CollectionActionType.moveFiles: + return _moveFilesToCollection(collectionID); + case CollectionActionType.unHide: + return _moveFilesToCollection(collectionID); + case CollectionActionType.restoreFiles: + return _restoreFilesToCollection(collectionID); + } + } + + Future _addToCollection(int collectionID) async { + final dialog = createProgressDialog(context, "Uploading files to album..."); + await dialog.show(); + try { + final List files = []; + final List filesPendingUpload = []; + if (widget.sharedFiles != null) { + filesPendingUpload.addAll( + await convertIncomingSharedMediaToFile( + widget.sharedFiles!, + collectionID, + ), + ); + } else { + for (final file in widget.selectedFiles!.files) { + final File? currentFile = + await (FilesDB.instance.getFile(file.generatedID!)); + if (currentFile == null) { + _logger.severe("Failed to find fileBy genID"); + continue; + } + if (currentFile.uploadedFileID == null) { + currentFile.collectionID = collectionID; + filesPendingUpload.add(currentFile); + } else { + files.add(currentFile); + } + } + } + if (filesPendingUpload.isNotEmpty) { + // filesPendingUpload might be getting ignored during auto-upload + // because the user deleted these files from ente in the past. + await IgnoredFilesService.instance + .removeIgnoredMappings(filesPendingUpload); + await FilesDB.instance.insertMultiple(filesPendingUpload); + } + if (files.isNotEmpty) { + await CollectionsService.instance.addToCollection(collectionID, files); + } + RemoteSyncService.instance.sync(silently: true); + await dialog.hide(); + widget.selectedFiles?.clearAll(); + return true; + } catch (e, s) { + _logger.severe("Could not add to album", e, s); + await dialog.hide(); + showGenericErrorDialog(context: context); + } + return false; + } + + Future _moveFilesToCollection(int toCollectionID) async { + final String message = widget.actionType == CollectionActionType.moveFiles + ? "Moving files to album..." + : "Unhiding files to album"; + final dialog = createProgressDialog(context, message); + await dialog.show(); + try { + final int fromCollectionID = + widget.selectedFiles!.files.first.collectionID!; + await CollectionsService.instance.move( + toCollectionID, + fromCollectionID, + widget.selectedFiles!.files.toList(), + ); + await dialog.hide(); + RemoteSyncService.instance.sync(silently: true); + widget.selectedFiles?.clearAll(); + + return true; + } on AssertionError catch (e) { + await dialog.hide(); + showErrorDialog(context, "Oops", e.message as String?); + return false; + } catch (e, s) { + _logger.severe("Could not move to album", e, s); + await dialog.hide(); + showGenericErrorDialog(context: context); + return false; + } + } + + Future _restoreFilesToCollection(int toCollectionID) async { + final dialog = createProgressDialog(context, "Restoring files..."); + await dialog.show(); + try { + await CollectionsService.instance + .restore(toCollectionID, widget.selectedFiles!.files.toList()); + RemoteSyncService.instance.sync(silently: true); + widget.selectedFiles?.clearAll(); + await dialog.hide(); + return true; + } on AssertionError catch (e) { + await dialog.hide(); + showErrorDialog(context, "Oops", e.message as String?); + return false; + } catch (e, s) { + _logger.severe("Could not move to album", e, s); + await dialog.hide(); + showGenericErrorDialog(context: context); + return false; + } + } +} diff --git a/lib/ui/home_widget.dart b/lib/ui/home_widget.dart index 37b5cf4bb..8551cd04e 100644 --- a/lib/ui/home_widget.dart +++ b/lib/ui/home_widget.dart @@ -28,7 +28,7 @@ import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/collections_gallery_widget.dart'; import 'package:photos/ui/common/bottom_shadow.dart'; -import 'package:photos/ui/create_collection_page.dart'; +import 'package:photos/ui/create_collection_sheet.dart'; import 'package:photos/ui/extents_page_view.dart'; import 'package:photos/ui/home/grant_permissions_widget.dart'; import 'package:photos/ui/home/header_widget.dart'; @@ -72,6 +72,7 @@ class _HomeWidgetState extends State { // ignore: unused_field StreamSubscription? _intentDataStreamSubscription; List? _sharedFiles; + bool _shouldRenderCreateCollectionSheet = false; late StreamSubscription _tabChangedEventSubscription; late StreamSubscription @@ -236,6 +237,7 @@ class _HomeWidgetState extends State { ReceiveSharingIntent.getMediaStream().listen( (List value) { setState(() { + _shouldRenderCreateCollectionSheet = true; _sharedFiles = value; }); }, @@ -317,9 +319,22 @@ class _HomeWidgetState extends State { return const LoadingPhotosWidget(); } - if (_sharedFiles != null && _sharedFiles!.isNotEmpty) { + if (_sharedFiles != null && + _sharedFiles!.isNotEmpty && + _shouldRenderCreateCollectionSheet) { + //The gallery is getting rebuilt for some reason when the keyboard is up. + //So to stop showing multiple CreateCollectionSheets, this flag + //needs to be set to false the first time it is rendered. + _shouldRenderCreateCollectionSheet = false; ReceiveSharingIntent.reset(); - return CreateCollectionPage(null, _sharedFiles); + Future.delayed(const Duration(milliseconds: 10), () { + createCollectionSheet( + null, + _sharedFiles, + context, + actionType: CollectionActionType.addFiles, + ); + }); } final isBottomInsetPresent = MediaQuery.of(context).viewPadding.bottom != 0; diff --git a/lib/ui/viewer/actions/file_selection_actions_widget.dart b/lib/ui/viewer/actions/file_selection_actions_widget.dart index 2be238678..65363a2c9 100644 --- a/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -1,7 +1,6 @@ import 'package:fast_base58/fast_base58.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:page_transition/page_transition.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/models/device_collection.dart'; @@ -20,7 +19,7 @@ import 'package:photos/ui/components/blur_menu_item_widget.dart'; import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart'; import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; -import 'package:photos/ui/create_collection_page.dart'; +import 'package:photos/ui/create_collection_sheet.dart'; import 'package:photos/ui/sharing/manage_links_widget.dart'; import 'package:photos/utils/delete_file_util.dart'; import 'package:photos/utils/magic_util.dart'; @@ -255,7 +254,12 @@ class _FileSelectionActionWidgetState extends State { widget.selectedFiles .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true); } - await _selectionCollectionForAction(CollectionActionType.moveFiles); + createCollectionSheet( + widget.selectedFiles, + null, + context, + actionType: CollectionActionType.moveFiles, + ); } Future _addToAlbum() async { @@ -263,7 +267,11 @@ class _FileSelectionActionWidgetState extends State { widget.selectedFiles .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true); } - await _selectionCollectionForAction(CollectionActionType.addFiles); + createCollectionSheet( + widget.selectedFiles, + null, + context, + ); } Future _onDeleteClick() async { @@ -339,7 +347,12 @@ class _FileSelectionActionWidgetState extends State { widget.selectedFiles .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true); } - await _selectionCollectionForAction(CollectionActionType.unHide); + createCollectionSheet( + widget.selectedFiles, + null, + context, + actionType: CollectionActionType.unHide, + ); } Future _onCreatedSharedLinkClicked() async { @@ -407,20 +420,4 @@ class _FileSelectionActionWidgetState extends State { showShortToast(context, "Link copied to clipboard"); } } - - Future _selectionCollectionForAction( - CollectionActionType type, - ) async { - return Navigator.push( - context, - PageTransition( - type: PageTransitionType.bottomToTop, - child: CreateCollectionPage( - widget.selectedFiles, - null, - actionType: type, - ), - ), - ); - } } diff --git a/lib/ui/viewer/actions/file_selection_overlay_bar.dart b/lib/ui/viewer/actions/file_selection_overlay_bar.dart index 522a583ab..6d20241bb 100644 --- a/lib/ui/viewer/actions/file_selection_overlay_bar.dart +++ b/lib/ui/viewer/actions/file_selection_overlay_bar.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:page_transition/page_transition.dart'; import 'package:photos/models/collection.dart'; import 'package:photos/models/device_collection.dart'; import 'package:photos/models/gallery_type.dart'; @@ -8,7 +7,7 @@ import 'package:photos/models/selected_files.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart'; import 'package:photos/ui/components/icon_button_widget.dart'; -import 'package:photos/ui/create_collection_page.dart'; +import 'package:photos/ui/create_collection_sheet.dart'; import 'package:photos/ui/viewer/actions/file_selection_actions_widget.dart'; import 'package:photos/utils/delete_file_util.dart'; import 'package:photos/utils/magic_util.dart'; @@ -85,9 +84,14 @@ class _FileSelectionOverlayBarState extends State { icon: Icons.visibility_off_outlined, iconButtonType: IconButtonType.primary, iconColor: iconColor, - onTap: () => _selectionCollectionForAction( - CollectionActionType.unHide, - ), + onTap: () { + createCollectionSheet( + widget.selectedFiles, + null, + context, + actionType: CollectionActionType.unHide, + ); + }, ), ); } @@ -143,22 +147,6 @@ class _FileSelectionOverlayBarState extends State { widget.selectedFiles.clearAll(); } - Future _selectionCollectionForAction( - CollectionActionType type, - ) async { - return Navigator.push( - context, - PageTransition( - type: PageTransitionType.bottomToTop, - child: CreateCollectionPage( - widget.selectedFiles, - null, - actionType: type, - ), - ), - ); - } - _selectedFilesListener() { widget.selectedFiles.files.isNotEmpty ? _bottomPosition.value = 0.0 diff --git a/lib/ui/viewer/file/fading_app_bar.dart b/lib/ui/viewer/file/fading_app_bar.dart index e483f706f..318ef3135 100644 --- a/lib/ui/viewer/file/fading_app_bar.dart +++ b/lib/ui/viewer/file/fading_app_bar.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:like_button/like_button.dart'; import 'package:logging/logging.dart'; import 'package:media_extension/media_extension.dart'; -import 'package:page_transition/page_transition.dart'; import 'package:path/path.dart' as file_path; import 'package:photo_manager/photo_manager.dart'; import 'package:photos/core/event_bus.dart'; @@ -26,7 +25,7 @@ import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; -import 'package:photos/ui/create_collection_page.dart'; +import 'package:photos/ui/create_collection_sheet.dart'; import 'package:photos/ui/viewer/file/custom_app_bar.dart'; import 'package:photos/utils/delete_file_util.dart'; import 'package:photos/utils/dialog_util.dart'; @@ -272,16 +271,11 @@ class FadingAppBarState extends State { Future _handleUnHideRequest(BuildContext context) async { final s = SelectedFiles(); s.files.add(widget.file); - Navigator.push( + createCollectionSheet( + s, + null, context, - PageTransition( - type: PageTransitionType.bottomToTop, - child: CreateCollectionPage( - s, - null, - actionType: CollectionActionType.unHide, - ), - ), + actionType: CollectionActionType.unHide, ); } diff --git a/lib/ui/viewer/file/fading_bottom_bar.dart b/lib/ui/viewer/file/fading_bottom_bar.dart index cb35b1ac4..6820edebd 100644 --- a/lib/ui/viewer/file/fading_bottom_bar.dart +++ b/lib/ui/viewer/file/fading_bottom_bar.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -import 'package:page_transition/page_transition.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/models/file.dart'; import 'package:photos/models/file_type.dart'; @@ -13,7 +12,7 @@ import 'package:photos/models/trash_file.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; -import 'package:photos/ui/create_collection_page.dart'; +import 'package:photos/ui/create_collection_sheet.dart'; import 'package:photos/ui/viewer/file/file_info_widget.dart'; import 'package:photos/utils/delete_file_util.dart'; import 'package:photos/utils/magic_util.dart'; @@ -238,16 +237,11 @@ class FadingBottomBarState extends State { onPressed: () { final selectedFiles = SelectedFiles(); selectedFiles.toggleSelection(widget.file); - Navigator.push( + createCollectionSheet( + selectedFiles, + null, context, - PageTransition( - type: PageTransitionType.bottomToTop, - child: CreateCollectionPage( - selectedFiles, - null, - actionType: CollectionActionType.restoreFiles, - ), - ), + actionType: CollectionActionType.restoreFiles, ); }, ), diff --git a/lib/ui/viewer/file/no_thumbnail_widget.dart b/lib/ui/viewer/file/no_thumbnail_widget.dart index c433d92d8..c6c76e472 100644 --- a/lib/ui/viewer/file/no_thumbnail_widget.dart +++ b/lib/ui/viewer/file/no_thumbnail_widget.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:photos/theme/ente_theme.dart'; class NoThumbnailWidget extends StatelessWidget { - const NoThumbnailWidget({Key? key}) : super(key: key); + final bool addBorder; + const NoThumbnailWidget({this.addBorder = true, Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -10,10 +11,12 @@ class NoThumbnailWidget extends StatelessWidget { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(1), - border: Border.all( - color: enteColorScheme.strokeFaint, - width: 1, - ), + border: addBorder + ? Border.all( + color: enteColorScheme.strokeFaint, + width: 1, + ) + : null, color: enteColorScheme.fillFaint, ), child: Center( diff --git a/lib/ui/viewer/gallery/gallery_overlay_widget.dart b/lib/ui/viewer/gallery/gallery_overlay_widget.dart index 78dcdacf6..a98ea0d0b 100644 --- a/lib/ui/viewer/gallery/gallery_overlay_widget.dart +++ b/lib/ui/viewer/gallery/gallery_overlay_widget.dart @@ -5,7 +5,6 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:page_transition/page_transition.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/ente_theme_data.dart'; @@ -16,7 +15,7 @@ import 'package:photos/models/magic_metadata.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/hidden_service.dart'; -import 'package:photos/ui/create_collection_page.dart'; +import 'package:photos/ui/create_collection_sheet.dart'; import 'package:photos/utils/delete_file_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/magic_util.dart'; @@ -232,36 +231,6 @@ class _OverlayWidgetState extends State { widget.selectedFiles.clearAll(); } - Future _createCollectionAction(CollectionActionType type) async { - Navigator.push( - context, - PageTransition( - type: PageTransitionType.bottomToTop, - child: CreateCollectionPage( - widget.selectedFiles, - null, - actionType: type, - ), - ), - ); - } - - Future _moveFiles() async { - unawaited( - Navigator.push( - context, - PageTransition( - type: PageTransitionType.bottomToTop, - child: CreateCollectionPage( - widget.selectedFiles, - null, - actionType: CollectionActionType.moveFiles, - ), - ), - ), - ); - } - List _getActions(BuildContext context) { final List actions = []; if (widget.type == GalleryType.trash) { @@ -291,21 +260,6 @@ class _OverlayWidgetState extends State { ); } - if (Configuration.instance.hasConfiguredAccount() && - widget.type == GalleryType.hidden) { - actions.add( - Tooltip( - message: "Unhide", - child: IconButton( - color: Theme.of(context).colorScheme.iconColor, - icon: const Icon(Icons.visibility), - onPressed: () { - _createCollectionAction(CollectionActionType.unHide); - }, - ), - ), - ); - } if (Configuration.instance.hasConfiguredAccount() && widget.type == GalleryType.ownedCollection && widget.collection!.type != CollectionType.favorites) { @@ -437,12 +391,6 @@ class _OverlayWidgetState extends State { case 'hide': await _handleHideRequest(context); break; - case 'add': - await _createCollectionAction(CollectionActionType.addFiles); - break; - case 'move': - await _moveFiles(); - break; case 'archive': await _handleVisibilityChangeRequest(context, visibilityArchive); break; @@ -464,16 +412,11 @@ class _OverlayWidgetState extends State { Icons.restore, ), onPressed: () { - Navigator.push( + createCollectionSheet( + widget.selectedFiles, + null, context, - PageTransition( - type: PageTransitionType.bottomToTop, - child: CreateCollectionPage( - widget.selectedFiles, - null, - actionType: CollectionActionType.restoreFiles, - ), - ), + actionType: CollectionActionType.restoreFiles, ); }, ),