Revamp how share is managed for album

Shares are now stored in the album json, such that users other than the album owner are aware of the shares
This commit is contained in:
Ming Ming 2021-11-12 01:03:36 +08:00
parent b9655c66c2
commit 6e9a34342a
12 changed files with 598 additions and 259 deletions

View file

@ -1,20 +1,85 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart'; import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/use_case/list_share.dart'; import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/list_sharee.dart';
class ListAlbumShareOutlierItem { class ListAlbumShareOutlierItem with EquatableMixin {
const ListAlbumShareOutlierItem(this.file, this.shares); const ListAlbumShareOutlierItem(this.file, this.shareItems);
@override
toString() {
return "$runtimeType {"
"file: '${file.path}', "
"shareItems: ${shareItems.toReadableString()}, "
"}";
}
@override
get props => [
file,
shareItems,
];
final File file; final File file;
final List<Share> shares; final List<ListAlbumShareOutlierShareItem> shareItems;
}
abstract class ListAlbumShareOutlierShareItem with EquatableMixin {
const ListAlbumShareOutlierShareItem();
}
class ListAlbumShareOutlierExtraShareItem
extends ListAlbumShareOutlierShareItem {
const ListAlbumShareOutlierExtraShareItem(this.share);
@override
toString() {
return "$runtimeType {"
"share: $share, "
"}";
}
@override
get props => [
share,
];
final Share share;
}
class ListAlbumShareOutlierMissingShareItem
extends ListAlbumShareOutlierShareItem {
const ListAlbumShareOutlierMissingShareItem(
this.shareWith, this.shareWithDisplayName);
@override
toString() {
return "$runtimeType {"
"shareWith: $shareWith, "
"shareWithDisplayName: $shareWithDisplayName, "
"}";
}
@override
get props => [
shareWith,
shareWithDisplayName,
];
final String shareWith;
final String? shareWithDisplayName;
} }
abstract class ListAlbumShareOutlierBlocEvent { abstract class ListAlbumShareOutlierBlocEvent {
@ -36,47 +101,47 @@ class ListAlbumShareOutlierBlocQuery extends ListAlbumShareOutlierBlocEvent {
final Album album; final Album album;
} }
abstract class ListAlbumShareOutlierBlocState { abstract class ListAlbumShareOutlierBlocState with EquatableMixin {
const ListAlbumShareOutlierBlocState( const ListAlbumShareOutlierBlocState(this.account, this.items);
this.account, this.albumShares, this.items);
@override @override
toString() { toString() {
return "$runtimeType {" return "$runtimeType {"
"account: $account, " "account: $account, "
"albumShares: List {length: ${albumShares.length}}, " "items: ${items.toReadableString()}, "
"items: List {length: ${items.length}}, "
"}"; "}";
} }
@override
get props => [
account,
items,
];
final Account? account; final Account? account;
final List<Share> albumShares;
final List<ListAlbumShareOutlierItem> items; final List<ListAlbumShareOutlierItem> items;
} }
class ListAlbumShareOutlierBlocInit extends ListAlbumShareOutlierBlocState { class ListAlbumShareOutlierBlocInit extends ListAlbumShareOutlierBlocState {
ListAlbumShareOutlierBlocInit() : super(null, const [], const []); ListAlbumShareOutlierBlocInit() : super(null, const []);
} }
class ListAlbumShareOutlierBlocLoading extends ListAlbumShareOutlierBlocState { class ListAlbumShareOutlierBlocLoading extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocLoading(Account? account, const ListAlbumShareOutlierBlocLoading(
List<Share> albumShares, List<ListAlbumShareOutlierItem> items) Account? account, List<ListAlbumShareOutlierItem> items)
: super(account, albumShares, items); : super(account, items);
} }
class ListAlbumShareOutlierBlocSuccess extends ListAlbumShareOutlierBlocState { class ListAlbumShareOutlierBlocSuccess extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocSuccess(Account? account, const ListAlbumShareOutlierBlocSuccess(
List<Share> albumShares, List<ListAlbumShareOutlierItem> items) Account? account, List<ListAlbumShareOutlierItem> items)
: super(account, albumShares, items); : super(account, items);
} }
class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState { class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocFailure( const ListAlbumShareOutlierBlocFailure(
Account? account, Account? account, List<ListAlbumShareOutlierItem> items, this.exception)
List<Share> albumShares, : super(account, items);
List<ListAlbumShareOutlierItem> items,
this.exception)
: super(account, albumShares, items);
@override @override
toString() { toString() {
@ -86,6 +151,12 @@ class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState {
"}"; "}";
} }
@override
get props => [
...super.props,
exception,
];
final dynamic exception; final dynamic exception;
} }
@ -95,7 +166,7 @@ class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState {
/// belongs, e.g., an unshared item in a shared album, or vice versa /// belongs, e.g., an unshared item in a shared album, or vice versa
class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent, class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
ListAlbumShareOutlierBlocState> { ListAlbumShareOutlierBlocState> {
ListAlbumShareOutlierBloc(this.shareRepo) ListAlbumShareOutlierBloc(this.shareRepo, this.shareeRepo)
: super(ListAlbumShareOutlierBlocInit()); : super(ListAlbumShareOutlierBlocInit());
@override @override
@ -110,54 +181,150 @@ class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
ListAlbumShareOutlierBlocQuery ev) async* { ListAlbumShareOutlierBlocQuery ev) async* {
try { try {
assert(ev.album.provider is AlbumStaticProvider); assert(ev.album.provider is AlbumStaticProvider);
yield ListAlbumShareOutlierBlocLoading( yield ListAlbumShareOutlierBlocLoading(ev.account, state.items);
ev.account, state.albumShares, state.items);
final albumShares = final albumShares = await () async {
(await ListShare(shareRepo)(ev.account, ev.album.albumFile!)) var temp = (ev.album.shares ?? [])
.where((element) => element.shareWith != null) .where((s) => !s.userId.equalsIgnoreCase(ev.account.username))
.sorted((a, b) => a.shareWith!.compareTo(b.shareWith!)); .toList();
final albumSharees = albumShares if (ev.album.albumFile!.ownerId != ev.account.username) {
.where((element) => element.shareType == ShareType.user) // add owner if the album is not owned by this account
.map((e) => e.shareWith!) final ownerSharee = (await ListSharee(shareeRepo)(ev.account))
.sorted(); .firstWhere((s) => s.shareWith == ev.album.albumFile!.ownerId);
final files = AlbumStaticProvider.of(ev.album) temp.add(AlbumShare(
.items userId: ownerSharee.shareWith,
.whereType<AlbumFileItem>() displayName: ownerSharee.shareWithDisplayNameUnique,
.map((e) => e.file) ));
.toList();
final products = <ListAlbumShareOutlierItem>[];
Object? error;
for (final f in files) {
try {
final shares = (await ListShare(shareRepo)(ev.account, f))
.where((element) => element.shareType == ShareType.user)
.toList();
final sharees = shares.map((e) => e.shareWith!).sorted();
if (!listEquals(sharees, albumSharees)) {
products.add(ListAlbumShareOutlierItem(f, shares));
}
} catch (e, stackTrace) {
_log.severe("[_query] Failed while listing share for file: ${f.path}",
e, stackTrace);
error = e;
} }
} return Map.fromEntries(
if (error == null) { temp.map((as) => MapEntry(as.userId.toLowerCase(), as)));
yield ListAlbumShareOutlierBlocSuccess( }();
ev.account, albumShares, products); final albumSharees =
albumShares.values.map((s) => s.userId.toLowerCase()).toSet();
final products = <ListAlbumShareOutlierItem>[];
final errors = <Object>[];
products.addAll(await _processAlbumFile(
ev.account, ev.album, albumShares, albumSharees, errors));
products.addAll(await _processAlbumItems(
ev.account, ev.album, albumShares, albumSharees, errors));
if (errors.isEmpty) {
yield ListAlbumShareOutlierBlocSuccess(ev.account, products);
} else { } else {
yield ListAlbumShareOutlierBlocFailure( yield ListAlbumShareOutlierBlocFailure(
ev.account, albumShares, products, error); ev.account, products, errors.first);
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.severe("[_onEventQuery] Exception while request", e, stackTrace); _log.severe("[_onEventQuery] Exception while request", e, stackTrace);
yield ListAlbumShareOutlierBlocFailure( yield ListAlbumShareOutlierBlocFailure(ev.account, state.items, e);
ev.account, state.albumShares, state.items, e); }
}
Future<List<ListAlbumShareOutlierItem>> _processAlbumFile(
Account account,
Album album,
Map<String, AlbumShare> albumShares,
Set<String> albumSharees,
List<Object> errors,
) async {
try {
final item = await _processSingleFile(
account, album.albumFile!, albumShares, albumSharees, errors);
return item == null ? [] : [item];
} catch (e, stackTrace) {
_log.severe(
"[_processAlbumFile] Failed while _processSingleFile: ${logFilename(album.albumFile?.path)}",
e,
stackTrace);
errors.add(e);
return [];
}
}
Future<List<ListAlbumShareOutlierItem>> _processAlbumItems(
Account account,
Album album,
Map<String, AlbumShare> albumShares,
Set<String> albumSharees,
List<Object> errors,
) async {
final products = <ListAlbumShareOutlierItem>[];
final files = AlbumStaticProvider.of(album)
.items
.whereType<AlbumFileItem>()
.map((e) => e.file)
.toList();
for (final f in files) {
try {
(await _processSingleFile(
account, f, albumShares, albumSharees, errors))
?.apply((item) {
products.add(item);
});
} catch (e, stackTrace) {
_log.severe(
"[_processAlbumItems] Failed while _processSingleFile: ${logFilename(f.path)}",
e,
stackTrace);
errors.add(e);
}
}
return products;
}
Future<ListAlbumShareOutlierItem?> _processSingleFile(
Account account,
File file,
Map<String, AlbumShare> albumShares,
Set<String> albumSharees,
List<Object> errors,
) async {
final shareItems = <ListAlbumShareOutlierShareItem>[];
final shares = (await ListShare(shareRepo)(account, file))
.where((element) => element.shareType == ShareType.user)
.toList();
final sharees = shares.map((s) => s.shareWith!.toLowerCase()).toSet();
final missings = albumSharees.difference(sharees);
_log.info(
"Missing shares: ${missings.toReadableString()} for file: ${logFilename(file.path)}");
for (final m in missings) {
try {
final as = albumShares[m.toLowerCase()]!;
shareItems.add(
ListAlbumShareOutlierMissingShareItem(as.userId, as.displayName));
} catch (e, stackTrace) {
_log.severe(
"[_processSingleFile] Failed while processing missing share for file: ${logFilename(file.path)}",
e,
stackTrace);
errors.add(e);
}
}
final extras = sharees.difference(albumSharees);
_log.info(
"Extra shares: ${extras.toReadableString()} for file: ${logFilename(file.path)}");
for (final e in extras) {
try {
shareItems.add(ListAlbumShareOutlierExtraShareItem(shares
.firstWhere((s) => s.shareWith?.equalsIgnoreCase(e) == true)));
} catch (e, stackTrace) {
_log.severe(
"[_processSingleFile] Failed while processing extra share for file: ${logFilename(file.path)}",
e,
stackTrace);
errors.add(e);
}
}
if (shareItems.isNotEmpty) {
return ListAlbumShareOutlierItem(file, shareItems);
} else {
return null;
} }
} }
final ShareRepo shareRepo; final ShareRepo shareRepo;
final ShareeRepo shareeRepo;
static final _log = static final _log =
Logger("bloc.list_album_share_outlier.ListAlbumShareOutlierBloc"); Logger("bloc.list_album_share_outlier.ListAlbumShareOutlierBloc");

View file

@ -18,6 +18,7 @@ import 'package:nc_photos/int_util.dart' as int_util;
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/type.dart'; import 'package:nc_photos/type.dart';
import 'package:nc_photos/use_case/get_file_binary.dart'; import 'package:nc_photos/use_case/get_file_binary.dart';
import 'package:nc_photos/use_case/ls.dart'; import 'package:nc_photos/use_case/ls.dart';
@ -36,6 +37,7 @@ class Album with EquatableMixin {
required this.provider, required this.provider,
required this.coverProvider, required this.coverProvider,
required this.sortProvider, required this.sortProvider,
this.shares,
this.albumFile, this.albumFile,
}) : lastUpdated = (lastUpdated ?? DateTime.now()).toUtc(); }) : lastUpdated = (lastUpdated ?? DateTime.now()).toUtc();
@ -95,6 +97,9 @@ class Album with EquatableMixin {
result["coverProvider"].cast<String, dynamic>()), result["coverProvider"].cast<String, dynamic>()),
sortProvider: AlbumSortProvider.fromJson( sortProvider: AlbumSortProvider.fromJson(
result["sortProvider"].cast<String, dynamic>()), result["sortProvider"].cast<String, dynamic>()),
shares: (result["shares"] as List?)
?.map((e) => AlbumShare.fromJson(e.cast<String, dynamic>()))
.toList(),
albumFile: result["albumFile"] == null albumFile: result["albumFile"] == null
? null ? null
: File.fromJson(result["albumFile"].cast<String, dynamic>()), : File.fromJson(result["albumFile"].cast<String, dynamic>()),
@ -109,6 +114,7 @@ class Album with EquatableMixin {
"provider: ${provider.toString(isDeep: isDeep)}, " "provider: ${provider.toString(isDeep: isDeep)}, "
"coverProvider: $coverProvider, " "coverProvider: $coverProvider, "
"sortProvider: $sortProvider, " "sortProvider: $sortProvider, "
"shares: ${shares?.toReadableString()}, "
"albumFile: $albumFile, " "albumFile: $albumFile, "
"}"; "}";
} }
@ -124,6 +130,7 @@ class Album with EquatableMixin {
AlbumProvider? provider, AlbumProvider? provider,
AlbumCoverProvider? coverProvider, AlbumCoverProvider? coverProvider,
AlbumSortProvider? sortProvider, AlbumSortProvider? sortProvider,
OrNull<List<AlbumShare>>? shares,
OrNull<File>? albumFile, OrNull<File>? albumFile,
}) { }) {
return Album( return Album(
@ -133,6 +140,7 @@ class Album with EquatableMixin {
provider: provider ?? this.provider, provider: provider ?? this.provider,
coverProvider: coverProvider ?? this.coverProvider, coverProvider: coverProvider ?? this.coverProvider,
sortProvider: sortProvider ?? this.sortProvider, sortProvider: sortProvider ?? this.sortProvider,
shares: shares == null ? this.shares : shares.obj,
albumFile: albumFile == null ? this.albumFile : albumFile.obj, albumFile: albumFile == null ? this.albumFile : albumFile.obj,
); );
} }
@ -145,6 +153,7 @@ class Album with EquatableMixin {
"provider": provider.toJson(), "provider": provider.toJson(),
"coverProvider": coverProvider.toJson(), "coverProvider": coverProvider.toJson(),
"sortProvider": sortProvider.toJson(), "sortProvider": sortProvider.toJson(),
if (shares != null) "shares": shares!.map((e) => e.toJson()).toList(),
// ignore albumFile // ignore albumFile
}; };
} }
@ -157,6 +166,7 @@ class Album with EquatableMixin {
"provider": provider.toJson(), "provider": provider.toJson(),
"coverProvider": coverProvider.toJson(), "coverProvider": coverProvider.toJson(),
"sortProvider": sortProvider.toJson(), "sortProvider": sortProvider.toJson(),
if (shares != null) "shares": shares!.map((e) => e.toJson()).toList(),
if (albumFile != null) "albumFile": albumFile!.toJson(), if (albumFile != null) "albumFile": albumFile!.toJson(),
}; };
} }
@ -168,6 +178,7 @@ class Album with EquatableMixin {
provider, provider,
coverProvider, coverProvider,
sortProvider, sortProvider,
shares,
albumFile, albumFile,
]; ];
@ -177,6 +188,7 @@ class Album with EquatableMixin {
final AlbumProvider provider; final AlbumProvider provider;
final AlbumCoverProvider coverProvider; final AlbumCoverProvider coverProvider;
final AlbumSortProvider sortProvider; final AlbumSortProvider sortProvider;
final List<AlbumShare>? shares;
/// How is this album stored on server /// How is this album stored on server
/// ///
@ -187,6 +199,50 @@ class Album with EquatableMixin {
static const version = 6; static const version = 6;
} }
class AlbumShare {
const AlbumShare({
required this.userId,
this.displayName,
});
factory AlbumShare.fromJson(JsonObj json) {
return AlbumShare(
userId: json["userId"],
displayName: json["displayName"],
);
}
JsonObj toJson() {
return {
"userId": userId,
if (displayName != null) "displayName": displayName,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AlbumShare &&
runtimeType == other.runtimeType &&
userId.equalsIgnoreCase(other.userId);
}
@override
get hashCode => userId.toLowerCase().hashCode;
@override
toString() {
return "$runtimeType {"
"userId: $userId, "
"displayName: $displayName, "
"}";
}
/// User ID or username, case insensitive
final String userId;
final String? displayName;
}
class AlbumRepo { class AlbumRepo {
AlbumRepo(this.dataSrc); AlbumRepo(this.dataSrc);

View file

@ -0,0 +1,6 @@
extension ObjectExtension<T> on T {
T apply(void Function(T obj) fn) {
fn(this);
return this;
}
}

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/use_case/create_share.dart'; import 'package:nc_photos/use_case/create_share.dart';
import 'package:nc_photos/use_case/list_share.dart'; import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/preprocess_album.dart'; import 'package:nc_photos/use_case/preprocess_album.dart';
@ -55,44 +56,39 @@ class AddToAlbum {
Future<void> _shareFiles( Future<void> _shareFiles(
Account account, Album album, List<File> files) async { Account account, Album album, List<File> files) async {
try { if (album.shares?.isNotEmpty != true) {
final albumShares = return;
(await ListShare(shareRepo)(account, album.albumFile!)) }
.where((element) => element.shareType == ShareType.user) final albumShares = (album.shares!.map((e) => e.userId).toList()
.map((e) => e.shareWith!) ..add(album.albumFile!.ownerId ?? account.username))
.toSet(); .where((element) => !element.equalsIgnoreCase(account.username))
if (albumShares.isEmpty) { .toSet();
return; if (albumShares.isEmpty) {
} return;
for (final f in files) { }
try { for (final f in files) {
final fileShares = (await ListShare(shareRepo)(account, f)) try {
.where((element) => element.shareType == ShareType.user) final fileShares = (await ListShare(shareRepo)(account, f))
.map((e) => e.shareWith!) .where((element) => element.shareType == ShareType.user)
.toSet(); .map((e) => e.shareWith!)
final diffShares = albumShares.difference(fileShares); .toSet();
for (final s in diffShares) { final diffShares = albumShares.difference(fileShares);
try { for (final s in diffShares) {
await CreateUserShare(shareRepo)(account, f, s); try {
} catch (e, stackTrace) { await CreateUserShare(shareRepo)(account, f, s);
_log.shout( } catch (e, stackTrace) {
"[_shareFiles] Failed while CreateUserShare: ${logFilename(f.path)}", _log.shout(
e, "[_shareFiles] Failed while CreateUserShare: ${logFilename(f.path)}",
stackTrace); e,
} stackTrace);
} }
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while listing shares: ${logFilename(f.path)}",
e,
stackTrace);
} }
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while listing shares: ${logFilename(f.path)}",
e,
stackTrace);
} }
} catch (e, stackTrace) {
_log.shout(
"[_shareFiles] Failed while listing album shares: ${logFilename(album.albumFile?.path)}",
e,
stackTrace);
} }
} }

View file

@ -1,56 +0,0 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/list_dir_share.dart';
import 'package:nc_photos/use_case/ls.dart';
class ListSharedAlbumItem {
const ListSharedAlbumItem(this.share, this.album);
final Share share;
final Album album;
}
class ListSharedAlbum {
const ListSharedAlbum(this.shareRepo, this.fileRepo, this.albumRepo);
/// List albums that are currently shared by you
///
/// If [whereShareWith] is not null, only shares sharing with [whereShareWith]
/// will be returned.
Future<List<ListSharedAlbumItem>> call(
Account account, {
String? whereShareWith,
}) async {
final shareItems = await ListDirShare(shareRepo)(
account, File(path: remote_storage_util.getRemoteAlbumsDir(account)));
final files = await Ls(fileRepo)(
account,
File(path: remote_storage_util.getRemoteAlbumsDir(account)),
);
final products = <ListSharedAlbumItem>[];
for (final si in shareItems) {
// find the file
final albumFile =
files.firstWhere((element) => element.compareServerIdentity(si.file));
final album = await albumRepo.get(
account,
albumFile,
);
for (final s in si.shares) {
if (whereShareWith != null && s.shareWith != whereShareWith) {
continue;
}
products.add(ListSharedAlbumItem(s, album));
}
}
return products;
}
final ShareRepo shareRepo;
final FileRepo fileRepo;
final AlbumRepo albumRepo;
}

View file

@ -9,7 +9,7 @@ import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/use_case/list_share.dart'; import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/use_case/preprocess_album.dart'; import 'package:nc_photos/use_case/preprocess_album.dart';
import 'package:nc_photos/use_case/unshare_file_from_album.dart'; import 'package:nc_photos/use_case/unshare_file_from_album.dart';
import 'package:nc_photos/use_case/update_album.dart'; import 'package:nc_photos/use_case/update_album.dart';
@ -43,15 +43,7 @@ class RemoveFromAlbum {
final removeFiles = final removeFiles =
items.whereType<AlbumFileItem>().map((e) => e.file).toList(); items.whereType<AlbumFileItem>().map((e) => e.file).toList();
if (removeFiles.isNotEmpty) { if (removeFiles.isNotEmpty) {
final albumShares = await _unshareFiles(account, newAlbum, removeFiles);
(await ListShare(shareRepo)(account, newAlbum.albumFile!))
.where((element) => element.shareType == ShareType.user)
.map((e) => e.shareWith!)
.toList();
if (albumShares.isNotEmpty) {
await UnshareFileFromAlbum(shareRepo, fileRepo, albumRepo)(
account, newAlbum, removeFiles, albumShares);
}
} }
} }
@ -96,6 +88,21 @@ class RemoveFromAlbum {
return newAlbum; return newAlbum;
} }
Future<void> _unshareFiles(
Account account, Album album, List<File> files) async {
if (album.shares?.isNotEmpty != true) {
return;
}
final albumShares = (album.shares!.map((e) => e.userId).toList()
..add(album.albumFile!.ownerId ?? account.username))
.where((element) => !element.equalsIgnoreCase(account.username))
.toList();
if (albumShares.isNotEmpty) {
await UnshareFileFromAlbum(shareRepo, fileRepo, albumRepo)(
account, album, files, albumShares);
}
}
final AlbumRepo albumRepo; final AlbumRepo albumRepo;
final ShareRepo shareRepo; final ShareRepo shareRepo;
final FileRepo fileRepo; final FileRepo fileRepo;

