import 'package:bloc/bloc.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/exception.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/string_extension.dart'; import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/use_case/list_album.dart'; import 'package:tuple/tuple.dart'; class ListAlbumBlocItem { ListAlbumBlocItem(this.album, this.isSharedByMe, this.isSharedToMe); final Album album; final bool isSharedByMe; final bool isSharedToMe; } 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 { ListAlbumBloc() : super(ListAlbumBlocInit()) { _albumUpdatedListener = AppEventListener(_onAlbumUpdatedEvent); _fileRemovedListener = AppEventListener(_onFileRemovedEvent); _albumCreatedListener = AppEventListener(_onAlbumCreatedEvent); _albumUpdatedListener.begin(); _fileRemovedListener.begin(); _albumCreatedListener.begin(); _fileMovedListener.begin(); _refreshThrottler = Throttler( onTriggered: (_) { add(_ListAlbumBlocExternalEvent()); }, logTag: "ListAlbumBloc.refresh", ); } @override mapEventToState(ListAlbumBlocEvent event) async* { _log.info("[mapEventToState] $event"); if (event is ListAlbumBlocQuery) { yield* _onEventQuery(event); } else if (event is _ListAlbumBlocExternalEvent) { yield* _onExternalEvent(event); } } @override close() { _albumUpdatedListener.end(); _fileRemovedListener.end(); _albumCreatedListener.end(); _fileMovedListener.end(); _refreshThrottler.clear(); return super.close(); } Stream _onEventQuery(ListAlbumBlocQuery ev) async* { yield ListAlbumBlocLoading(ev.account, state.items); bool hasContent = state.items.isNotEmpty; if (!hasContent) { // show something instantly on first load final cacheState = await _queryOffline(ev); yield ListAlbumBlocLoading(ev.account, cacheState.items); hasContent = cacheState.items.isNotEmpty; } final newState = await _queryOnline(ev); if (newState is ListAlbumBlocFailure) { yield ListAlbumBlocFailure( ev.account, newState.items.isNotEmpty ? newState.items : state.items, newState.exception); } else { yield newState; } } Stream _onExternalEvent( _ListAlbumBlocExternalEvent ev) async* { yield ListAlbumBlocInconsistent(state.account, state.items); } void _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) { if (state is ListAlbumBlocInit) { // no data in this bloc, ignore return; } _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 (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 (ev.destination .startsWith(remote_storage_util.getRemoteAlbumsDir(ev.account))) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, ); } } void _onAlbumCreatedEvent(AlbumCreatedEvent ev) { if (state is ListAlbumBlocInit) { // no data in this bloc, ignore return; } add(_ListAlbumBlocExternalEvent()); } Future _queryOffline(ListAlbumBlocQuery ev) => _queryWithAlbumDataSource( ev, FileAppDbDataSource(), AlbumAppDbDataSource()); Future _queryOnline(ListAlbumBlocQuery ev) => _queryWithAlbumDataSource( ev, FileCachedDataSource(), AlbumCachedDataSource()); Future _queryWithAlbumDataSource(ListAlbumBlocQuery ev, FileDataSource fileDataSource, AlbumDataSource albumDataSrc) async { try { final albums = []; final errors = []; await for (final result in ListAlbum( FileRepo(fileDataSource), AlbumRepo(albumDataSrc))(ev.account)) { if (result is Tuple2) { if (result.item1 is CacheNotFoundException) { _log.info( "[_queryWithAlbumDataSource] Cache not found", result.item1); } else { _log.shout("[_queryWithAlbumDataSource] Exception while ListAlbum", result.item1, result.item2); } errors.add(result.item1); } else if (result is Album) { albums.add(result); } } final shareRepo = ShareRepo(ShareRemoteDataSource()); final shares = await shareRepo.listDir(ev.account, File(path: remote_storage_util.getRemoteAlbumsDir(ev.account))); final items = albums.map((e) { final isSharedByMe = shares.any((element) => element.path.trimAny("/") == e.albumFile!.strippedPath); final isSharedToMe = e.albumFile!.ownerId != ev.account.username; return ListAlbumBlocItem(e, isSharedByMe, isSharedToMe); }).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); } } late AppEventListener _albumUpdatedListener; late AppEventListener _fileRemovedListener; late AppEventListener _albumCreatedListener; late final _fileMovedListener = AppEventListener(_onFileMovedEvent); late Throttler _refreshThrottler; static final _log = Logger("bloc.list_album.ListAlbumBloc"); }