import 'package:bloc/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_util.dart' as file_util; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/or_null.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/list_album.dart'; class ListAlbumBlocItem { ListAlbumBlocItem(this.album); final Album album; } abstract class ListAlbumBlocEvent { const ListAlbumBlocEvent(); } class ListAlbumBlocQuery extends ListAlbumBlocEvent { const ListAlbumBlocQuery(this.account); @override toString() { return "$runtimeType {" "account: $account, " "}"; } final Account account; } /// An external event has happened and may affect the state of this bloc class _ListAlbumBlocExternalEvent extends ListAlbumBlocEvent { const _ListAlbumBlocExternalEvent(); @override toString() { return "$runtimeType {" "}"; } } abstract class ListAlbumBlocState { const ListAlbumBlocState(this.account, this.items); @override toString() { return "$runtimeType {" "account: $account, " "items: List {length: ${items.length}}, " "}"; } final Account? account; final List items; } class ListAlbumBlocInit extends ListAlbumBlocState { const ListAlbumBlocInit() : super(null, const []); } class ListAlbumBlocLoading extends ListAlbumBlocState { const ListAlbumBlocLoading(Account? account, List items) : super(account, items); } class ListAlbumBlocSuccess extends ListAlbumBlocState { const ListAlbumBlocSuccess(Account? account, List items) : super(account, items); } class ListAlbumBlocFailure extends ListAlbumBlocState { const ListAlbumBlocFailure( Account? account, List items, this.exception) : super(account, items); @override toString() { return "$runtimeType {" "super: ${super.toString()}, " "exception: $exception, " "}"; } final dynamic exception; } /// The state of this bloc is inconsistent. This typically means that the data /// may have been changed externally class ListAlbumBlocInconsistent extends ListAlbumBlocState { const ListAlbumBlocInconsistent( Account? account, List items) : super(account, items); } class ListAlbumBloc extends Bloc { /// Constructor /// /// If [offlineC] is not null, this [DiContainer] will be used when requesting /// offline contents, otherwise [_c] will be used ListAlbumBloc( this._c, [ DiContainer? offlineC, ]) : _offlineC = offlineC ?? _c, assert(require(_c)), assert(offlineC == null || require(offlineC)), assert(ListAlbum.require(_c)), assert(offlineC == null || ListAlbum.require(offlineC)), super(const ListAlbumBlocInit()) { _albumUpdatedListener = AppEventListener(_onAlbumUpdatedEvent); _fileRemovedListener = AppEventListener(_onFileRemovedEvent); _albumCreatedListener = AppEventListener(_onAlbumCreatedEvent); _albumUpdatedListener.begin(); _fileRemovedListener.begin(); _albumCreatedListener.begin(); _fileMovedListener.begin(); _shareCreatedListener.begin(); _shareRemovedListener.begin(); _refreshThrottler = Throttler( onTriggered: (_) { add(const _ListAlbumBlocExternalEvent()); }, logTag: "ListAlbumBloc.refresh", ); on(_onEvent); } static bool require(DiContainer c) => true; static ListAlbumBloc of(Account account) { final name = bloc_util.getInstNameForAccount("ListAlbumBloc", 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 c = KiwiContainer().resolve(); final offlineC = c.copyWith( fileRepo: OrNull(c.fileRepoLocal), albumRepo: OrNull(c.albumRepoLocal), ); final bloc = ListAlbumBloc(c, offlineC); KiwiContainer().registerInstance(bloc, name: name); return bloc; } } @override close() { _albumUpdatedListener.end(); _fileRemovedListener.end(); _albumCreatedListener.end(); _fileMovedListener.end(); _shareCreatedListener.end(); _shareRemovedListener.end(); _refreshThrottler.clear(); return super.close(); } Future _onEvent( ListAlbumBlocEvent event, Emitter emit) async { _log.info("[_onEvent] $event"); if (event is ListAlbumBlocQuery) { await _onEventQuery(event, emit); } else if (event is _ListAlbumBlocExternalEvent) { await _onExternalEvent(event, emit); } } Future _onEventQuery( ListAlbumBlocQuery ev, Emitter emit) async { emit(ListAlbumBlocLoading(ev.account, state.items)); bool hasContent = state.items.isNotEmpty; if (!hasContent) { // show something instantly on first load final cacheState = await _queryOffline(ev); emit(ListAlbumBlocLoading(ev.account, cacheState.items)); hasContent = cacheState.items.isNotEmpty; } final newState = await _queryOnline(ev); if (newState is ListAlbumBlocFailure) { emit(ListAlbumBlocFailure( ev.account, newState.items.isNotEmpty ? newState.items : state.items, newState.exception)); } else { emit(newState); } } Future _onExternalEvent( _ListAlbumBlocExternalEvent ev, Emitter emit) async { emit(ListAlbumBlocInconsistent(state.account, state.items)); } void _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) { if (state is ListAlbumBlocInit) { // no data in this bloc, ignore return; } if (_isAccountOfInterest(ev.account)) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, ); } } void _onFileRemovedEvent(FileRemovedEvent ev) { if (state is ListAlbumBlocInit) { // no data in this bloc, ignore return; } if (_isAccountOfInterest(ev.account) && file_util.isAlbumFile(ev.account, ev.file)) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, ); } } void _onFileMovedEvent(FileMovedEvent ev) { if (state is ListAlbumBlocInit) { // 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.getRemoteAlbumsDir(ev.account))) { // moving from/to album dir _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, ); } } } void _onAlbumCreatedEvent(AlbumCreatedEvent ev) { if (state is ListAlbumBlocInit) { // no data in this bloc, ignore return; } if (_isAccountOfInterest(ev.account)) { add(const _ListAlbumBlocExternalEvent()); } } void _onShareCreatedEvent(ShareCreatedEvent ev) => _onShareChanged(ev.account, ev.share); void _onShareRemovedEvent(ShareRemovedEvent ev) => _onShareChanged(ev.account, ev.share); void _onShareChanged(Account account, Share share) { if (_isAccountOfInterest(account)) { final webdavPath = file_util.unstripPath(account, share.path); if (webdavPath .startsWith(remote_storage_util.getRemoteAlbumsDir(account))) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, ); } } } Future _queryOffline(ListAlbumBlocQuery ev) => _queryWithAlbumDataSource(_offlineC, ev); Future _queryOnline(ListAlbumBlocQuery ev) => _queryWithAlbumDataSource(_c, ev); Future _queryWithAlbumDataSource( DiContainer c, ListAlbumBlocQuery ev) async { try { final albums = []; final errors = []; await for (final result in ListAlbum(c)(ev.account)) { if (result is ExceptionEvent) { if (result.error is CacheNotFoundException) { _log.info( "[_queryWithAlbumDataSource] Cache not found", result.error); } else { _log.shout("[_queryWithAlbumDataSource] Exception while ListAlbum", result.error, result.stackTrace); } errors.add(result.error); } else if (result is Album) { albums.add(result); } } final items = albums.map((e) => ListAlbumBlocItem(e)).toList(); if (errors.isEmpty) { return ListAlbumBlocSuccess(ev.account, items); } else { return ListAlbumBlocFailure(ev.account, items, errors.first); } } catch (e, stacktrace) { _log.severe("[_queryWithAlbumDataSource] Exception", e, stacktrace); return ListAlbumBlocFailure(ev.account, [], e); } } bool _isAccountOfInterest(Account account) => state.account == null || state.account!.compareServerIdentity(account); final DiContainer _c; final DiContainer _offlineC; late AppEventListener _albumUpdatedListener; late AppEventListener _fileRemovedListener; late AppEventListener _albumCreatedListener; late final _fileMovedListener = AppEventListener(_onFileMovedEvent); late final _shareCreatedListener = AppEventListener(_onShareCreatedEvent); late final _shareRemovedListener = AppEventListener(_onShareRemovedEvent); late Throttler _refreshThrottler; static final _log = Logger("bloc.list_album.ListAlbumBloc"); }