nc-photos/lib/bloc/list_album_share_outlier.dart
2021-11-25 21:02:41 +08:00

329 lines
9.6 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/ci_string.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/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 CiString 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 != 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, as)));
}();
final albumSharees = albumShares.values.map((s) => s.userId).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<CiString, AlbumShare> albumShares,
Set<CiString> 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<CiString, AlbumShare> albumShares,
Set<CiString> 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<CiString, AlbumShare> albumShares,
Set<CiString> 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!).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]!;
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 == e)));
} 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");
}