mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +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/di_container.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/util.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/rx_extension.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/list_collection.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_common/ci_string.dart';
|
||||
import 'package:np_common/type.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 {
|
||||
var lastData = const CollectionStreamEvent(
|
||||
data: [],
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:copy_with/copy_with.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/util.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
@ -40,6 +41,9 @@ class Collection with EquatableMixin {
|
|||
/// See [CollectionContentProvider.itemSort]
|
||||
CollectionItemSort get itemSort => contentProvider.itemSort;
|
||||
|
||||
/// See [CollectionContentProvider.sharees]
|
||||
List<CollectionShare> get shares => contentProvider.shares;
|
||||
|
||||
/// See [CollectionContentProvider.getCoverUrl]
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
|
@ -77,6 +81,8 @@ enum CollectionCapability {
|
|||
labelItem,
|
||||
// set the cover image
|
||||
manualCover,
|
||||
// share the collection with other user on the same server
|
||||
share,
|
||||
}
|
||||
|
||||
/// Provide the actual content of a collection
|
||||
|
@ -106,6 +112,10 @@ abstract class CollectionContentProvider with EquatableMixin {
|
|||
/// Return the sort type
|
||||
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
|
||||
///
|
||||
/// 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/person.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/new_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.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:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
abstract class CollectionAdapter {
|
||||
|
@ -71,6 +74,18 @@ abstract class CollectionAdapter {
|
|||
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
|
||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original);
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import 'package:flutter/foundation.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/collection_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/util.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:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
/// A read-only collection that does not support modifying its items
|
||||
mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
||||
mixin CollectionAdapterReadOnlyTag implements CollectionAdapter {
|
||||
@override
|
||||
Future<int> addFiles(
|
||||
List<FileDescriptor> files, {
|
||||
|
@ -48,3 +51,28 @@ mixin CollectionReadOnlyAdapter implements CollectionAdapter {
|
|||
Future<Collection?> updatePostLoad(List<CollectionItem> items) =>
|
||||
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:logging/logging.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/entity/album/cover_provider.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/builder.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/album_item_adapter.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/file.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/object_extension.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/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/preprocess_album.dart';
|
||||
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.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
|
||||
Future<CollectionItem> adaptToNewItem(NewCollectionItem original) async {
|
||||
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/entity/collection.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_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';
|
||||
|
||||
class CollectionLocationGroupAdapter
|
||||
with CollectionReadOnlyAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionLocationGroupAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
|
@ -43,11 +46,6 @@ class CollectionLocationGroupAdapter
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability 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/entity/collection.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_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';
|
||||
|
||||
class CollectionMemoryAdapter
|
||||
with CollectionReadOnlyAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionMemoryAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
|
@ -42,11 +45,6 @@ class CollectionMemoryAdapter
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability 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/entity/collection.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_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';
|
||||
|
||||
@npLog
|
||||
class CollectionNcAlbumAdapter implements CollectionAdapter {
|
||||
class CollectionNcAlbumAdapter
|
||||
with CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionNcAlbumAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
_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/entity/collection.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_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';
|
||||
|
||||
class CollectionPersonAdapter
|
||||
with CollectionReadOnlyAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionPersonAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
|
@ -45,11 +48,6 @@ class CollectionPersonAdapter
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability 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/entity/collection.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_item.dart';
|
||||
import 'package:nc_photos/entity/collection_item/basic_item.dart';
|
||||
import 'package:nc_photos/use_case/list_tagged_file.dart';
|
||||
|
||||
class CollectionTagAdapter
|
||||
with CollectionReadOnlyAdapter
|
||||
with
|
||||
CollectionAdapterReadOnlyTag,
|
||||
CollectionAdapterUnremovableTag,
|
||||
CollectionAdapterUnshareableTag
|
||||
implements CollectionAdapter {
|
||||
CollectionTagAdapter(this._c, this.account, this.collection)
|
||||
: assert(require(_c)),
|
||||
|
@ -32,11 +35,6 @@ class CollectionTagAdapter
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() {
|
||||
throw UnsupportedError("Operation not supported");
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPermitted(CollectionCapability 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/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:to_string/to_string.dart';
|
||||
|
||||
|
@ -52,6 +53,7 @@ class CollectionAlbumProvider
|
|||
CollectionCapability.manualItem,
|
||||
CollectionCapability.manualSort,
|
||||
CollectionCapability.labelItem,
|
||||
CollectionCapability.share,
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -66,6 +68,17 @@ class CollectionAlbumProvider
|
|||
@override
|
||||
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
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
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/use_case/list_location_group.dart';
|
||||
|
||||
|
@ -31,6 +32,9 @@ class CollectionLocationGroupProvider
|
|||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
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/file_descriptor.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
|
@ -39,6 +40,9 @@ class CollectionMemoryProvider
|
|||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:equatable/equatable.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
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/nc_album.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
@ -45,6 +46,9 @@ class CollectionNcAlbumProvider
|
|||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
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/person.dart';
|
||||
|
||||
|
@ -34,6 +35,9 @@ class CollectionPersonProvider
|
|||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:clock/clock.dart';
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:nc_photos/account.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/tag.dart';
|
||||
|
||||
|
@ -31,6 +32,9 @@ class CollectionTagProvider
|
|||
@override
|
||||
CollectionItemSort get itemSort => CollectionItemSort.dateDescending;
|
||||
|
||||
@override
|
||||
List<CollectionShare> get shares => [];
|
||||
|
||||
@override
|
||||
String? getCoverUrl(
|
||||
int width,
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.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';
|
||||
|
||||
part 'util.g.dart';
|
||||
|
||||
enum CollectionSort {
|
||||
dateDescending,
|
||||
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> {
|
||||
List<Collection> sortedBy(CollectionSort by) {
|
||||
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;
|
||||
}
|
||||
|
||||
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/share.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/remove.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: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/exception.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/unshare_file_from_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: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:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
part 'share_album_with_user.g.dart';
|
||||
|
||||
|
@ -24,7 +25,7 @@ class ShareAlbumWithUser {
|
|||
Account account,
|
||||
Album album,
|
||||
Sharee sharee, {
|
||||
void Function(File)? onShareFileFailed,
|
||||
ErrorWithValueHandler<File>? onShareFileFailed,
|
||||
}) async {
|
||||
assert(album.provider is AlbumStaticProvider);
|
||||
final newShares = (album.shares ?? [])
|
||||
|
@ -55,7 +56,7 @@ class ShareAlbumWithUser {
|
|||
Account account,
|
||||
Album album,
|
||||
CiString shareWith, {
|
||||
void Function(File)? onShareFileFailed,
|
||||
ErrorWithValueHandler<File>? onShareFileFailed,
|
||||
}) async {
|
||||
final files = AlbumStaticProvider.of(album)
|
||||
.items
|
||||
|
@ -70,7 +71,7 @@ class ShareAlbumWithUser {
|
|||
"[_createFileShares] Failed sharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
|
||||
e,
|
||||
stackTrace);
|
||||
onShareFileFailed?.call(album.albumFile!);
|
||||
onShareFileFailed?.call(album.albumFile!, e, stackTrace);
|
||||
}
|
||||
for (final f in files) {
|
||||
_log.info("[_createFileShares] Sharing '${f.path}' with '$shareWith'");
|
||||
|
@ -81,7 +82,7 @@ class ShareAlbumWithUser {
|
|||
"[_createFileShares] Failed sharing file '${logFilename(f.path)}' with '$shareWith'",
|
||||
e,
|
||||
stackTrace);
|
||||
onShareFileFailed?.call(f);
|
||||
onShareFileFailed?.call(f, e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,5 +11,5 @@ extension _$ShareAlbumWithUserNpLog on ShareAlbumWithUser {
|
|||
Logger get _log => 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/share.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/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:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
part 'unshare_album_with_user.g.dart';
|
||||
|
||||
|
@ -31,7 +32,7 @@ class UnshareAlbumWithUser {
|
|||
Account account,
|
||||
Album album,
|
||||
CiString shareWith, {
|
||||
void Function(Share)? onUnshareFileFailed,
|
||||
ErrorWithValueHandler<Share>? onUnshareFileFailed,
|
||||
}) async {
|
||||
assert(album.provider is AlbumStaticProvider);
|
||||
// remove the share from album file
|
||||
|
@ -59,7 +60,7 @@ class UnshareAlbumWithUser {
|
|||
Account account,
|
||||
Album album,
|
||||
CiString shareWith, {
|
||||
void Function(Share)? onUnshareFileFailed,
|
||||
ErrorWithValueHandler<Share>? onUnshareFileFailed,
|
||||
}) async {
|
||||
// remove share from the album file
|
||||
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'",
|
||||
e,
|
||||
stackTrace);
|
||||
onUnshareFileFailed?.call(s);
|
||||
onUnshareFileFailed?.call(s, e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,5 +11,5 @@ extension _$UnshareAlbumWithUserNpLog on UnshareAlbumWithUser {
|
|||
Logger get _log => 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:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
part 'unshare_file_from_album.g.dart';
|
||||
|
||||
|
@ -35,7 +36,7 @@ class UnshareFileFromAlbum {
|
|||
Album album,
|
||||
List<File> files,
|
||||
List<CiString> unshareWith, {
|
||||
void Function(Share)? onUnshareFileFailed,
|
||||
ErrorWithValueHandler<Share>? onUnshareFileFailed,
|
||||
}) async {
|
||||
_log.info(
|
||||
"[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,
|
||||
void Function(Share)? onUnshareFileFailed) async {
|
||||
ErrorWithValueHandler<Share>? onUnshareFileFailed) async {
|
||||
for (final s in shares) {
|
||||
try {
|
||||
await RemoveShare(_c.shareRepo)(account, s);
|
||||
} catch (e, stackTrace) {
|
||||
_log.severe(
|
||||
"[_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;
|
||||
|
||||
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/selectable_item_list.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/viewer.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 canManualCover =
|
||||
adapter.isPermitted(CollectionCapability.manualCover);
|
||||
final canShare = adapter.isPermitted(CollectionCapability.share);
|
||||
|
||||
final actions = <Widget>[
|
||||
ZoomMenuButton(
|
||||
|
@ -26,6 +27,12 @@ class _AppBar extends StatelessWidget {
|
|||
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) {
|
||||
actions.add(PopupMenuButton<_MenuOption>(
|
||||
|
@ -107,6 +114,17 @@ class _AppBar extends StatelessWidget {
|
|||
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 {
|
||||
|
|
|
@ -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/k.dart' as k;
|
||||
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/unshare_album_with_user.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/widget/album_share_outlier_browser.dart';
|
||||
import 'package:nc_photos/widget/dialog_scaffold.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
@ -240,7 +240,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
|
|||
widget.account,
|
||||
_album,
|
||||
sharee,
|
||||
onShareFileFailed: (_) {
|
||||
onShareFileFailed: (_, __, ___) {
|
||||
hasFailure = true;
|
||||
},
|
||||
);
|
||||
|
@ -279,7 +279,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
|
|||
widget.account,
|
||||
_album,
|
||||
share.shareWith,
|
||||
onUnshareFileFailed: (_) {
|
||||
onUnshareFileFailed: (_, __, ___) {
|
||||
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:kiwi/kiwi.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:test/test.dart';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:kiwi/kiwi.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:test/test.dart';
|
||||
|
||||
|
|
Loading…
Reference in a new issue