View file

@ -6,30 +6,65 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/use_case/create_share.dart'; import 'package:nc_photos/use_case/create_share.dart';
import 'package:nc_photos/use_case/update_album.dart';
class ShareAlbumWithUser { class ShareAlbumWithUser {
ShareAlbumWithUser(this.shareRepo); ShareAlbumWithUser(this.shareRepo, this.albumRepo);
Future<void> call( Future<void> call(
Account account,
Album album,
Sharee sharee, {
void Function(File)? onShareFileFailed,
}) async {
assert(album.provider is AlbumStaticProvider);
// add the share to album file
final newAlbum = album.copyWith(
shares: OrNull((album.shares ?? [])
..add(AlbumShare(
userId: sharee.shareWith,
displayName: sharee.shareWithDisplayNameUnique,
))),
);
await UpdateAlbum(albumRepo)(account, newAlbum);
await _createFileShares(
account,
newAlbum,
sharee.shareWith,
onShareFileFailed: onShareFileFailed,
);
}
Future<void> _createFileShares(
Account account, Account account,
Album album, Album album,
String shareWith, { String shareWith, {
void Function(File)? onShareFileFailed, void Function(File)? onShareFileFailed,
}) async { }) async {
assert(album.provider is AlbumStaticProvider);
final files = AlbumStaticProvider.of(album) final files = AlbumStaticProvider.of(album)
.items .items
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.map((e) => e.file); .map((e) => e.file);
await CreateUserShare(shareRepo)(account, album.albumFile!, shareWith); try {
await CreateUserShare(shareRepo)(account, album.albumFile!, shareWith);
} catch (e, stackTrace) {
_log.severe(
"[_createFileShares] Failed sharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
e,
stackTrace);
onShareFileFailed?.call(album.albumFile!);
}
for (final f in files) { for (final f in files) {
_log.info("[call] Sharing '${f.path}' with '$shareWith'"); _log.info("[_createFileShares] Sharing '${f.path}' with '$shareWith'");
try { try {
await CreateUserShare(shareRepo)(account, f, shareWith); await CreateUserShare(shareRepo)(account, f, shareWith);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.severe( _log.severe(
"[call] Failed sharing file '${logFilename(f.path)}' with '$shareWith'", "[_createFileShares] Failed sharing file '${logFilename(f.path)}' with '$shareWith'",
e, e,
stackTrace); stackTrace);
onShareFileFailed?.call(f); onShareFileFailed?.call(f);
@ -38,6 +73,7 @@ class ShareAlbumWithUser {
} }
final ShareRepo shareRepo; final ShareRepo shareRepo;
final AlbumRepo albumRepo;
static final _log = static final _log =
Logger("use_case.share_album_with_user.ShareAlbumWithUser"); Logger("use_case.share_album_with_user.ShareAlbumWithUser");

View file

@ -1,13 +1,16 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/use_case/list_shared_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/remove_share.dart';
import 'package:nc_photos/use_case/unshare_file_from_album.dart'; import 'package:nc_photos/use_case/unshare_file_from_album.dart';
import 'package:nc_photos/use_case/update_album.dart';
class UnshareAlbumWithUser { class UnshareAlbumWithUser {
UnshareAlbumWithUser(this.shareRepo, this.fileRepo, this.albumRepo); UnshareAlbumWithUser(this.shareRepo, this.fileRepo, this.albumRepo);
@ -19,16 +22,43 @@ class UnshareAlbumWithUser {
void Function(Share)? onUnshareFileFailed, void Function(Share)? onUnshareFileFailed,
}) async { }) async {
assert(album.provider is AlbumStaticProvider); assert(album.provider is AlbumStaticProvider);
final shareRepo = ShareRepo(ShareRemoteDataSource()); // remove the share from album file
final sharedItems = final newShares =
await ListSharedAlbum(shareRepo, fileRepo, albumRepo)(account); album.shares?.where((s) => s.userId != shareWith).toList() ?? [];
final thisShare = sharedItems final newAlbum = album.copyWith(
.firstWhere((element) => shares: OrNull(newShares.isEmpty ? null : newShares),
element.album.albumFile!.compareServerIdentity(album.albumFile!) && );
element.share.shareWith == shareWith) await UpdateAlbum(albumRepo)(account, newAlbum);
.share;
await RemoveShare(shareRepo)(account, thisShare);
await _deleteFileShares(
account,
newAlbum,
shareWith,
onUnshareFileFailed: onUnshareFileFailed,
);
}
Future<void> _deleteFileShares(
Account account,
Album album,
String shareWith, {
void Function(Share)? onUnshareFileFailed,
}) async {
// remove share from the album file
final albumShares = await ListShare(shareRepo)(account, album.albumFile!);
for (final s in albumShares.where((s) => s.shareWith == shareWith)) {
try {
await RemoveShare(shareRepo)(account, s);
} catch (e, stackTrace) {
_log.severe(
"[_deleteFileShares] Failed unsharing album file '${logFilename(album.albumFile?.path)}' with '$shareWith'",
e,
stackTrace);
onUnshareFileFailed?.call(s);
}
}
// then remove shares from all files in this album
final files = AlbumStaticProvider.of(album) final files = AlbumStaticProvider.of(album)
.items .items
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
@ -39,7 +69,6 @@ class UnshareAlbumWithUser {
album, album,
files, files,
[shareWith], [shareWith],
listSharedAlbumResults: sharedItems,
onUnshareFileFailed: onUnshareFileFailed, onUnshareFileFailed: onUnshareFileFailed,
); );
} }
@ -47,4 +76,7 @@ class UnshareAlbumWithUser {
final ShareRepo shareRepo; final ShareRepo shareRepo;
final FileRepo fileRepo; final FileRepo fileRepo;
final AlbumRepo albumRepo; final AlbumRepo albumRepo;
static final _log =
Logger("use_case.unshare_album_with_user.UnshareAlbumWithUser");
} }

View file

@ -6,8 +6,8 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/use_case/list_album.dart';
import 'package:nc_photos/use_case/list_share.dart'; import 'package:nc_photos/use_case/list_share.dart';
import 'package:nc_photos/use_case/list_shared_album.dart';
import 'package:nc_photos/use_case/remove_share.dart'; import 'package:nc_photos/use_case/remove_share.dart';
class UnshareFileFromAlbum { class UnshareFileFromAlbum {
@ -22,19 +22,19 @@ class UnshareFileFromAlbum {
Album album, Album album,
List<File> files, List<File> files,
List<String> unshareWith, { List<String> unshareWith, {
List<ListSharedAlbumItem>? listSharedAlbumResults,
void Function(Share)? onUnshareFileFailed, void Function(Share)? onUnshareFileFailed,
}) async { }) async {
_log.info( _log.info(
"[call] Unshare ${files.length} files from album '${album.name}' with ${unshareWith.length} users"); "[call] Unshare ${files.length} files from album '${album.name}' with ${unshareWith.length} users");
// list albums with shares identical to one of [unshareWith] // list albums with shares identical to any element in [unshareWith]
final otherAlbums = (listSharedAlbumResults ?? final otherAlbums = (await ListAlbum(fileRepo, albumRepo)(account)
await ListSharedAlbum(shareRepo, fileRepo, albumRepo)(account)) .where((event) => event is Album)
.where((element) => .cast<Album>()
!element.album.albumFile!.compareServerIdentity(album.albumFile!) && .where((album) =>
element.album.provider is AlbumStaticProvider && !album.albumFile!.compareServerIdentity(album.albumFile!) &&
unshareWith.contains(element.share.shareWith)) album.provider is AlbumStaticProvider &&
.toList(); album.shares?.any((s) => unshareWith.contains(s.userId)) == true)
.toList());
// look for shares that are exclusive to this album // look for shares that are exclusive to this album
final exclusiveShares = <Share>[]; final exclusiveShares = <Share>[];
@ -49,14 +49,18 @@ class UnshareFileFromAlbum {
} }
} }
for (final a in otherAlbums) { for (final a in otherAlbums) {
final albumFiles = AlbumStaticProvider.of(a.album) // check if the album is shared with the same users
if (!a.shares!
.any((as) => exclusiveShares.any((s) => s.shareWith == as.userId))) {
continue;
}
final albumFiles = AlbumStaticProvider.of(a)
.items .items
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
exclusiveShares.removeWhere((s) => exclusiveShares.removeWhere(
a.share.shareWith == s.shareWith && (s) => albumFiles.any((element) => element.fileId == s.itemSource));
albumFiles.any((element) => element.fileId == s.itemSource));
} }
// unshare them // unshare them

View file

@ -13,8 +13,9 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/entity/sharee/data_source.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
@ -219,8 +220,8 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
subtitle: Text( subtitle: Text(L10n.global().missingShareDescription(
L10n.global().missingShareDescription(item.shareWithDisplayName)), item.shareWithDisplayName ?? item.shareWith)),
trailing: trailing, trailing: trailing,
); );
} }
@ -321,9 +322,9 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
_items = []; _items = [];
} else if (state is ListAlbumShareOutlierBlocSuccess || } else if (state is ListAlbumShareOutlierBlocSuccess ||
state is ListAlbumShareOutlierBlocLoading) { state is ListAlbumShareOutlierBlocLoading) {
_transformItems(state.albumShares, state.items); _transformItems(state.items);
} else if (state is ListAlbumShareOutlierBlocFailure) { } else if (state is ListAlbumShareOutlierBlocFailure) {
_transformItems(state.albumShares, state.items); _transformItems(state.items);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(state.exception)), content: Text(exception_util.toUserString(state.exception)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
@ -362,48 +363,20 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
} }
} }
void _transformItems( void _transformItems(List<ListAlbumShareOutlierItem> items) {
List<Share> albumShares, List<ListAlbumShareOutlierItem> items) { _items = () sync* {
final to = for (final item in items) {
albumShares.sorted((a, b) => a.shareWith!.compareTo(b.shareWith!)); for (final si in item.shareItems) {
for (final i in items) { if (si is ListAlbumShareOutlierMissingShareItem) {
_transformItem(to, i); yield _MissingShareeItem(
} item.file, si.shareWith, si.shareWithDisplayName);
} } else if (si is ListAlbumShareOutlierExtraShareItem) {
yield _ExtraShareItem(item.file, si.share);
void _transformItem(List<Share> albumShares, ListAlbumShareOutlierItem item) { }
final from =
item.shares.sorted((a, b) => a.shareWith!.compareTo(b.shareWith!));
final to = albumShares;
var fromI = 0, toI = 0;
while (fromI < from.length && toI < to.length) {
final fromShare = from[fromI];
final toShare = to[toI];
if (fromShare.shareWith == toShare.shareWith) {
++fromI;
++toI;
} else {
final diff = fromShare.shareWith!.compareTo(toShare.shareWith!);
if (diff < 0) {
// extra element in from
_items.add(_ExtraShareItem(item.file, fromShare));
++fromI;
} else {
// extra element in to
_items.add(_MissingShareeItem(
item.file, toShare.shareWith!, toShare.shareWithDisplayName));
++toI;
} }
} }
} }()
.toList();
for (var i = fromI; i < from.length; ++i) {
_items.add(_ExtraShareItem(item.file, from[i]));
}
for (var i = toI; i < to.length; ++i) {
_items.add(_MissingShareeItem(
item.file, to[i].shareWith!, to[i].shareWithDisplayName));
}
} }
Future<void> _fixMissingSharee(_MissingShareeItem item) async { Future<void> _fixMissingSharee(_MissingShareeItem item) async {
@ -489,7 +462,8 @@ class _AlbumShareOutlierBrowserState extends State<AlbumShareOutlierBrowser> {
_itemStatuses[fileKey]!.remove(shareeKey); _itemStatuses[fileKey]!.remove(shareeKey);
} }
late final _bloc = ListAlbumShareOutlierBloc(); late final _bloc = ListAlbumShareOutlierBloc(
ShareRepo(ShareRemoteDataSource()), ShareeRepo(ShareeRemoteDataSource()));
var _items = <_ListItem>[]; var _items = <_ListItem>[];
final _itemStatuses = <String, Map<String, _ItemStatus>>{}; final _itemStatuses = <String, Map<String, _ItemStatus>>{};
@ -515,7 +489,7 @@ class _MissingShareeItem extends _ListItem {
final File file; final File file;
final String shareWith; final String shareWith;
final String shareWithDisplayName; final String? shareWithDisplayName;
} }
enum _ItemStatus { enum _ItemStatus {

View file

@ -5,7 +5,6 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_db.dart'; import 'package:nc_photos/app_db.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/list_share.dart';
import 'package:nc_photos/bloc/list_sharee.dart'; import 'package:nc_photos/bloc/list_sharee.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
@ -42,7 +41,6 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
initState() { initState() {
super.initState(); super.initState();
_shareeBloc.add(ListShareeBlocQuery(widget.account)); _shareeBloc.add(ListShareeBlocQuery(widget.account));
_shareBloc.add(ListShareBlocQuery(widget.account, widget.album.albumFile!));
} }
@override @override
@ -60,12 +58,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
_onListShareeBlocStateChanged(context, shareeState), _onListShareeBlocStateChanged(context, shareeState),
child: BlocBuilder<ListShareeBloc, ListShareeBlocState>( child: BlocBuilder<ListShareeBloc, ListShareeBlocState>(
bloc: _shareeBloc, bloc: _shareeBloc,
builder: (_, shareeState) => builder: (_, shareeState) => _buildContent(context, shareeState),
BlocBuilder<ListShareBloc, ListShareBlocState>(
bloc: _shareBloc,
builder: (context, shareState) =>
_buildContent(context, shareeState, shareState),
),
), ),
), ),
), ),
@ -73,11 +66,9 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
); );
} }
Widget _buildContent(BuildContext context, ListShareeBlocState shareeState, Widget _buildContent(BuildContext context, ListShareeBlocState shareeState) {
ListShareBlocState shareState) {
final List<Widget> children; final List<Widget> children;
if (shareeState is ListShareeBlocLoading || if (shareeState is ListShareeBlocLoading) {
shareState is ListShareBlocLoading) {
children = [ children = [
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
@ -95,7 +86,7 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
children = shareeState.items children = shareeState.items
.where((element) => element.type == ShareeType.user) .where((element) => element.type == ShareeType.user)
.sorted((a, b) => a.label.compareTo(b.label)) .sorted((a, b) => a.label.compareTo(b.label))
.map((sharee) => _buildItem(context, shareState, sharee)) .map((sharee) => _buildItem(context, sharee))
.toList(); .toList();
} }
return GestureDetector( return GestureDetector(
@ -107,14 +98,15 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
); );
} }
Widget _buildItem( Widget _buildItem(BuildContext context, Sharee sharee) {
BuildContext context, ListShareBlocState shareState, Sharee sharee) {
final bool isShared; final bool isShared;
if (_overrideSharee.containsKey(sharee.shareWith)) { if (_overrideSharee.containsKey(sharee.shareWith)) {
isShared = _overrideSharee[sharee.shareWith]!; isShared = _overrideSharee[sharee.shareWith]!;
} else { } else {
isShared = shareState.items isShared = widget.album.shares
.any((element) => element.shareWith == sharee.shareWith); ?.map((e) => e.userId)
.contains(sharee.shareWith) ??
false;
} }
final isProcessing = final isProcessing =
@ -185,12 +177,13 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
Future<void> _createShare(Sharee sharee) async { Future<void> _createShare(Sharee sharee) async {
final shareRepo = ShareRepo(ShareRemoteDataSource()); final shareRepo = ShareRepo(ShareRemoteDataSource());
final albumRepo = AlbumRepo(AlbumCachedDataSource(AppDb()));
var hasFailure = false; var hasFailure = false;
try { try {
await ShareAlbumWithUser(shareRepo)( await ShareAlbumWithUser(shareRepo, albumRepo)(
widget.account, widget.account,
widget.album, widget.album,
sharee.shareWith, sharee,
onShareFileFailed: (_) { onShareFileFailed: (_) {
hasFailure = true; hasFailure = true;
}, },
@ -260,7 +253,6 @@ class _ShareAlbumDialogState extends State<ShareAlbumDialog> {
} }
final _shareeBloc = ListShareeBloc(); final _shareeBloc = ListShareeBloc();
final _shareBloc = ListShareBloc();
final _processingSharee = <String>[]; final _processingSharee = <String>[];
/// Store the modified value of each sharee /// Store the modified value of each sharee

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/album/upgrader.dart'; import 'package:nc_photos/entity/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/type.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
@ -313,6 +314,8 @@ void main() {
)); ));
}); });
test("shares", _fromJsonShares);
test("albumFile", () { test("albumFile", () {
final json = <String, dynamic>{ final json = <String, dynamic>{
"version": Album.version, "version": Album.version,
@ -602,6 +605,8 @@ void main() {
}, },
}); });
}); });
test("shares", _toRemoteJsonShares);
}); });
group("toAppDbJson", () { group("toAppDbJson", () {
@ -852,6 +857,8 @@ void main() {
}); });
}); });
test("shares", _toAppDbJsonShares);
test("albumFile", () { test("albumFile", () {
final album = Album( final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
@ -1669,3 +1676,121 @@ void main() {
}); });
}); });
} }
void _fromJsonShares() {
final json = <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"shares": <JsonObj>[
{"userId": "admin"},
],
};
expect(
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
upgraderV5: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
shares: [AlbumShare(userId: "admin".toCi())],
));
}
void _toRemoteJsonShares() {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
shares: [AlbumShare(userId: "admin".toCi())],
);
expect(album.toRemoteJson(), <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"shares": [
<String, dynamic>{
"userId": "admin",
},
],
});
}
void _toAppDbJsonShares() {
final album = Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
provider: AlbumStaticProvider(
items: [],
),
coverProvider: AlbumAutoCoverProvider(),
sortProvider: const AlbumNullSortProvider(),
shares: [AlbumShare(userId: "admin".toCi())],
);
expect(album.toAppDbJson(), <String, dynamic>{
"version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "null",
"content": <String, dynamic>{},
},
"shares": [
<String, dynamic>{
"userId": "admin",
},
],
});
}