import 'package:bloc/bloc.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/search.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/use_case/search.dart'; abstract class SearchBlocEvent { const SearchBlocEvent(); } class SearchBlocQuery extends SearchBlocEvent { const SearchBlocQuery(this.account, this.criteria); @override toString() => "$runtimeType {" "account: $account, " "criteria: $criteria, " "}"; final Account account; final SearchCriteria criteria; } /// An external event has happened and may affect the state of this bloc class _SearchBlocExternalEvent extends SearchBlocEvent { const _SearchBlocExternalEvent(); @override toString() => "$runtimeType {" "}"; } class SearchBlocResetLanding extends SearchBlocEvent { const SearchBlocResetLanding(this.account); @override toString() => "$runtimeType {" "account: $account, " "}"; final Account account; } abstract class SearchBlocState { const SearchBlocState(this.account, this.criteria, this.items); @override toString() => "$runtimeType {" "account: $account, " "criteria: $criteria, " "items: List {length: ${items.length}}, " "}"; final Account? account; final SearchCriteria criteria; final List items; } class SearchBlocInit extends SearchBlocState { SearchBlocInit() : super(null, SearchCriteria("", []), const []); } class SearchBlocLoading extends SearchBlocState { const SearchBlocLoading( Account? account, SearchCriteria criteria, List items) : super(account, criteria, items); } class SearchBlocSuccess extends SearchBlocState { const SearchBlocSuccess( Account? account, SearchCriteria criteria, List items) : super(account, criteria, items); } class SearchBlocFailure extends SearchBlocState { const SearchBlocFailure(Account? account, SearchCriteria criteria, List items, this.exception) : super(account, criteria, items); @override toString() => "$runtimeType {" "super: ${super.toString()}, " "exception: $exception, " "}"; final Object exception; } /// The state of this bloc is inconsistent. This typically means that the data /// may have been changed externally class SearchBlocInconsistent extends SearchBlocState { const SearchBlocInconsistent( Account? account, SearchCriteria criteria, List items) : super(account, criteria, items); } class SearchBloc extends Bloc { SearchBloc(this._c) : assert(require(_c)), assert(Search.require(_c)), super(SearchBlocInit()) { _fileRemovedEventListener.begin(); _filePropertyUpdatedEventListener.begin(); // not listening to restore event because search works only with local data // sources and they are not aware of restore events on(_onEvent); } static bool require(DiContainer c) => true; @override close() { _fileRemovedEventListener.end(); _filePropertyUpdatedEventListener.end(); return super.close(); } Future _onEvent( SearchBlocEvent event, Emitter emit) async { _log.info("[_onEvent] $event"); if (event is SearchBlocQuery) { await _onEventQuery(event, emit); } else if (event is SearchBlocResetLanding) { emit(SearchBlocInit()); } else if (event is _SearchBlocExternalEvent) { await _onExternalEvent(event, emit); } } Future _onEventQuery( SearchBlocQuery ev, Emitter emit) async { try { emit(SearchBlocLoading(ev.account, ev.criteria, state.items)); emit(SearchBlocSuccess(ev.account, ev.criteria, await _query(ev))); } catch (e, stackTrace) { _log.severe("[_onEventQuery] Exception while request", e, stackTrace); emit(SearchBlocFailure(ev.account, ev.criteria, state.items, e)); } } Future _onExternalEvent( _SearchBlocExternalEvent ev, Emitter emit) async { emit(SearchBlocInconsistent(state.account, state.criteria, state.items)); } void _onFileRemovedEvent(FileRemovedEvent ev) { if (state is SearchBlocInit) { // no data in this bloc, ignore return; } if (_isFileOfInterest(ev.file)) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, ); } } void _onFilePropertyUpdatedEvent(FilePropertyUpdatedEvent ev) { if (!ev.hasAnyProperties([ FilePropertyUpdatedEvent.propMetadata, FilePropertyUpdatedEvent.propIsArchived, FilePropertyUpdatedEvent.propOverrideDateTime, FilePropertyUpdatedEvent.propFavorite, ])) { // not interested return; } if (state is SearchBlocInit) { // no data in this bloc, ignore return; } if (!_isFileOfInterest(ev.file)) { return; } if (ev.hasAnyProperties([ FilePropertyUpdatedEvent.propIsArchived, FilePropertyUpdatedEvent.propOverrideDateTime, FilePropertyUpdatedEvent.propFavorite, ])) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, ); } else { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 10), maxPendingCount: 10, ); } } Future> _query(SearchBlocQuery ev) => Search(_c)(ev.account, ev.criteria); bool _isFileOfInterest(File file) { if (!file_util.isSupportedFormat(file)) { return false; } for (final r in state.account?.roots ?? []) { final dir = File(path: file_util.unstripPath(state.account!, r)); if (file_util.isUnderDir(file, dir)) { return true; } } return false; } final DiContainer _c; late final _fileRemovedEventListener = AppEventListener(_onFileRemovedEvent); late final _filePropertyUpdatedEventListener = AppEventListener(_onFilePropertyUpdatedEvent); late final _refreshThrottler = Throttler( onTriggered: (_) { add(const _SearchBlocExternalEvent()); }, logTag: "SearchBloc.refresh", ); static final _log = Logger("bloc.search.SearchBloc"); }