mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Regression: share and unshare album
This commit is contained in:
parent
ec8e9efa6f
commit
d2886e55c1
40 changed files with 1032 additions and 52 deletions
|
@ -8,16 +8,22 @@ import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/controller/collection_items_controller.dart';
|
import 'package:nc_photos/controller/collection_items_controller.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/entity/sharee.dart';
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/rx_extension.dart';
|
import 'package:nc_photos/rx_extension.dart';
|
||||||
import 'package:nc_photos/use_case/collection/create_collection.dart';
|
import 'package:nc_photos/use_case/collection/create_collection.dart';
|
||||||
import 'package:nc_photos/use_case/collection/edit_collection.dart';
|
import 'package:nc_photos/use_case/collection/edit_collection.dart';
|
||||||
import 'package:nc_photos/use_case/collection/list_collection.dart';
|
import 'package:nc_photos/use_case/collection/list_collection.dart';
|
||||||
import 'package:nc_photos/use_case/collection/remove_collections.dart';
|
import 'package:nc_photos/use_case/collection/remove_collections.dart';
|
||||||
|
import 'package:nc_photos/use_case/collection/share_collection.dart';
|
||||||
|
import 'package:nc_photos/use_case/collection/unshare_collection.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
@ -183,6 +189,54 @@ class CollectionsController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> share(Collection collection, Sharee sharee) async {
|
||||||
|
try {
|
||||||
|
Collection? newCollection;
|
||||||
|
final result = await _mutex.protect(() async {
|
||||||
|
return await ShareCollection(_c)(
|
||||||
|
account,
|
||||||
|
collection,
|
||||||
|
sharee,
|
||||||
|
onCollectionUpdated: (c) {
|
||||||
|
newCollection = c;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (newCollection != null) {
|
||||||
|
_updateCollection(newCollection!);
|
||||||
|
}
|
||||||
|
if (result == CollectionShareResult.partial) {
|
||||||
|
_dataStreamController.addError(const CollectionPartialShareException());
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_dataStreamController.addError(e, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unshare(Collection collection, CiString userId) async {
|
||||||
|
try {
|
||||||
|
Collection? newCollection;
|
||||||
|
final result = await _mutex.protect(() async {
|
||||||
|
return await UnshareCollection(_c)(
|
||||||
|
account,
|
||||||
|
collection,
|
||||||
|
userId,
|
||||||
|
onCollectionUpdated: (c) {
|
||||||
|
newCollection = c;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (newCollection != null) {
|
||||||
|
_updateCollection(newCollection!);
|
||||||
|
}
|
||||||
|
if (result == CollectionShareResult.partial) {
|
||||||
|
_dataStreamController.addError(const CollectionPartialShareException());
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_dataStreamController.addError(e, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _load() async {
|
Future<void> _load() async {
|
||||||
var lastData = const CollectionStreamEvent(
|
var lastData = const CollectionStreamEvent(
|
||||||
data: [],
|
data: [],
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:copy_with/copy_with.dart';
|
import 'package:copy_with/copy_with.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/sorter.dart';
|
import 'package:nc_photos/entity/collection_item/sorter.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:to_string/to_string.dart';
|
import 'package:to_string/to_string.dart';
|
||||||
|
@ -40,6 +41,9 @@ class Collection with EquatableMixin {
|
||||||
/// See [CollectionContentProvider.itemSort]
|
/// See [CollectionContentProvider.itemSort]
|
||||||
CollectionItemSort get itemSort => contentProvider.itemSort;
|
CollectionItemSort get itemSort => contentProvider.itemSort;
|
||||||
|
|
||||||
|
/// See [CollectionContentProvider.sharees]
|
||||||
|
List<CollectionShare> get shares => contentProvider.shares;
|
||||||
|
|
||||||
/// See [CollectionContentProvider.getCoverUrl]
|
/// See [CollectionContentProvider.getCoverUrl]
|
||||||
String? getCoverUrl(
|
String? getCoverUrl(
|
||||||
int width,
|
int width,
|
||||||
|
@ -77,6 +81,8 @@ enum CollectionCapability {
|
||||||
labelItem,
|
labelItem,
|
||||||
// set the cover image
|
// set the cover image
|
||||||
manualCover,
|
manualCover,
|
||||||
|
// share the collection with other user on the same server
|
||||||
|
share,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide the actual content of a collection
|
/// Provide the actual content of a collection
|
||||||
|
@ -106,6 +112,10 @@ abstract class CollectionContentProvider with EquatableMixin {
|
||||||
/// Return the sort type
|
/// Return the sort type
|
||||||
CollectionItemSort get itemSort;
|
CollectionItemSort get itemSort;
|
||||||
|
|
||||||
|
/// Return list of users who have access to this collection, excluding the
|
||||||
|
/// current user
|
||||||
|
List<CollectionShare> get shares;
|
||||||
|
|
||||||
/// Return the URL of the cover image if available
|
/// Return the URL of the cover image if available
|
||||||
///
|
///
|
||||||
/// The [width] and [height] are provided as a hint only, implementations are
|
/// The [width] and [height] are provided as a hint only, implementations are
|
||||||
|
|
|
@ -14,11 +14,14 @@ import 'package:nc_photos/entity/collection/content_provider/memory.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/person.dart';
|
import 'package:nc_photos/entity/collection/content_provider/person.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/tag.dart';
|
import 'package:nc_photos/entity/collection/content_provider/tag.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/entity/sharee.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
abstract class CollectionAdapter {
|
abstract class CollectionAdapter {
|
||||||
|
@ -71,6 +74,18 @@ abstract class CollectionAdapter {
|
||||||
required ValueChanged<Collection> onCollectionUpdated,
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Share the collection with [sharee]
|
||||||
|
Future<CollectionShareResult> share(
|
||||||
|
Sharee sharee, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Unshare the collection with a user
|
||||||
|
Future<CollectionShareResult> unshare(
|
||||||
|
CiString userId, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
});
|
||||||
|
|
||||||
/// Convert a [NewCollectionItem] to an adapted one
|
/// Convert a [NewCollectionItem] to an adapted one
|
||||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/entity/sharee.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
/// A read-only collection that does not support modifying its items
|
/// A read-only collection that does not support modifying its items
|
||||||
mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
mixin CollectionAdapterReadOnlyTag implements CollectionAdapter {
|
||||||
@override
|
@override
|
||||||
Future<int> addFiles(
|
Future<int> addFiles(
|
||||||
List<FileDescriptor> files, {
|
List<FileDescriptor> files, {
|
||||||
|
@ -48,3 +51,28 @@ mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
||||||
Future<Collection?> updatePostLoad(List<CollectionItem> items) =>
|
Future<Collection?> updatePostLoad(List<CollectionItem> items) =>
|
||||||
Future.value(null);
|
Future.value(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mixin CollectionAdapterUnremovableTag implements CollectionAdapter {
|
||||||
|
@override
|
||||||
|
Future<void> remove() {
|
||||||
|
throw UnsupportedError("Operation not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin CollectionAdapterUnshareableTag implements CollectionAdapter {
|
||||||
|
@override
|
||||||
|
Future<CollectionShareResult> share(
|
||||||
|
Sharee sharee, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
}) {
|
||||||
|
throw UnsupportedError("Operation not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CollectionShareResult> unshare(
|
||||||
|
CiString userId, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
}) {
|
||||||
|
throw UnsupportedError("Operation not supported");
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/debug_util.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||||
import 'package:nc_photos/entity/album/item.dart';
|
import 'package:nc_photos/entity/album/item.dart';
|
||||||
|
@ -10,12 +11,14 @@ import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection/builder.dart';
|
import 'package:nc_photos/entity/collection/builder.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
import 'package:nc_photos/entity/collection/content_provider/album.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/album_item_adapter.dart';
|
import 'package:nc_photos/entity/collection_item/album_item_adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
import 'package:nc_photos/entity/collection_item/new_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/util.dart';
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/entity/sharee.dart';
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
|
@ -23,9 +26,12 @@ import 'package:nc_photos/use_case/album/add_file_to_album.dart';
|
||||||
import 'package:nc_photos/use_case/album/edit_album.dart';
|
import 'package:nc_photos/use_case/album/edit_album.dart';
|
||||||
import 'package:nc_photos/use_case/album/remove_album.dart';
|
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/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/preprocess_album.dart';
|
import 'package:nc_photos/use_case/preprocess_album.dart';
|
||||||
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:np_common/type.dart';
|
import 'package:np_common/type.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -173,6 +179,50 @@ class CollectionAlbumAdapter implements CollectionAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CollectionShareResult> share(
|
||||||
|
Sharee sharee, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
}) async {
|
||||||
|
var fileFailed = false;
|
||||||
|
final newAlbum = await ShareAlbumWithUser(_c.shareRepo, _c.albumRepo)(
|
||||||
|
account,
|
||||||
|
_provider.album,
|
||||||
|
sharee,
|
||||||
|
onShareFileFailed: (f, e, stackTrace) {
|
||||||
|
_log.severe("[share] Failed to share file: ${logFilename(f.path)}", e,
|
||||||
|
stackTrace);
|
||||||
|
fileFailed = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
onCollectionUpdated(CollectionBuilder.byAlbum(account, newAlbum));
|
||||||
|
return fileFailed
|
||||||
|
? CollectionShareResult.partial
|
||||||
|
: CollectionShareResult.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CollectionShareResult> unshare(
|
||||||
|
CiString userId, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
}) async {
|
||||||
|
var fileFailed = false;
|
||||||
|
final newAlbum = await UnshareAlbumWithUser(_c)(
|
||||||
|
account,
|
||||||
|
_provider.album,
|
||||||
|
userId,
|
||||||
|
onUnshareFileFailed: (f, e, stackTrace) {
|
||||||
|
_log.severe("[unshare] Failed to unshare file: ${logFilename(f.path)}",
|
||||||
|
e, stackTrace);
|
||||||
|
fileFailed = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
onCollectionUpdated(CollectionBuilder.byAlbum(account, newAlbum));
|
||||||
|
return fileFailed
|
||||||
|
? CollectionShareResult.partial
|
||||||
|
: CollectionShareResult.ok;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original) async {
|
Future<CollectionItem> adaptToNewItem(NewCollectionItem original) async {
|
||||||
if (original is NewCollectionFileItem) {
|
if (original is NewCollectionFileItem) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/read_only_adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/location_group.dart';
|
import 'package:nc_photos/entity/collection/content_provider/location_group.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||||
|
@ -11,7 +11,10 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/use_case/list_location_file.dart';
|
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||||
|
|
||||||
class CollectionLocationGroupAdapter
|
class CollectionLocationGroupAdapter
|
||||||
with CollectionReadOnlyAdapter
|
with
|
||||||
|
CollectionAdapterReadOnlyTag,
|
||||||
|
CollectionAdapterUnremovableTag,
|
||||||
|
CollectionAdapterUnshareableTag
|
||||||
implements CollectionAdapter {
|
implements CollectionAdapter {
|
||||||
CollectionLocationGroupAdapter(this._c, this.account, this.collection)
|
CollectionLocationGroupAdapter(this._c, this.account, this.collection)
|
||||||
: assert(require(_c)),
|
: assert(require(_c)),
|
||||||
|
@ -43,11 +46,6 @@ class CollectionLocationGroupAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> remove() {
|
|
||||||
throw UnsupportedError("Operation not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isPermitted(CollectionCapability capability) =>
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
_provider.capabilities.contains(capability);
|
_provider.capabilities.contains(capability);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/read_only_adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/memory.dart';
|
import 'package:nc_photos/entity/collection/content_provider/memory.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||||
|
@ -11,7 +11,10 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/use_case/list_location_file.dart';
|
import 'package:nc_photos/use_case/list_location_file.dart';
|
||||||
|
|
||||||
class CollectionMemoryAdapter
|
class CollectionMemoryAdapter
|
||||||
with CollectionReadOnlyAdapter
|
with
|
||||||
|
CollectionAdapterReadOnlyTag,
|
||||||
|
CollectionAdapterUnremovableTag,
|
||||||
|
CollectionAdapterUnshareableTag
|
||||||
implements CollectionAdapter {
|
implements CollectionAdapter {
|
||||||
CollectionMemoryAdapter(this._c, this.account, this.collection)
|
CollectionMemoryAdapter(this._c, this.account, this.collection)
|
||||||
: assert(require(_c)),
|
: assert(require(_c)),
|
||||||
|
@ -42,11 +45,6 @@ class CollectionMemoryAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> remove() {
|
|
||||||
throw UnsupportedError("Operation not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isPermitted(CollectionCapability capability) =>
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
_provider.capabilities.contains(capability);
|
_provider.capabilities.contains(capability);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
import 'package:nc_photos/entity/collection/content_provider/nc_album.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||||
|
@ -27,7 +28,9 @@ import 'package:np_common/type.dart';
|
||||||
part 'nc_album.g.dart';
|
part 'nc_album.g.dart';
|
||||||
|
|
||||||
@npLog
|
@npLog
|
||||||
class CollectionNcAlbumAdapter implements CollectionAdapter {
|
class CollectionNcAlbumAdapter
|
||||||
|
with CollectionAdapterUnshareableTag
|
||||||
|
implements CollectionAdapter {
|
||||||
CollectionNcAlbumAdapter(this._c, this.account, this.collection)
|
CollectionNcAlbumAdapter(this._c, this.account, this.collection)
|
||||||
: assert(require(_c)),
|
: assert(require(_c)),
|
||||||
_provider = collection.contentProvider as CollectionNcAlbumProvider;
|
_provider = collection.contentProvider as CollectionNcAlbumProvider;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/read_only_adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/person.dart';
|
import 'package:nc_photos/entity/collection/content_provider/person.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||||
|
@ -12,7 +12,10 @@ import 'package:nc_photos/use_case/list_face.dart';
|
||||||
import 'package:nc_photos/use_case/populate_person.dart';
|
import 'package:nc_photos/use_case/populate_person.dart';
|
||||||
|
|
||||||
class CollectionPersonAdapter
|
class CollectionPersonAdapter
|
||||||
with CollectionReadOnlyAdapter
|
with
|
||||||
|
CollectionAdapterReadOnlyTag,
|
||||||
|
CollectionAdapterUnremovableTag,
|
||||||
|
CollectionAdapterUnshareableTag
|
||||||
implements CollectionAdapter {
|
implements CollectionAdapter {
|
||||||
CollectionPersonAdapter(this._c, this.account, this.collection)
|
CollectionPersonAdapter(this._c, this.account, this.collection)
|
||||||
: assert(require(_c)),
|
: assert(require(_c)),
|
||||||
|
@ -45,11 +48,6 @@ class CollectionPersonAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> remove() {
|
|
||||||
throw UnsupportedError("Operation not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isPermitted(CollectionCapability capability) =>
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
_provider.capabilities.contains(capability);
|
_provider.capabilities.contains(capability);
|
||||||
|
|
|
@ -2,14 +2,17 @@ import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter.dart';
|
||||||
import 'package:nc_photos/entity/collection/adapter/read_only_adapter.dart';
|
import 'package:nc_photos/entity/collection/adapter/adapter_mixin.dart';
|
||||||
import 'package:nc_photos/entity/collection/content_provider/tag.dart';
|
import 'package:nc_photos/entity/collection/content_provider/tag.dart';
|
||||||
import 'package:nc_photos/entity/collection_item.dart';
|
import 'package:nc_photos/entity/collection_item.dart';
|
||||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||||
import 'package:nc_photos/use_case/list_tagged_file.dart';
|
import 'package:nc_photos/use_case/list_tagged_file.dart';
|
||||||
|
|
||||||
class CollectionTagAdapter
|
class CollectionTagAdapter
|
||||||
with CollectionReadOnlyAdapter
|
with
|
||||||
|
CollectionAdapterReadOnlyTag,
|
||||||
|
CollectionAdapterUnremovableTag,
|
||||||
|
CollectionAdapterUnshareableTag
|
||||||
implements CollectionAdapter {
|
implements CollectionAdapter {
|
||||||
CollectionTagAdapter(this._c, this.account, this.collection)
|
CollectionTagAdapter(this._c, this.account, this.collection)
|
||||||
: assert(require(_c)),
|
: assert(require(_c)),
|
||||||
|
@ -32,11 +35,6 @@ class CollectionTagAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> remove() {
|
|
||||||
throw UnsupportedError("Operation not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isPermitted(CollectionCapability capability) =>
|
bool isPermitted(CollectionCapability capability) =>
|
||||||
_provider.capabilities.contains(capability);
|
_provider.capabilities.contains(capability);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
import 'package:nc_photos/entity/album/provider.dart';
|
||||||
import 'package:nc_photos/entity/collection.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/entity/collection_item/util.dart';
|
||||||
import 'package:to_string/to_string.dart';
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
|
@ -52,6 +53,7 @@ class CollectionAlbumProvider
|
||||||
CollectionCapability.manualItem,
|
CollectionCapability.manualItem,
|
||||||
CollectionCapability.manualSort,
|
CollectionCapability.manualSort,
|
||||||
CollectionCapability.labelItem,
|
CollectionCapability.labelItem,
|
||||||
|
CollectionCapability.share,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -66,6 +68,17 @@ class CollectionAlbumProvider
|
||||||
@override
|
@override
|
||||||
CollectionItemSort get itemSort => album.sortProvider.toCollectionItemSort();
|
CollectionItemSort get itemSort => album.sortProvider.toCollectionItemSort();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionShare> get shares =>
|
||||||
|
album.shares
|
||||||
|
?.where((s) => s.userId != account.userId)
|
||||||
|
.map((s) => CollectionShare(
|
||||||
|
userId: s.userId,
|
||||||
|
username: s.displayName ?? s.userId.raw,
|
||||||
|
))
|
||||||
|
.toList() ??
|
||||||
|
const [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getCoverUrl(
|
String? getCoverUrl(
|
||||||
int width,
|
int width,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/entity/collection.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/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/use_case/list_location_group.dart';
|
import 'package:nc_photos/use_case/list_location_group.dart';
|
||||||
|
|
||||||
|
@ -31,6 +32,9 @@ class CollectionLocationGroupProvider
|
||||||
@override
|
@override
|
||||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionShare> get shares => [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getCoverUrl(
|
String? getCoverUrl(
|
||||||
int width,
|
int width,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/entity/collection.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/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
@ -39,6 +40,9 @@ class CollectionMemoryProvider
|
||||||
@override
|
@override
|
||||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionShare> get shares => [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getCoverUrl(
|
String? getCoverUrl(
|
||||||
int width,
|
int width,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/entity/collection.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/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/nc_album.dart';
|
import 'package:nc_photos/entity/nc_album.dart';
|
||||||
import 'package:to_string/to_string.dart';
|
import 'package:to_string/to_string.dart';
|
||||||
|
@ -45,6 +46,9 @@ class CollectionNcAlbumProvider
|
||||||
@override
|
@override
|
||||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionShare> get shares => [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getCoverUrl(
|
String? getCoverUrl(
|
||||||
int width,
|
int width,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/entity/collection.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/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/person.dart';
|
import 'package:nc_photos/entity/person.dart';
|
||||||
|
|
||||||
|
@ -34,6 +35,9 @@ class CollectionPersonProvider
|
||||||
@override
|
@override
|
||||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionShare> get shares => [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getCoverUrl(
|
String? getCoverUrl(
|
||||||
int width,
|
int width,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:clock/clock.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/collection.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/entity/collection_item/util.dart';
|
||||||
import 'package:nc_photos/entity/tag.dart';
|
import 'package:nc_photos/entity/tag.dart';
|
||||||
|
|
||||||
|
@ -31,6 +32,9 @@ class CollectionTagProvider
|
||||||
@override
|
@override
|
||||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CollectionShare> get shares => [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? getCoverUrl(
|
String? getCoverUrl(
|
||||||
int width,
|
int width,
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:nc_photos/entity/collection.dart';
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
part 'util.g.dart';
|
||||||
|
|
||||||
enum CollectionSort {
|
enum CollectionSort {
|
||||||
dateDescending,
|
dateDescending,
|
||||||
dateAscending,
|
dateAscending,
|
||||||
|
@ -14,6 +19,28 @@ enum CollectionSort {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class CollectionShare with EquatableMixin {
|
||||||
|
const CollectionShare({
|
||||||
|
required this.userId,
|
||||||
|
required this.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [userId, username];
|
||||||
|
|
||||||
|
final CiString userId;
|
||||||
|
final String username;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CollectionShareResult {
|
||||||
|
ok,
|
||||||
|
partial,
|
||||||
|
}
|
||||||
|
|
||||||
extension CollectionListExtension on Iterable<Collection> {
|
extension CollectionListExtension on Iterable<Collection> {
|
||||||
List<Collection> sortedBy(CollectionSort by) {
|
List<Collection> sortedBy(CollectionSort by) {
|
||||||
return map<Tuple2<Comparable, Collection>>((e) {
|
return map<Tuple2<Comparable, Collection>>((e) {
|
||||||
|
|
14
app/lib/entity/collection/util.g.dart
Normal file
14
app/lib/entity/collection/util.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'util.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$CollectionShareToString on CollectionShare {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "CollectionShare {userId: $userId, username: $username}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,3 +103,18 @@ class AlbumItemPermissionException implements Exception {
|
||||||
|
|
||||||
final dynamic message;
|
final dynamic message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CollectionPartialShareException implements Exception {
|
||||||
|
const CollectionPartialShareException([this.message]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (message == null) {
|
||||||
|
return "CollectionPartialShareException";
|
||||||
|
} else {
|
||||||
|
return "CollectionPartialShareException: $message";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final dynamic message;
|
||||||
|
}
|
||||||
|
|
57
app/lib/suggester.dart
Normal file
57
app/lib/suggester.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'package:woozy_search/woozy_search.dart';
|
||||||
|
|
||||||
|
part 'suggester.g.dart';
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class Suggester<T> {
|
||||||
|
Suggester({
|
||||||
|
required this.items,
|
||||||
|
required this.itemToKeywords,
|
||||||
|
int maxResult = 5,
|
||||||
|
}) : _searcher = Woozy(limit: maxResult) {
|
||||||
|
for (final a in items) {
|
||||||
|
for (final k in itemToKeywords(a)) {
|
||||||
|
_searcher.addEntry(k.toCaseInsensitiveString(), value: a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> search(CiString phrase) {
|
||||||
|
final results = _searcher.search(phrase.toCaseInsensitiveString());
|
||||||
|
if (kDebugMode) {
|
||||||
|
final str = results.map((e) => "${e.score}: ${e.text}").join("\n");
|
||||||
|
_log.info("[search] Search '$phrase':\n$str");
|
||||||
|
}
|
||||||
|
final matches = results
|
||||||
|
.where((e) => e.score > 0)
|
||||||
|
.map((e) {
|
||||||
|
if (itemToKeywords(e.value as T).any((k) => k.startsWith(phrase))) {
|
||||||
|
// prefer names that start exactly with the search phrase
|
||||||
|
return Tuple2(e.score + 1, e.value as T);
|
||||||
|
} else {
|
||||||
|
return Tuple2(e.score, e.value as T);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sorted((a, b) => a.item1.compareTo(b.item1))
|
||||||
|
.reversed
|
||||||
|
.distinctIf(
|
||||||
|
(a, b) => identical(a.item2, b.item2),
|
||||||
|
(a) => a.item2.hashCode,
|
||||||
|
)
|
||||||
|
.map((e) => e.item2)
|
||||||
|
.toList();
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<T> items;
|
||||||
|
final List<CiString> Function(T item) itemToKeywords;
|
||||||
|
|
||||||
|
final Woozy _searcher;
|
||||||
|
}
|
14
app/lib/suggester.g.dart
Normal file
14
app/lib/suggester.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'suggester.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$SuggesterNpLog on Suggester {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log = Logger("suggester.Suggester");
|
||||||
|
}
|
|
@ -6,10 +6,10 @@ import 'package:nc_photos/entity/album/item.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
import 'package:nc_photos/entity/album/provider.dart';
|
||||||
import 'package:nc_photos/entity/share.dart';
|
import 'package:nc_photos/entity/share.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
|
import 'package:nc_photos/use_case/album/unshare_file_from_album.dart';
|
||||||
import 'package:nc_photos/use_case/list_share.dart';
|
import 'package:nc_photos/use_case/list_share.dart';
|
||||||
import 'package:nc_photos/use_case/remove.dart';
|
import 'package:nc_photos/use_case/remove.dart';
|
||||||
import 'package:nc_photos/use_case/remove_share.dart';
|
import 'package:nc_photos/use_case/remove_share.dart';
|
||||||
import 'package:nc_photos/use_case/unshare_file_from_album.dart';
|
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||||
import 'package:nc_photos/exception.dart';
|
import 'package:nc_photos/exception.dart';
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:nc_photos/use_case/album/unshare_file_from_album.dart';
|
||||||
import 'package:nc_photos/use_case/preprocess_album.dart';
|
import 'package:nc_photos/use_case/preprocess_album.dart';
|
||||||
import 'package:nc_photos/use_case/unshare_file_from_album.dart';
|
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
|
|
@ -13,6 +13,7 @@ import 'package:nc_photos/use_case/create_share.dart';
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/ci_string.dart';
|
import 'package:np_common/ci_string.dart';
|
||||||
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
part 'share_album_with_user.g.dart';
|
part 'share_album_with_user.g.dart';
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ class ShareAlbumWithUser {
|
||||||
Account account,
|
Account account,
|
||||||
Album album,
|
Album album,
|
||||||
Sharee sharee, {
|
Sharee sharee, {
|
||||||
void Function(File)? onShareFileFailed,
|
ErrorWithValueHandler<File>? onShareFileFailed,
|
||||||
}) async {
|
}) async {
|
||||||
assert(album.provider is AlbumStaticProvider);
|
assert(album.provider is AlbumStaticProvider);
|
||||||
final newShares = (album.shares ?? [])
|
final newShares = (album.shares ?? [])
|
||||||
|
@ -55,7 +56,7 @@ class ShareAlbumWithUser {
|
||||||
Account account,
|
Account account,
|
||||||
Album album,
|
Album album,
|
||||||
CiString shareWith, {
|
CiString shareWith, {
|
||||||
void Function(File)? onShareFileFailed,
|
ErrorWithValueHandler<File>? onShareFileFailed,
|
||||||
}) async {
|
}) async {
|
||||||
final files = AlbumStaticProvider.of(album)
|
final files = AlbumStaticProvider.of(album)
|
||||||
.items
|
.items
|
||||||
|
@ -70,7 +71,7 @@ class ShareAlbumWithUser {
|
||||||
"[_createFileShares] Failed sharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
|
"[_createFileShares] Failed sharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
|
||||||
e,
|
e,
|
||||||
stackTrace);
|
stackTrace);
|
||||||
onShareFileFailed?.call(album.albumFile!);
|
onShareFileFailed?.call(album.albumFile!, e, stackTrace);
|
||||||
}
|
}
|
||||||
for (final f in files) {
|
for (final f in files) {
|
||||||
_log.info("[_createFileShares] Sharing '${f.path}' with '$shareWith'");
|
_log.info("[_createFileShares] Sharing '${f.path}' with '$shareWith'");
|
||||||
|
@ -81,7 +82,7 @@ class ShareAlbumWithUser {
|
||||||
"[_createFileShares] Failed sharing file '${logFilename(f.path)}' with '$shareWith'",
|
"[_createFileShares] Failed sharing file '${logFilename(f.path)}' with '$shareWith'",
|
||||||
e,
|
e,
|
||||||
stackTrace);
|
stackTrace);
|
||||||
onShareFileFailed?.call(f);
|
onShareFileFailed?.call(f, e, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,5 +11,5 @@ extension _$ShareAlbumWithUserNpLog on ShareAlbumWithUser {
|
||||||
Logger get _log => log;
|
Logger get _log => log;
|
||||||
|
|
||||||
static final log =
|
static final log =
|
||||||
Logger("use_case.share_album_with_user.ShareAlbumWithUser");
|
Logger("use_case.album.share_album_with_user.ShareAlbumWithUser");
|
||||||
}
|
}
|
|
@ -7,12 +7,13 @@ import 'package:nc_photos/entity/album/item.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
import 'package:nc_photos/entity/album/provider.dart';
|
||||||
import 'package:nc_photos/entity/share.dart';
|
import 'package:nc_photos/entity/share.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
|
import 'package:nc_photos/use_case/album/unshare_file_from_album.dart';
|
||||||
import 'package:nc_photos/use_case/list_share.dart';
|
import 'package:nc_photos/use_case/list_share.dart';
|
||||||
import 'package:nc_photos/use_case/remove_share.dart';
|
import 'package:nc_photos/use_case/remove_share.dart';
|
||||||
import 'package:nc_photos/use_case/unshare_file_from_album.dart';
|
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/ci_string.dart';
|
import 'package:np_common/ci_string.dart';
|
||||||
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
part 'unshare_album_with_user.g.dart';
|
part 'unshare_album_with_user.g.dart';
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ class UnshareAlbumWithUser {
|
||||||
Account account,
|
Account account,
|
||||||
Album album,
|
Album album,
|
||||||
CiString shareWith, {
|
CiString shareWith, {
|
||||||
void Function(Share)? onUnshareFileFailed,
|
ErrorWithValueHandler<Share>? onUnshareFileFailed,
|
||||||
}) async {
|
}) async {
|
||||||
assert(album.provider is AlbumStaticProvider);
|
assert(album.provider is AlbumStaticProvider);
|
||||||
// remove the share from album file
|
// remove the share from album file
|
||||||
|
@ -59,7 +60,7 @@ class UnshareAlbumWithUser {
|
||||||
Account account,
|
Account account,
|
||||||
Album album,
|
Album album,
|
||||||
CiString shareWith, {
|
CiString shareWith, {
|
||||||
void Function(Share)? onUnshareFileFailed,
|
ErrorWithValueHandler<Share>? onUnshareFileFailed,
|
||||||
}) async {
|
}) async {
|
||||||
// remove share from the album file
|
// remove share from the album file
|
||||||
final albumShares = await ListShare(_c)(account, album.albumFile!);
|
final albumShares = await ListShare(_c)(account, album.albumFile!);
|
||||||
|
@ -71,7 +72,7 @@ class UnshareAlbumWithUser {
|
||||||
"[_deleteFileShares] Failed unsharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
|
"[_deleteFileShares] Failed unsharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
|
||||||
e,
|
e,
|
||||||
stackTrace);
|
stackTrace);
|
||||||
onUnshareFileFailed?.call(s);
|
onUnshareFileFailed?.call(s, e, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,5 @@ extension _$UnshareAlbumWithUserNpLog on UnshareAlbumWithUser {
|
||||||
Logger get _log => log;
|
Logger get _log => log;
|
||||||
|
|
||||||
static final log =
|
static final log =
|
||||||
Logger("use_case.unshare_album_with_user.UnshareAlbumWithUser");
|
Logger("use_case.album.unshare_album_with_user.UnshareAlbumWithUser");
|
||||||
}
|
}
|
|
@ -14,6 +14,7 @@ import 'package:nc_photos/use_case/list_share.dart';
|
||||||
import 'package:nc_photos/use_case/remove_share.dart';
|
import 'package:nc_photos/use_case/remove_share.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/ci_string.dart';
|
import 'package:np_common/ci_string.dart';
|
||||||
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
part 'unshare_file_from_album.g.dart';
|
part 'unshare_file_from_album.g.dart';
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class UnshareFileFromAlbum {
|
||||||
Album album,
|
Album album,
|
||||||
List<File> files,
|
List<File> files,
|
||||||
List<CiString> unshareWith, {
|
List<CiString> unshareWith, {
|
||||||
void Function(Share)? onUnshareFileFailed,
|
ErrorWithValueHandler<Share>? onUnshareFileFailed,
|
||||||
}) async {
|
}) async {
|
||||||
_log.info(
|
_log.info(
|
||||||
"[call] Unshare ${files.length} files from album '${album.name}' with ${unshareWith.length} users");
|
"[call] Unshare ${files.length} files from album '${album.name}' with ${unshareWith.length} users");
|
||||||
|
@ -85,14 +86,14 @@ class UnshareFileFromAlbum {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _unshare(Account account, List<Share> shares,
|
Future<void> _unshare(Account account, List<Share> shares,
|
||||||
void Function(Share)? onUnshareFileFailed) async {
|
ErrorWithValueHandler<Share>? onUnshareFileFailed) async {
|
||||||
for (final s in shares) {
|
for (final s in shares) {
|
||||||
try {
|
try {
|
||||||
await RemoveShare(_c.shareRepo)(account, s);
|
await RemoveShare(_c.shareRepo)(account, s);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_log.severe(
|
_log.severe(
|
||||||
"[_unshare] Failed while RemoveShare: ${s.path}", e, stackTrace);
|
"[_unshare] Failed while RemoveShare: ${s.path}", e, stackTrace);
|
||||||
onUnshareFileFailed?.call(s);
|
onUnshareFileFailed?.call(s, e, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -11,5 +11,5 @@ extension _$UnshareFileFromAlbumNpLog on UnshareFileFromAlbum {
|
||||||
Logger get _log => log;
|
Logger get _log => log;
|
||||||
|
|
||||||
static final log =
|
static final log =
|
||||||
Logger("use_case.unshare_file_from_album.UnshareFileFromAlbum");
|
Logger("use_case.album.unshare_file_from_album.UnshareFileFromAlbum");
|
||||||
}
|
}
|
25
app/lib/use_case/collection/share_collection.dart
Normal file
25
app/lib/use_case/collection/share_collection.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
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';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
|
import 'package:nc_photos/entity/sharee.dart';
|
||||||
|
|
||||||
|
class ShareCollection {
|
||||||
|
const ShareCollection(this._c);
|
||||||
|
|
||||||
|
/// Share the collection with [sharee]
|
||||||
|
Future<CollectionShareResult> call(
|
||||||
|
Account account,
|
||||||
|
Collection collection,
|
||||||
|
Sharee sharee, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
}) =>
|
||||||
|
CollectionAdapter.of(_c, account, collection).share(
|
||||||
|
sharee,
|
||||||
|
onCollectionUpdated: onCollectionUpdated,
|
||||||
|
);
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
25
app/lib/use_case/collection/unshare_collection.dart
Normal file
25
app/lib/use_case/collection/unshare_collection.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
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';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
|
|
||||||
|
class UnshareCollection {
|
||||||
|
const UnshareCollection(this._c);
|
||||||
|
|
||||||
|
/// Unshare the collection with a user
|
||||||
|
Future<CollectionShareResult> call(
|
||||||
|
Account account,
|
||||||
|
Collection collection,
|
||||||
|
CiString userId, {
|
||||||
|
required ValueChanged<Collection> onCollectionUpdated,
|
||||||
|
}) =>
|
||||||
|
CollectionAdapter.of(_c, account, collection).unshare(
|
||||||
|
userId,
|
||||||
|
onCollectionUpdated: onCollectionUpdated,
|
||||||
|
);
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ import 'package:nc_photos/widget/photo_list_item.dart';
|
||||||
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
|
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
|
||||||
import 'package:nc_photos/widget/selectable_item_list.dart';
|
import 'package:nc_photos/widget/selectable_item_list.dart';
|
||||||
import 'package:nc_photos/widget/selection_app_bar.dart';
|
import 'package:nc_photos/widget/selection_app_bar.dart';
|
||||||
|
import 'package:nc_photos/widget/share_collection_dialog.dart';
|
||||||
import 'package:nc_photos/widget/simple_input_dialog.dart';
|
import 'package:nc_photos/widget/simple_input_dialog.dart';
|
||||||
import 'package:nc_photos/widget/viewer.dart';
|
import 'package:nc_photos/widget/viewer.dart';
|
||||||
import 'package:nc_photos/widget/zoom_menu_button.dart';
|
import 'package:nc_photos/widget/zoom_menu_button.dart';
|
||||||
|
|
|
@ -16,6 +16,7 @@ class _AppBar extends StatelessWidget {
|
||||||
final canRename = adapter.isPermitted(CollectionCapability.rename);
|
final canRename = adapter.isPermitted(CollectionCapability.rename);
|
||||||
final canManualCover =
|
final canManualCover =
|
||||||
adapter.isPermitted(CollectionCapability.manualCover);
|
adapter.isPermitted(CollectionCapability.manualCover);
|
||||||
|
final canShare = adapter.isPermitted(CollectionCapability.share);
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
ZoomMenuButton(
|
ZoomMenuButton(
|
||||||
|
@ -26,6 +27,12 @@ class _AppBar extends StatelessWidget {
|
||||||
context.read<PrefController>().setAlbumBrowserZoomLevel(value);
|
context.read<PrefController>().setAlbumBrowserZoomLevel(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (canShare)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _onSharePressed(context),
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
tooltip: L10n.global().shareTooltip,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
if (state.items.isNotEmpty || canRename) {
|
if (state.items.isNotEmpty || canRename) {
|
||||||
actions.add(PopupMenuButton<_MenuOption>(
|
actions.add(PopupMenuButton<_MenuOption>(
|
||||||
|
@ -107,6 +114,17 @@ class _AppBar extends StatelessWidget {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onSharePressed(BuildContext context) async {
|
||||||
|
final bloc = context.read<_Bloc>();
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => ShareCollectionDialog(
|
||||||
|
account: bloc.account,
|
||||||
|
collection: bloc.state.collection,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppBarCover extends StatelessWidget {
|
class _AppBarCover extends StatelessWidget {
|
||||||
|
|
|
@ -15,8 +15,8 @@ import 'package:nc_photos/entity/sharee.dart';
|
||||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/use_case/share_album_with_user.dart';
|
import 'package:nc_photos/use_case/album/share_album_with_user.dart';
|
||||||
import 'package:nc_photos/use_case/unshare_album_with_user.dart';
|
import 'package:nc_photos/use_case/album/unshare_album_with_user.dart';
|
||||||
import 'package:nc_photos/widget/album_share_outlier_browser.dart';
|
import 'package:nc_photos/widget/album_share_outlier_browser.dart';
|
||||||
import 'package:nc_photos/widget/dialog_scaffold.dart';
|
import 'package:nc_photos/widget/dialog_scaffold.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
@ -240,7 +240,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
|
||||||
widget.account,
|
widget.account,
|
||||||
_album,
|
_album,
|
||||||
sharee,
|
sharee,
|
||||||
onShareFileFailed: (_) {
|
onShareFileFailed: (_, __, ___) {
|
||||||
hasFailure = true;
|
hasFailure = true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -279,7 +279,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
|
||||||
widget.account,
|
widget.account,
|
||||||
_album,
|
_album,
|
||||||
share.shareWith,
|
share.shareWith,
|
||||||
onUnshareFileFailed: (_) {
|
onUnshareFileFailed: (_, __, ___) {
|
||||||
hasFailure = true;
|
hasFailure = true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
237
app/lib/widget/share_collection_dialog.dart
Normal file
237
app/lib/widget/share_collection_dialog.dart
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:copy_with/copy_with.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
|
import 'package:kiwi/kiwi.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/controller/account_controller.dart';
|
||||||
|
import 'package:nc_photos/controller/collections_controller.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/collection.dart';
|
||||||
|
import 'package:nc_photos/entity/collection/util.dart';
|
||||||
|
import 'package:nc_photos/entity/sharee.dart';
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
import 'package:nc_photos/exception_event.dart';
|
||||||
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
import 'package:nc_photos/suggester.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_common/ci_string.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
|
part 'share_collection_dialog.g.dart';
|
||||||
|
part 'share_collection_dialog/bloc.dart';
|
||||||
|
part 'share_collection_dialog/state_event.dart';
|
||||||
|
|
||||||
|
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||||
|
|
||||||
|
/// Dialog to share a new collection to other user on the same server
|
||||||
|
///
|
||||||
|
/// Return the created collection, or null if user cancelled
|
||||||
|
class ShareCollectionDialog extends StatelessWidget {
|
||||||
|
const ShareCollectionDialog({
|
||||||
|
super.key,
|
||||||
|
required this.account,
|
||||||
|
required this.collection,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => _Bloc(
|
||||||
|
container: KiwiContainer().resolve<DiContainer>(),
|
||||||
|
account: account,
|
||||||
|
collectionsController:
|
||||||
|
context.read<AccountController>().collectionsController,
|
||||||
|
collection: collection,
|
||||||
|
),
|
||||||
|
child: const _WrappedShareCollectionDialog(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final Collection collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WrappedShareCollectionDialog extends StatefulWidget {
|
||||||
|
const _WrappedShareCollectionDialog();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _WrappedShareCollectionDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WrappedShareCollectionDialogState
|
||||||
|
extends State<_WrappedShareCollectionDialog> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_bloc.add(const _LoadSharee());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocListener(
|
||||||
|
listeners: [
|
||||||
|
BlocListener<_Bloc, _State>(
|
||||||
|
listenWhen: (previous, current) => previous.error != current.error,
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.error != null) {
|
||||||
|
if (state.error!.error is CollectionPartialShareException) {
|
||||||
|
// TODO localize string
|
||||||
|
SnackBarManager().showSnackBar(const SnackBar(
|
||||||
|
content: Text("Collection shared partially"),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content:
|
||||||
|
Text(exception_util.toUserString(state.error!.error)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: _BlocBuilder(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.collection != current.collection ||
|
||||||
|
previous.processingShares != current.processingShares,
|
||||||
|
builder: (context, state) {
|
||||||
|
final shares = {
|
||||||
|
...state.collection.shares,
|
||||||
|
...state.processingShares,
|
||||||
|
}.sortedBy((e) => e.username);
|
||||||
|
return SimpleDialog(
|
||||||
|
title: Text(L10n.global().shareAlbumDialogTitle),
|
||||||
|
children: [
|
||||||
|
...shares.map((s) => _ShareView(
|
||||||
|
share: s,
|
||||||
|
isProcessing: state.processingShares.contains(s),
|
||||||
|
onPressed: () {
|
||||||
|
_bloc.add(_Unshare(s));
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
const _ShareeInputView(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _bloc = context.read<_Bloc>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareeInputView extends StatefulWidget {
|
||||||
|
const _ShareeInputView();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ShareeInputViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareeInputViewState extends State<_ShareeInputView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocListener(
|
||||||
|
listeners: [
|
||||||
|
BlocListener<_Bloc, _State>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.shareeSuggester != current.shareeSuggester,
|
||||||
|
listener: (context, state) {
|
||||||
|
// search again
|
||||||
|
if (_lastPattern != null) {
|
||||||
|
_onSearch(_lastPattern!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: TypeAheadField<Sharee>(
|
||||||
|
textFieldConfiguration: TextFieldConfiguration(
|
||||||
|
controller: _textController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: L10n.global().addUserInputHint,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
direction: AxisDirection.up,
|
||||||
|
suggestionsCallback: _onSearch,
|
||||||
|
itemBuilder: (context, suggestion) => ListTile(
|
||||||
|
title: Text(suggestion.label),
|
||||||
|
subtitle: Text(suggestion.shareWith.toString()),
|
||||||
|
),
|
||||||
|
onSuggestionSelected: _onSuggestionSelected,
|
||||||
|
hideOnEmpty: true,
|
||||||
|
hideOnLoading: true,
|
||||||
|
autoFlipDirection: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Sharee> _onSearch(String pattern) {
|
||||||
|
_lastPattern = pattern;
|
||||||
|
final suggester = _bloc.state.shareeSuggester;
|
||||||
|
return suggester?.search(pattern.toCi()) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSuggestionSelected(Sharee sharee) {
|
||||||
|
_textController.clear();
|
||||||
|
_bloc.add(_Share(sharee));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _bloc = context.read<_Bloc>();
|
||||||
|
final _textController = TextEditingController();
|
||||||
|
|
||||||
|
String? _lastPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareView extends StatelessWidget {
|
||||||
|
const _ShareView({
|
||||||
|
required this.share,
|
||||||
|
required this.isProcessing,
|
||||||
|
this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Widget trailing;
|
||||||
|
if (isProcessing) {
|
||||||
|
trailing = const Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(end: 12),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
trailing = Checkbox(
|
||||||
|
value: true,
|
||||||
|
onChanged: (_) {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SimpleDialogOption(
|
||||||
|
onPressed: isProcessing ? null : onPressed,
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(share.username),
|
||||||
|
subtitle: Text(share.userId.toString()),
|
||||||
|
// pass through the tap event
|
||||||
|
trailing: IgnorePointer(
|
||||||
|
child: trailing,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CollectionShare share;
|
||||||
|
final bool isProcessing;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
}
|
116
app/lib/widget/share_collection_dialog.g.dart
Normal file
116
app/lib/widget/share_collection_dialog.g.dart
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'share_collection_dialog.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithLintRuleGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class $_StateCopyWithWorker {
|
||||||
|
_State call(
|
||||||
|
{Collection? collection,
|
||||||
|
List<CollectionShare>? processingShares,
|
||||||
|
List<Sharee>? sharees,
|
||||||
|
Suggester<Sharee>? shareeSuggester,
|
||||||
|
ExceptionEvent? error});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
|
_$_StateCopyWithWorkerImpl(this.that);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_State call(
|
||||||
|
{dynamic collection,
|
||||||
|
dynamic processingShares,
|
||||||
|
dynamic sharees = copyWithNull,
|
||||||
|
dynamic shareeSuggester = copyWithNull,
|
||||||
|
dynamic error = copyWithNull}) {
|
||||||
|
return _State(
|
||||||
|
collection: collection as Collection? ?? that.collection,
|
||||||
|
processingShares:
|
||||||
|
processingShares as List<CollectionShare>? ?? that.processingShares,
|
||||||
|
sharees:
|
||||||
|
sharees == copyWithNull ? that.sharees : sharees as List<Sharee>?,
|
||||||
|
shareeSuggester: shareeSuggester == copyWithNull
|
||||||
|
? that.shareeSuggester
|
||||||
|
: shareeSuggester as Suggester<Sharee>?,
|
||||||
|
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _State that;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $_StateCopyWith on _State {
|
||||||
|
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||||
|
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$_BlocNpLog on _Bloc {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log = Logger("widget.share_collection_dialog._Bloc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$_StateToString on _State {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_State {collection: $collection, processingShares: [length: ${processingShares.length}], sharees: ${sharees == null ? null : "[length: ${sharees!.length}]"}, shareeSuggester: $shareeSuggester, error: $error}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_UpdateCollectionToString on _UpdateCollection {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_UpdateCollection {collection: $collection}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_LoadShareeToString on _LoadSharee {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_LoadSharee {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_RefreshSuggesterToString on _RefreshSuggester {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_RefreshSuggester {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_ShareToString on _Share {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_Share {sharee: $sharee}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_UnshareToString on _Unshare {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_Unshare {share: $share}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_SetErrorToString on _SetError {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_SetError {error: $error, stackTrace: $stackTrace}";
|
||||||
|
}
|
||||||
|
}
|
147
app/lib/widget/share_collection_dialog/bloc.dart
Normal file
147
app/lib/widget/share_collection_dialog/bloc.dart
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
part of '../share_collection_dialog.dart';
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class _Bloc extends Bloc<_Event, _State> {
|
||||||
|
_Bloc({
|
||||||
|
required DiContainer container,
|
||||||
|
required this.account,
|
||||||
|
required this.collectionsController,
|
||||||
|
required Collection collection,
|
||||||
|
}) : _c = container,
|
||||||
|
super(_State.init(
|
||||||
|
collection: collection,
|
||||||
|
)) {
|
||||||
|
on<_UpdateCollection>(_onUpdateCollection);
|
||||||
|
on<_LoadSharee>(_onLoadSharee);
|
||||||
|
on<_RefreshSuggester>(_onRefreshSuggester);
|
||||||
|
|
||||||
|
on<_ShareEventTag>((ev, emit) {
|
||||||
|
if (ev is _Share) {
|
||||||
|
return _onShare(ev, emit);
|
||||||
|
} else if (ev is _Unshare) {
|
||||||
|
return _onUnshare(ev, emit);
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on<_SetError>(_onSetError);
|
||||||
|
|
||||||
|
_collectionControllerSubscription = collectionsController.stream.listen(
|
||||||
|
(event) {
|
||||||
|
final c = event.data
|
||||||
|
.firstWhere((d) => state.collection.compareIdentity(d.collection));
|
||||||
|
if (!identical(c, state.collection)) {
|
||||||
|
add(_UpdateCollection(c.collection));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (e, stackTrace) {
|
||||||
|
add(_SetError(e, stackTrace));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_collectionControllerSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChange(Change<_State> change) {
|
||||||
|
if (change.currentState.sharees != change.nextState.sharees ||
|
||||||
|
change.currentState.collection != change.nextState.collection ||
|
||||||
|
change.currentState.processingShares !=
|
||||||
|
change.nextState.processingShares) {
|
||||||
|
add(const _RefreshSuggester());
|
||||||
|
}
|
||||||
|
super.onChange(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(Object error, StackTrace stackTrace) {
|
||||||
|
add(_SetError(error, stackTrace));
|
||||||
|
super.onError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateCollection(_UpdateCollection ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(collection: ev.collection));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadSharee(_LoadSharee ev, Emitter<_State> emit) async {
|
||||||
|
_log.info(ev);
|
||||||
|
final sharees = await _c.shareeRepo.list(account);
|
||||||
|
emit(state.copyWith(sharees: sharees));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRefreshSuggester(_RefreshSuggester ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
final searchable = state.sharees
|
||||||
|
?.where((s) =>
|
||||||
|
!state.collection.shares.any((e) => e.userId == s.shareWith))
|
||||||
|
.where((s) =>
|
||||||
|
!state.processingShares.any((e) => e.userId == s.shareWith))
|
||||||
|
.where((s) => s.shareWith != account.userId)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
emit(state.copyWith(
|
||||||
|
shareeSuggester: Suggester<Sharee>(
|
||||||
|
items: searchable,
|
||||||
|
itemToKeywords: (item) => [item.shareWith, item.label.toCi()],
|
||||||
|
maxResult: 10,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onShare(_Share ev, Emitter<_State> emit) async {
|
||||||
|
_log.info(ev);
|
||||||
|
if (state.collection.shares.any((s) => s.userId == ev.sharee.shareWith) ||
|
||||||
|
state.processingShares.any((s) => s.userId == ev.sharee.shareWith)) {
|
||||||
|
_log.fine("[_onShare] Already shared with sharee: ${ev.sharee}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(state.copyWith(
|
||||||
|
processingShares: [
|
||||||
|
...state.processingShares,
|
||||||
|
CollectionShare(
|
||||||
|
userId: ev.sharee.shareWith,
|
||||||
|
username: ev.sharee.label,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
await collectionsController.share(state.collection, ev.sharee);
|
||||||
|
emit(state.copyWith(
|
||||||
|
processingShares: state.processingShares
|
||||||
|
.where((s) => s.userId != ev.sharee.shareWith)
|
||||||
|
.toList(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onUnshare(_Unshare ev, Emitter<_State> emit) async {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(
|
||||||
|
processingShares: [
|
||||||
|
...state.processingShares,
|
||||||
|
ev.share,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
await collectionsController.unshare(state.collection, ev.share.userId);
|
||||||
|
emit(state.copyWith(
|
||||||
|
processingShares: state.processingShares
|
||||||
|
.where((s) => s.userId != ev.share.userId)
|
||||||
|
.toList(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetError(_SetError ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
final Account account;
|
||||||
|
final CollectionsController collectionsController;
|
||||||
|
|
||||||
|
StreamSubscription? _collectionControllerSubscription;
|
||||||
|
}
|
96
app/lib/widget/share_collection_dialog/state_event.dart
Normal file
96
app/lib/widget/share_collection_dialog/state_event.dart
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
part of '../share_collection_dialog.dart';
|
||||||
|
|
||||||
|
@genCopyWith
|
||||||
|
@toString
|
||||||
|
class _State {
|
||||||
|
const _State({
|
||||||
|
required this.collection,
|
||||||
|
required this.processingShares,
|
||||||
|
this.sharees,
|
||||||
|
this.shareeSuggester,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory _State.init({
|
||||||
|
required Collection collection,
|
||||||
|
}) {
|
||||||
|
return _State(
|
||||||
|
collection: collection,
|
||||||
|
processingShares: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Collection collection;
|
||||||
|
final List<CollectionShare> processingShares;
|
||||||
|
|
||||||
|
final List<Sharee>? sharees;
|
||||||
|
final Suggester<Sharee>? shareeSuggester;
|
||||||
|
|
||||||
|
final ExceptionEvent? error;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Event {
|
||||||
|
const _Event();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _UpdateCollection implements _Event {
|
||||||
|
const _UpdateCollection(this.collection);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Collection collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _LoadSharee implements _Event {
|
||||||
|
const _LoadSharee();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _RefreshSuggester implements _Event {
|
||||||
|
const _RefreshSuggester();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin _ShareEventTag implements _Event {}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _Share with _ShareEventTag implements _Event {
|
||||||
|
const _Share(this.sharee);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Sharee sharee;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _Unshare with _ShareEventTag implements _Event {
|
||||||
|
const _Unshare(this.share);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final CollectionShare share;
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _SetError implements _Event {
|
||||||
|
const _SetError(this.error, [this.stackTrace]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Object error;
|
||||||
|
final StackTrace? stackTrace;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:kiwi/kiwi.dart';
|
import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:nc_photos/or_null.dart';
|
import 'package:nc_photos/or_null.dart';
|
||||||
import 'package:nc_photos/use_case/share_album_with_user.dart';
|
import 'package:nc_photos/use_case/album/share_album_with_user.dart';
|
||||||
import 'package:np_common/ci_string.dart';
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:kiwi/kiwi.dart';
|
import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/use_case/unshare_album_with_user.dart';
|
import 'package:nc_photos/use_case/album/unshare_album_with_user.dart';
|
||||||
import 'package:np_common/ci_string.dart';
|
import 'package:np_common/ci_string.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue