diff --git a/app/assets/2.0x/ic_add_collections_outlined_24dp.png b/app/assets/2.0x/ic_add_collections_outlined_24dp.png new file mode 100644 index 00000000..d50cc231 Binary files /dev/null and b/app/assets/2.0x/ic_add_collections_outlined_24dp.png differ diff --git a/app/assets/3.0x/ic_add_collections_outlined_24dp.png b/app/assets/3.0x/ic_add_collections_outlined_24dp.png new file mode 100644 index 00000000..b464792a Binary files /dev/null and b/app/assets/3.0x/ic_add_collections_outlined_24dp.png differ diff --git a/app/assets/ic_add_collections_outlined_24dp.png b/app/assets/ic_add_collections_outlined_24dp.png new file mode 100644 index 00000000..08536503 Binary files /dev/null and b/app/assets/ic_add_collections_outlined_24dp.png differ diff --git a/app/lib/asset.dart b/app/lib/asset.dart new file mode 100644 index 00000000..1f7893cb --- /dev/null +++ b/app/lib/asset.dart @@ -0,0 +1,2 @@ +const icAddCollectionsOutlined24 = + "assets/ic_add_collections_outlined_24dp.png"; diff --git a/app/lib/entity/collection.dart b/app/lib/entity/collection.dart index 93e9d6c6..dd747e90 100644 --- a/app/lib/entity/collection.dart +++ b/app/lib/entity/collection.dart @@ -61,6 +61,9 @@ class Collection with EquatableMixin { /// See [CollectionContentProvider.isDynamicCollection] bool get isDynamicCollection => contentProvider.isDynamicCollection; + /// See [CollectionContentProvider.isPendingSharedAlbum] + bool get isPendingSharedAlbum => contentProvider.isPendingSharedAlbum; + @override List get props => [ name, @@ -136,4 +139,10 @@ abstract class CollectionContentProvider with EquatableMixin { /// A collection is defined as a dynamic one when the items are not specified /// explicitly by the user, but rather derived from some conditions bool get isDynamicCollection; + + /// Return whether this is a shared album pending to be added + /// + /// In some implementation, shared album does not immediately get added to the + /// collections list + bool get isPendingSharedAlbum; } diff --git a/app/lib/entity/collection/adapter.dart b/app/lib/entity/collection/adapter.dart index a348e089..ce144716 100644 --- a/app/lib/entity/collection/adapter.dart +++ b/app/lib/entity/collection/adapter.dart @@ -86,6 +86,9 @@ abstract class CollectionAdapter { required ValueChanged onCollectionUpdated, }); + /// Import a pending shared collection and return the resulting collection + Future importPendingShared(); + /// Convert a [NewCollectionItem] to an adapted one Future adaptToNewItem(NewCollectionItem original); diff --git a/app/lib/entity/collection/adapter/adapter_mixin.dart b/app/lib/entity/collection/adapter/adapter_mixin.dart index 8deda148..94dedea8 100644 --- a/app/lib/entity/collection/adapter/adapter_mixin.dart +++ b/app/lib/entity/collection/adapter/adapter_mixin.dart @@ -75,4 +75,9 @@ mixin CollectionAdapterUnshareableTag implements CollectionAdapter { }) { throw UnsupportedError("Operation not supported"); } + + @override + Future importPendingShared() { + throw UnsupportedError("Operation not supported"); + } } diff --git a/app/lib/entity/collection/adapter/album.dart b/app/lib/entity/collection/adapter/album.dart index 203f3437..e261e5ed 100644 --- a/app/lib/entity/collection/adapter/album.dart +++ b/app/lib/entity/collection/adapter/album.dart @@ -28,6 +28,7 @@ import 'package:nc_photos/use_case/album/remove_album.dart'; import 'package:nc_photos/use_case/album/remove_from_album.dart'; import 'package:nc_photos/use_case/album/share_album_with_user.dart'; import 'package:nc_photos/use_case/album/unshare_album_with_user.dart'; +import 'package:nc_photos/use_case/import_pending_shared_album.dart'; import 'package:nc_photos/use_case/preprocess_album.dart'; import 'package:nc_photos/use_case/unimport_shared_album.dart'; import 'package:nc_photos/use_case/update_album_with_actual_items.dart'; @@ -224,6 +225,13 @@ class CollectionAlbumAdapter implements CollectionAdapter { : CollectionShareResult.ok; } + @override + Future importPendingShared() async { + final newAlbum = + await ImportPendingSharedAlbum(_c)(account, _provider.album); + return CollectionBuilder.byAlbum(account, newAlbum); + } + @override Future adaptToNewItem(NewCollectionItem original) async { if (original is NewCollectionFileItem) { diff --git a/app/lib/entity/collection/content_provider/album.dart b/app/lib/entity/collection/content_provider/album.dart index 3aca7682..361a6c2f 100644 --- a/app/lib/entity/collection/content_provider/album.dart +++ b/app/lib/entity/collection/content_provider/album.dart @@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/collection.dart'; import 'package:nc_photos/entity/collection/util.dart'; import 'package:nc_photos/entity/collection_item/util.dart'; +import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:to_string/to_string.dart'; part 'album.g.dart'; @@ -102,6 +103,12 @@ class CollectionAlbumProvider @override bool get isDynamicCollection => album.provider is! AlbumStaticProvider; + @override + bool get isPendingSharedAlbum => + album.albumFile?.path.startsWith( + remote_storage_util.getRemotePendingSharedAlbumsDir(account)) == + true; + @override List get props => [account, album]; diff --git a/app/lib/entity/collection/content_provider/location_group.dart b/app/lib/entity/collection/content_provider/location_group.dart index 513af6fa..5a4cac2b 100644 --- a/app/lib/entity/collection/content_provider/location_group.dart +++ b/app/lib/entity/collection/content_provider/location_group.dart @@ -53,6 +53,9 @@ class CollectionLocationGroupProvider @override bool get isDynamicCollection => true; + @override + bool get isPendingSharedAlbum => false; + @override List get props => [account, location]; diff --git a/app/lib/entity/collection/content_provider/memory.dart b/app/lib/entity/collection/content_provider/memory.dart index 003b1256..0411d4ea 100644 --- a/app/lib/entity/collection/content_provider/memory.dart +++ b/app/lib/entity/collection/content_provider/memory.dart @@ -61,6 +61,9 @@ class CollectionMemoryProvider @override bool get isDynamicCollection => true; + @override + bool get isPendingSharedAlbum => false; + @override String toString() => _$toString(); diff --git a/app/lib/entity/collection/content_provider/nc_album.dart b/app/lib/entity/collection/content_provider/nc_album.dart index b0995579..9499701e 100644 --- a/app/lib/entity/collection/content_provider/nc_album.dart +++ b/app/lib/entity/collection/content_provider/nc_album.dart @@ -76,6 +76,9 @@ class CollectionNcAlbumProvider @override bool get isDynamicCollection => false; + @override + bool get isPendingSharedAlbum => false; + @override List get props => [account, album]; diff --git a/app/lib/entity/collection/content_provider/person.dart b/app/lib/entity/collection/content_provider/person.dart index 72bdfcd3..42d16ab6 100644 --- a/app/lib/entity/collection/content_provider/person.dart +++ b/app/lib/entity/collection/content_provider/person.dart @@ -51,6 +51,9 @@ class CollectionPersonProvider @override bool get isDynamicCollection => true; + @override + bool get isPendingSharedAlbum => false; + @override List get props => [account, person]; diff --git a/app/lib/entity/collection/content_provider/tag.dart b/app/lib/entity/collection/content_provider/tag.dart index 79fba8e7..de736f60 100644 --- a/app/lib/entity/collection/content_provider/tag.dart +++ b/app/lib/entity/collection/content_provider/tag.dart @@ -46,6 +46,9 @@ class CollectionTagProvider @override bool get isDynamicCollection => true; + @override + bool get isPendingSharedAlbum => false; + @override List get props => [account, tags]; diff --git a/app/lib/use_case/collection/import_pending_shared_collection.dart b/app/lib/use_case/collection/import_pending_shared_collection.dart new file mode 100644 index 00000000..53eba2a7 --- /dev/null +++ b/app/lib/use_case/collection/import_pending_shared_collection.dart @@ -0,0 +1,18 @@ +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/collection.dart'; +import 'package:nc_photos/entity/collection/adapter.dart'; + +class ImportPendingSharedCollection { + const ImportPendingSharedCollection(this._c); + + /// Import a pending shared collection to the app + /// + /// For some implementations, shared collection may live in a temporary + /// state before being accepted by the user. This use case will accept the + /// share and import the collection to the collections view + Future call(Account account, Collection collection) => + CollectionAdapter.of(_c, account, collection).importPendingShared(); + + final DiContainer _c; +} diff --git a/app/lib/widget/asset_icon.dart b/app/lib/widget/asset_icon.dart new file mode 100644 index 00000000..32beec2c --- /dev/null +++ b/app/lib/widget/asset_icon.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class AssetIcon extends StatelessWidget { + const AssetIcon( + this.assetName, { + super.key, + this.size, + this.color, + }); + + @override + Widget build(BuildContext context) { + return ImageIcon( + AssetImage(assetName), + size: size, + color: color, + ); + } + + final String assetName; + final double? size; + final Color? color; +} diff --git a/app/lib/widget/collection_browser.dart b/app/lib/widget/collection_browser.dart index 050f0a10..dafa3b0a 100644 --- a/app/lib/widget/collection_browser.dart +++ b/app/lib/widget/collection_browser.dart @@ -13,6 +13,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/asset.dart' as asset; import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/controller/account_controller.dart'; @@ -39,8 +40,10 @@ import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/use_case/archive_file.dart'; +import 'package:nc_photos/use_case/collection/import_pending_shared_collection.dart'; import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; import 'package:nc_photos/use_case/remove.dart'; +import 'package:nc_photos/widget/asset_icon.dart'; import 'package:nc_photos/widget/collection_picker.dart'; import 'package:nc_photos/widget/draggable_item_list.dart'; import 'package:nc_photos/widget/export_collection_dialog.dart'; @@ -164,6 +167,18 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } }, ), + BlocListener<_Bloc, _State>( + listenWhen: (previous, current) => + previous.importResult != current.importResult, + listener: (context, state) { + if (state.importResult != null) { + Navigator.of(context).pushReplacementNamed( + CollectionBrowser.routeName, + arguments: CollectionBrowserArguments(state.importResult!), + ); + } + }, + ), BlocListener<_Bloc, _State>( listenWhen: (previous, current) => previous.error != current.error, diff --git a/app/lib/widget/collection_browser.g.dart b/app/lib/widget/collection_browser.g.dart index ad8c948b..9729e7d8 100644 --- a/app/lib/widget/collection_browser.g.dart +++ b/app/lib/widget/collection_browser.g.dart @@ -29,6 +29,7 @@ abstract class $_StateCopyWithWorker { List<_Item>? editTransformedItems, CollectionItemSort? editSort, bool? isDragging, + Collection? importResult, ExceptionEvent? error, String? message}); } @@ -53,6 +54,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic editTransformedItems = copyWithNull, dynamic editSort = copyWithNull, dynamic isDragging, + dynamic importResult = copyWithNull, dynamic error = copyWithNull, dynamic message = copyWithNull}) { return _State( @@ -82,6 +84,9 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { ? that.editSort : editSort as CollectionItemSort?, isDragging: isDragging as bool? ?? that.isDragging, + importResult: importResult == copyWithNull + ? that.importResult + : importResult as Collection?, error: error == copyWithNull ? that.error : error as ExceptionEvent?, message: message == copyWithNull ? that.message : message as String?); } @@ -128,7 +133,7 @@ extension _$_BlocNpLog on _Bloc { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: $selectedItems, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isDragging: $isDragging, error: $error, message: $message}"; + return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: $selectedItems, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isDragging: $isDragging, importResult: $importResult, error: $error, message: $message}"; } } @@ -153,6 +158,14 @@ extension _$_TransformItemsToString on _TransformItems { } } +extension _$_ImportPendingSharedCollectionToString + on _ImportPendingSharedCollection { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_ImportPendingSharedCollection {}"; + } +} + extension _$_DownloadToString on _Download { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/collection_browser/app_bar.dart b/app/lib/widget/collection_browser/app_bar.dart index 97a2c9a3..4f0d1ed7 100644 --- a/app/lib/widget/collection_browser/app_bar.dart +++ b/app/lib/widget/collection_browser/app_bar.dart @@ -33,6 +33,12 @@ class _AppBar extends StatelessWidget { icon: const Icon(Icons.share), tooltip: L10n.global().shareTooltip, ), + if (state.collection.isPendingSharedAlbum) + IconButton( + onPressed: () => _onAddToCollectionsViewPressed(context), + icon: const AssetIcon(asset.icAddCollectionsOutlined24), + tooltip: L10n.global().addToCollectionsViewTooltip, + ), ]; if (state.items.isNotEmpty || canRename) { actions.add(PopupMenuButton<_MenuOption>( @@ -125,6 +131,10 @@ class _AppBar extends StatelessWidget { ), ); } + + Future _onAddToCollectionsViewPressed(BuildContext context) async { + context.read<_Bloc>().add(const _ImportPendingSharedCollection()); + } } class _AppBarCover extends StatelessWidget { diff --git a/app/lib/widget/collection_browser/bloc.dart b/app/lib/widget/collection_browser/bloc.dart index a5f0ffdc..ad0567c5 100644 --- a/app/lib/widget/collection_browser/bloc.dart +++ b/app/lib/widget/collection_browser/bloc.dart @@ -19,6 +19,7 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag { on<_UpdateCollection>(_onUpdateCollection); on<_LoadItems>(_onLoad); on<_TransformItems>(_onTransformItems); + on<_ImportPendingSharedCollection>(_onImportPendingSharedCollection); on<_Download>(_onDownload); @@ -56,6 +57,8 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag { add(_UpdateCollection(c.collection)); } }); + } else { + _log.info("[_Bloc] Ad hoc collection"); } _itemsControllerSubscription = itemsController.stream.listen( (_) {}, @@ -121,6 +124,15 @@ class _Bloc extends Bloc<_Event, _State> implements BlocTag { } } + Future _onImportPendingSharedCollection( + _ImportPendingSharedCollection ev, Emitter<_State> emit) async { + _log.info(ev); + // pending collections are always ad hoc + final newCollection = + await ImportPendingSharedCollection(_c)(account, state.collection); + emit(state.copyWith(importResult: newCollection)); + } + void _onBeginEdit(_BeginEdit ev, Emitter<_State> emit) { _log.info("$ev"); emit(state.copyWith(isEditMode: true)); diff --git a/app/lib/widget/collection_browser/state_event.dart b/app/lib/widget/collection_browser/state_event.dart index 01b5fa9c..fa2de2e2 100644 --- a/app/lib/widget/collection_browser/state_event.dart +++ b/app/lib/widget/collection_browser/state_event.dart @@ -19,6 +19,7 @@ class _State { this.editTransformedItems, this.editSort, required this.isDragging, + this.importResult, this.error, this.message, }); @@ -66,6 +67,8 @@ class _State { final bool isDragging; + final Collection? importResult; + final ExceptionEvent? error; final String? message; } @@ -104,6 +107,14 @@ class _TransformItems implements _Event { final List items; } +@toString +class _ImportPendingSharedCollection implements _Event { + const _ImportPendingSharedCollection(); + + @override + String toString() => _$toString(); +} + @toString class _Download implements _Event { const _Download(); diff --git a/app/lib/widget/sharing_browser.dart b/app/lib/widget/sharing_browser.dart index e5c76186..3766245f 100644 --- a/app/lib/widget/sharing_browser.dart +++ b/app/lib/widget/sharing_browser.dart @@ -10,6 +10,7 @@ import 'package:nc_photos/bloc/list_sharing.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album/data_source.dart'; +import 'package:nc_photos/entity/collection/builder.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/share.dart'; @@ -20,7 +21,7 @@ import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/use_case/import_potential_shared_album.dart'; -import 'package:nc_photos/widget/album_browser_util.dart' as album_browser_util; +import 'package:nc_photos/widget/collection_browser.dart'; import 'package:nc_photos/widget/empty_list_indicator.dart'; import 'package:nc_photos/widget/network_thumbnail.dart'; import 'package:nc_photos/widget/shared_file_viewer.dart'; @@ -252,7 +253,12 @@ class _SharingBrowserState extends State { } void _onAlbumShareItemTap(BuildContext context, ListSharingAlbum share) { - album_browser_util.push(context, widget.account, share.album); + Navigator.of(context).pushNamed( + CollectionBrowser.routeName, + arguments: CollectionBrowserArguments( + CollectionBuilder.byAlbum(widget.account, share.album), + ), + ); } void _transformItems(List items) {