nc-photos/lib/bloc/list_album_share_outlier.dart
Ming Ming 6e9a34342a 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
2021-11-25 21:02:41 +08:00

331 lines
9.7 KiB
Dart

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:logging/logging.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/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.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/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_sharee.dart';
class ListAlbumShareOutlierItem with EquatableMixin {
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 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 {
const ListAlbumShareOutlierBlocEvent();
}
class ListAlbumShareOutlierBlocQuery extends ListAlbumShareOutlierBlocEvent {
const ListAlbumShareOutlierBlocQuery(this.account, this.album);
@override
toString() {
return "$runtimeType {"
"account: $account, "
"album: $album, "
"}";
}
final Account account;
final Album album;
}
abstract class ListAlbumShareOutlierBlocState with EquatableMixin {
const ListAlbumShareOutlierBlocState(this.account, this.items);
@override
toString() {
return "$runtimeType {"
"account: $account, "
"items: ${items.toReadableString()}, "
"}";
}
@override
get props => [
account,
items,
];
final Account? account;
final List<ListAlbumShareOutlierItem> items;
}
class ListAlbumShareOutlierBlocInit extends ListAlbumShareOutlierBlocState {
ListAlbumShareOutlierBlocInit() : super(null, const []);
}
class ListAlbumShareOutlierBlocLoading extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocLoading(
Account? account, List<ListAlbumShareOutlierItem> items)
: super(account, items);
}
class ListAlbumShareOutlierBlocSuccess extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocSuccess(
Account? account, List<ListAlbumShareOutlierItem> items)
: super(account, items);
}
class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState {
const ListAlbumShareOutlierBlocFailure(
Account? account, List<ListAlbumShareOutlierItem> items, this.exception)
: super(account, items);
@override
toString() {
return "$runtimeType {"
"super: ${super.toString()}, "
"exception: $exception, "
"}";
}
@override
get props => [
...super.props,
exception,
];
final dynamic exception;
}
/// List the outliers in a shared album
///
/// An outlier is a file where its shares are different to the album's that it
/// belongs, e.g., an unshared item in a shared album, or vice versa
class ListAlbumShareOutlierBloc extends Bloc<ListAlbumShareOutlierBlocEvent,
ListAlbumShareOutlierBlocState> {
ListAlbumShareOutlierBloc(this.shareRepo, this.shareeRepo)
: super(ListAlbumShareOutlierBlocInit());
@override
mapEventToState(ListAlbumShareOutlierBlocEvent event) async* {
_log.info("[mapEventToState] $event");
if (event is ListAlbumShareOutlierBlocQuery) {
yield* _onEventQuery(event);
}
}
Stream<ListAlbumShareOutlierBlocState> _onEventQuery(
ListAlbumShareOutlierBlocQuery ev) async* {
try {
assert(ev.album.provider is AlbumStaticProvider);
yield ListAlbumShareOutlierBlocLoading(ev.account, state.items);
final albumShares = await () async {
var temp = (ev.album.shares ?? [])
.where((s) => !s.userId.equalsIgnoreCase(ev.account.username))
.toList();
if (ev.album.albumFile!.ownerId != ev.account.username) {
// add owner if the album is not owned by this account
final ownerSharee = (await ListSharee(shareeRepo)(ev.account))
.firstWhere((s) => s.shareWith == ev.album.albumFile!.ownerId);
temp.add(AlbumShare(
userId: ownerSharee.shareWith,
displayName: ownerSharee.shareWithDisplayNameUnique,
));
}
return Map.fromEntries(
temp.map((as) => MapEntry(as.userId.toLowerCase(), as)));
}();
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 {
yield ListAlbumShareOutlierBlocFailure(
ev.account, products, errors.first);
}
} catch (e, stackTrace) {
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
yield ListAlbumShareOutlierBlocFailure(ev.account, 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 ShareeRepo shareeRepo;
static final _log =
Logger("bloc.list_album_share_outlier.ListAlbumShareOutlierBloc");
}