Regression: share and unshare album

This commit is contained in:
Ming Ming 2023-05-02 01:05:33 +08:00
parent ec8e9efa6f
commit d2886e55c1
40 changed files with 1032 additions and 52 deletions

View file

@ -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: [],

View file

@ -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

View file

@ -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);

View file

@ -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");
}
}

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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) {

View 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}";
}
}

View file

@ -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
View 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
View 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");
}

View file

@ -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';

View file

@ -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';

View file

@ -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);
}
}
}

View file

@ -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");
}

View file

@ -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);
}
}

View file

@ -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");
}

View file

@ -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);
}
}
}

View file

@ -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");
}

View 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;
}

View 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;
}

View file

@ -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';

View file

@ -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 {

View file

@ -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;
},
);

View 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;
}

View 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}";
}
}

View 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;
}

View 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;
}

View file

@ -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';

View file

@ -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';