import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/use_case/find_file.dart'; import 'package:nc_photos/use_case/list_share_with_me.dart'; import 'package:nc_photos/use_case/ls.dart'; import 'package:nc_photos/use_case/ls_single_file.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:path/path.dart' as path_lib; import 'package:to_string/to_string.dart'; part 'list_sharing.g.dart'; abstract class ListSharingItem { const ListSharingItem(this.share); final Share share; } class ListSharingFile extends ListSharingItem { const ListSharingFile(Share share, this.file) : super(share); final File file; } class ListSharingAlbum extends ListSharingItem { const ListSharingAlbum(Share share, this.album) : super(share); final Album album; } abstract class ListSharingBlocEvent { const ListSharingBlocEvent(); } @toString class ListSharingBlocQuery extends ListSharingBlocEvent { const ListSharingBlocQuery(this.account); @override String toString() => _$toString(); final Account account; } @toString class _ListSharingBlocShareRemoved extends ListSharingBlocEvent { const _ListSharingBlocShareRemoved(this.shares); @override String toString() => _$toString(); @Format(r"${$?.toReadableString()}") final List shares; } @toString class _ListSharingBlocPendingSharedAlbumMoved extends ListSharingBlocEvent { const _ListSharingBlocPendingSharedAlbumMoved( this.account, this.file, this.destination); @override String toString() => _$toString(); final Account account; final File file; final String destination; } @toString abstract class ListSharingBlocState { const ListSharingBlocState(this.account, this.items); @override String toString() => _$toString(); final Account? account; final List items; } class ListSharingBlocInit extends ListSharingBlocState { ListSharingBlocInit() : super(null, const []); } class ListSharingBlocLoading extends ListSharingBlocState { const ListSharingBlocLoading(Account? account, List items) : super(account, items); } class ListSharingBlocSuccess extends ListSharingBlocState { const ListSharingBlocSuccess(Account? account, List items) : super(account, items); ListSharingBlocSuccess copyWith({ Account? account, List? items, }) => ListSharingBlocSuccess( account ?? this.account, items ?? List.of(this.items), ); } @toString class ListSharingBlocFailure extends ListSharingBlocState { const ListSharingBlocFailure( Account? account, List items, this.exception) : super(account, items); @override String toString() => _$toString(); ListSharingBlocFailure copyWith({ Account? account, List? items, dynamic exception, }) => ListSharingBlocFailure( account ?? this.account, items ?? List.of(this.items), exception ?? this.exception, ); final dynamic exception; } /// List shares to be shown in [SharingBrowser] @npLog class ListSharingBloc extends Bloc { ListSharingBloc(this._c) : assert(require(_c)), assert(FindFile.require(_c)), assert(ListShareWithMe.require(_c)), assert(LsSingleFile.require(_c)), super(ListSharingBlocInit()) { _shareRemovedListener.begin(); _fileMovedEventListener.begin(); _refreshThrottler = Throttler( onTriggered: (shares) { add(_ListSharingBlocShareRemoved(shares)); }, logTag: "ListSharingBloc.refresh", ); on(_onEvent); } static bool require(DiContainer c) => DiContainer.has(c, DiType.albumRepo) && DiContainer.has(c, DiType.fileRepo) && DiContainer.has(c, DiType.shareRepo); static ListSharingBloc of(Account account) { final name = bloc_util.getInstNameForRootAwareAccount("ListSharingBloc", account); try { _log.fine("[of] Resolving bloc for '$name'"); return KiwiContainer().resolve(name); } catch (_) { // no created instance for this account, make a new one _log.info("[of] New bloc instance for account: $account"); final bloc = ListSharingBloc(KiwiContainer().resolve()); KiwiContainer().registerInstance(bloc, name: name); return bloc; } } @override close() { _shareRemovedListener.end(); _fileMovedEventListener.end(); _refreshThrottler.clear(); return super.close(); } Future _onEvent( ListSharingBlocEvent event, Emitter emit) async { _log.info("[_onEvent] $event"); if (event is ListSharingBlocQuery) { await _onEventQuery(event, emit); } else if (event is _ListSharingBlocShareRemoved) { await _onEventShareRemoved(event, emit); } else if (event is _ListSharingBlocPendingSharedAlbumMoved) { await _onEventPendingSharedAlbumMoved(event, emit); } } Future _onEventQuery( ListSharingBlocQuery ev, Emitter emit) async { try { emit(ListSharingBlocLoading(ev.account, state.items)); emit(ListSharingBlocSuccess(ev.account, await _query(ev))); } catch (e, stackTrace) { _log.severe("[_onEventQuery] Exception while request", e, stackTrace); emit(ListSharingBlocFailure(ev.account, state.items, e)); } } Future _onEventShareRemoved(_ListSharingBlocShareRemoved ev, Emitter emit) async { if (state is! ListSharingBlocSuccess && state is! ListSharingBlocFailure) { return; } final newItems = state.items.where((i) => !ev.shares.contains(i.share)).toList(); // i love hacks :) emit((state as dynamic).copyWith( items: newItems, ) as ListSharingBlocState); } Future _onEventPendingSharedAlbumMoved( _ListSharingBlocPendingSharedAlbumMoved ev, Emitter emit) async { if (state.items.isEmpty) { return; } try { emit(ListSharingBlocLoading(ev.account, state.items)); final items = List.of(state.items); items.removeWhere( (i) => i is ListSharingAlbum && i.share.path == ev.file.strippedPath); final newShares = await ListShareWithMe(_c)(ev.account, File(path: ev.destination)); final newAlbumFile = await LsSingleFile(_c)(ev.account, ev.destination); final newAlbum = await _c.albumRepo.get(ev.account, newAlbumFile); for (final s in newShares) { items.add(ListSharingAlbum(s, newAlbum)); } emit(ListSharingBlocSuccess(ev.account, items)); } catch (e, stackTrace) { _log.severe("[_onEventPendingSharedAlbumMoved] Exception while request", e, stackTrace); emit(ListSharingBlocFailure(ev.account, state.items, e)); } } void _onShareRemovedEvent(ShareRemovedEvent ev) { if (_isAccountOfInterest(ev.account)) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, data: ev.share, ); } } void _onFileMovedEvent(FileMovedEvent ev) { if (state is ListSharingBlocInit) { // no data in this bloc, ignore return; } if (_isAccountOfInterest(ev.account)) { if (ev.destination .startsWith(remote_storage_util.getRemoteAlbumsDir(ev.account)) && ev.file.path.startsWith(remote_storage_util .getRemotePendingSharedAlbumsDir(ev.account))) { // moving from/to pending dir add(_ListSharingBlocPendingSharedAlbumMoved( ev.account, ev.file, ev.destination)); } } } Future> _query(ListSharingBlocQuery ev) async { final sharedAlbumFiles = await Ls(_c.fileRepo)( ev.account, File( path: remote_storage_util.getRemoteAlbumsDir(ev.account), )); return (await Future.wait([ _querySharesByMe(ev, sharedAlbumFiles), _querySharesWithMe(ev, sharedAlbumFiles), ])) .reduce((value, element) => value + element); } Future> _querySharesByMe( ListSharingBlocQuery ev, List sharedAlbumFiles) async { final shares = await _c.shareRepo.listAll(ev.account); final futures = shares.map((s) async { final webdavPath = file_util.unstripPath(ev.account, s.path); // include link share dirs if (s.itemType == ShareItemType.folder) { if (webdavPath.startsWith( remote_storage_util.getRemoteLinkSharesDir(ev.account))) { return ListSharingFile( s, File( path: webdavPath, fileId: s.itemSource, isCollection: true, ), ); } } // include shared albums if (path_lib.dirname(webdavPath) == remote_storage_util.getRemoteAlbumsDir(ev.account)) { try { final file = sharedAlbumFiles .firstWhere((element) => element.fileId == s.itemSource); return await _querySharedAlbum(ev, s, file); } catch (e, stackTrace) { _log.severe( "[_querySharesWithMe] Shared album not found: ${s.itemSource}", e, stackTrace); return null; } } if (!file_util.isSupportedMime(s.mimeType)) { return null; } // show only link shares if (s.url == null) { return null; } if (ev.account.roots .every((r) => r.isNotEmpty && !s.path.startsWith("$r/"))) { // ignore files not under root dirs return null; } try { final file = (await FindFile(_c)(ev.account, [s.itemSource])).first; return ListSharingFile(s, file); } catch (e, stackTrace) { _log.severe("[_querySharesByMe] File not found: ${s.itemSource}", e, stackTrace); return null; } }); return (await Future.wait(futures)).whereType().toList(); } Future> _querySharesWithMe( ListSharingBlocQuery ev, List sharedAlbumFiles) async { final pendingSharedAlbumFiles = await Ls(_c.fileRepo)( ev.account, File( path: remote_storage_util.getRemotePendingSharedAlbumsDir(ev.account), )); final shares = await _c.shareRepo.reverseListAll(ev.account); final futures = shares.map((s) async { final webdavPath = file_util.unstripPath(ev.account, s.path); // include pending shared albums if (path_lib.dirname(webdavPath) == remote_storage_util.getRemotePendingSharedAlbumsDir(ev.account)) { try { final file = pendingSharedAlbumFiles .firstWhere((element) => element.fileId == s.itemSource); return await _querySharedAlbum(ev, s, file); } catch (e, stackTrace) { _log.severe( "[_querySharesWithMe] Pending shared album not found: ${s.itemSource}", e, stackTrace); return null; } } // include shared albums if (path_lib.dirname(webdavPath) == remote_storage_util.getRemoteAlbumsDir(ev.account)) { try { final file = sharedAlbumFiles .firstWhere((element) => element.fileId == s.itemSource); return await _querySharedAlbum(ev, s, file); } catch (e, stackTrace) { _log.severe( "[_querySharesWithMe] Shared album not found: ${s.itemSource}", e, stackTrace); return null; } } }); return (await Future.wait(futures)).whereType().toList(); } Future _querySharedAlbum( ListSharingBlocQuery ev, Share share, File albumFile) async { try { final album = await _c.albumRepo.get(ev.account, albumFile); return ListSharingAlbum(share, album); } catch (e, stackTrace) { _log.shout( "[_querySharedAlbum] Failed while getting album", e, stackTrace); return null; } } bool _isAccountOfInterest(Account account) => state.account == null || state.account!.compareServerIdentity(account); final DiContainer _c; late final _shareRemovedListener = AppEventListener(_onShareRemovedEvent); late final _fileMovedEventListener = AppEventListener(_onFileMovedEvent); late Throttler _refreshThrottler; static final _log = _$ListSharingBlocNpLog.log; }