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 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 items; } class ListAlbumShareOutlierBlocInit extends ListAlbumShareOutlierBlocState { ListAlbumShareOutlierBlocInit() : super(null, const []); } class ListAlbumShareOutlierBlocLoading extends ListAlbumShareOutlierBlocState { const ListAlbumShareOutlierBlocLoading( Account? account, List items) : super(account, items); } class ListAlbumShareOutlierBlocSuccess extends ListAlbumShareOutlierBlocState { const ListAlbumShareOutlierBlocSuccess( Account? account, List items) : super(account, items); } class ListAlbumShareOutlierBlocFailure extends ListAlbumShareOutlierBlocState { const ListAlbumShareOutlierBlocFailure( Account? account, List 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 { ListAlbumShareOutlierBloc(this.shareRepo, this.shareeRepo) : super(ListAlbumShareOutlierBlocInit()); @override mapEventToState(ListAlbumShareOutlierBlocEvent event) async* { _log.info("[mapEventToState] $event"); if (event is ListAlbumShareOutlierBlocQuery) { yield* _onEventQuery(event); } } Stream _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 = []; final errors = []; 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> _processAlbumFile( Account account, Album album, Map albumShares, Set albumSharees, List 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> _processAlbumItems( Account account, Album album, Map albumShares, Set albumSharees, List errors, ) async { final products = []; final files = AlbumStaticProvider.of(album) .items .whereType() .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 _processSingleFile( Account account, File file, Map albumShares, Set albumSharees, List errors, ) async { final shareItems = []; 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"); }