import 'package:bloc/bloc.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/entity/tag.dart'; import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/use_case/list_album.dart'; import 'package:nc_photos/use_case/list_location_group.dart'; import 'package:nc_photos/use_case/list_person.dart'; import 'package:nc_photos/use_case/list_tag.dart'; import 'package:tuple/tuple.dart'; import 'package:woozy_search/woozy_search.dart'; abstract class HomeSearchResult {} class HomeSearchAlbumResult implements HomeSearchResult { const HomeSearchAlbumResult(this.album); @override toString() => "$runtimeType {" "album: $album, " "}"; final Album album; } class HomeSearchTagResult implements HomeSearchResult { const HomeSearchTagResult(this.tag); @override toString() => "$runtimeType {" "tag: $tag, " "}"; final Tag tag; } class HomeSearchPersonResult implements HomeSearchResult { const HomeSearchPersonResult(this.person); @override toString() => "$runtimeType {" "person: $person, " "}"; final Person person; } class HomeSearchLocationResult implements HomeSearchResult { const HomeSearchLocationResult(this.location); @override toString() => "$runtimeType {" "location: $location, " "}"; final LocationGroup location; } abstract class HomeSearchSuggestionBlocEvent { const HomeSearchSuggestionBlocEvent(); } class HomeSearchSuggestionBlocPreloadData extends HomeSearchSuggestionBlocEvent { const HomeSearchSuggestionBlocPreloadData(); @override toString() => "$runtimeType {" "}"; } class HomeSearchSuggestionBlocSearch extends HomeSearchSuggestionBlocEvent { const HomeSearchSuggestionBlocSearch(this.phrase); @override toString() => "$runtimeType {" "phrase: '$phrase', " "}"; final CiString phrase; } abstract class HomeSearchSuggestionBlocState { const HomeSearchSuggestionBlocState(this.results); @override toString() => "$runtimeType {" "results: List {legth: ${results.length}}, " "}"; final List results; } class HomeSearchSuggestionBlocInit extends HomeSearchSuggestionBlocState { const HomeSearchSuggestionBlocInit() : super(const []); } class HomeSearchSuggestionBlocLoading extends HomeSearchSuggestionBlocState { const HomeSearchSuggestionBlocLoading(List results) : super(results); } class HomeSearchSuggestionBlocSuccess extends HomeSearchSuggestionBlocState { const HomeSearchSuggestionBlocSuccess(List results) : super(results); } class HomeSearchSuggestionBlocFailure extends HomeSearchSuggestionBlocState { const HomeSearchSuggestionBlocFailure( List results, this.exception) : super(results); @override toString() => "$runtimeType {" "super: ${super.toString()}, " "exception: $exception, " "}"; final Object exception; } class HomeSearchSuggestionBloc extends Bloc { HomeSearchSuggestionBloc(this.account) : super(const HomeSearchSuggestionBlocInit()) { final c = KiwiContainer().resolve(); assert(require(c)); assert(ListTag.require(c)); _c = c.withLocalRepo(); on(_onEvent); add(const HomeSearchSuggestionBlocPreloadData()); } static bool require(DiContainer c) => true; Future _onEvent(HomeSearchSuggestionBlocEvent event, Emitter emit) async { _log.info("[_onEvent] $event"); if (event is HomeSearchSuggestionBlocSearch) { await _onEventSearch(event, emit); } else if (event is HomeSearchSuggestionBlocPreloadData) { await _onEventPreloadData(event, emit); } } Future _onEventSearch(HomeSearchSuggestionBlocSearch ev, Emitter emit) async { if (ev.phrase.raw.isEmpty) { emit(const HomeSearchSuggestionBlocSuccess([])); return; } emit(HomeSearchSuggestionBlocLoading(state.results)); // doesn't work with upper case final results = _search.search(ev.phrase.toCaseInsensitiveString()); if (kDebugMode) { final str = results.map((e) => "${e.score}: ${e.text}").join("\n"); _log.fine("[_onEventSearch] Search '${ev.phrase}':\n$str"); } final matches = results .where((element) => element.score > 0) .map((e) { if (e.value!.toKeywords().any((k) => k.startsWith(ev.phrase))) { // prefer names that start exactly with the search phrase return Tuple2(e.score + 1, e.value); } else { return Tuple2(e.score, e.value); } }) .sorted((a, b) => b.item1.compareTo(a.item1)) .distinctIf( (a, b) => identical(a.item2, b.item2), (a) => a.item2.hashCode, ) .map((e) => e.item2!.toResult()) .toList(); emit(HomeSearchSuggestionBlocSuccess(matches)); } Future _onEventPreloadData(HomeSearchSuggestionBlocPreloadData ev, Emitter emit) async { final product = <_Searcheable>[]; try { final albums = await ListAlbum(_c)(account) .where((event) => event is Album) .toList(); product.addAll(albums.map((a) => _AlbumSearcheable(a))); _log.info("[_onEventPreloadData] Loaded ${albums.length} albums"); } catch (e) { _log.warning("[_onEventPreloadData] Failed while ListAlbum", e); } try { final tags = await ListTag(_c)(account); product.addAll(tags.map((t) => _TagSearcheable(t))); _log.info("[_onEventPreloadData] Loaded ${tags.length} tags"); } catch (e) { _log.warning("[_onEventPreloadData] Failed while ListTag", e); } try { final persons = await ListPerson(_c)(account); product.addAll(persons.map((t) => _PersonSearcheable(t))); _log.info("[_onEventPreloadData] Loaded ${persons.length} people"); } catch (e) { _log.warning("[_onEventPreloadData] Failed while ListPerson", e); } try { final locations = await ListLocationGroup(_c)(account); // make sure no duplicates final map = {}; for (final l in locations.name + locations.admin1 + locations.admin2 + locations.countryCode) { map[l.place] = l; } product.addAll(map.values.map((e) => _LocationSearcheable(e))); _log.info( "[_onEventPreloadData] Loaded ${locations.name.length + locations.countryCode.length} locations"); } catch (e) { _log.warning("[_onEventPreloadData] Failed while ListLocationGroup", e); } _setSearchItems(product); } void _setSearchItems(List<_Searcheable> searcheables) { _search.setEntries([]); for (final s in searcheables) { for (final k in s.toKeywords()) { _search.addEntry(k.toCaseInsensitiveString(), value: s); } } } final Account account; late final DiContainer _c; final _search = Woozy<_Searcheable>(limit: 10); static final _log = Logger("bloc.album_search_suggestion.HomeSearchSuggestionBloc"); } abstract class _Searcheable { List toKeywords(); HomeSearchResult toResult(); } class _AlbumSearcheable implements _Searcheable { const _AlbumSearcheable(this.album); @override toKeywords() => [album.name.toCi()]; @override toResult() => HomeSearchAlbumResult(album); final Album album; } class _TagSearcheable implements _Searcheable { const _TagSearcheable(this.tag); @override toKeywords() => [tag.displayName.toCi()]; @override toResult() => HomeSearchTagResult(tag); final Tag tag; } class _PersonSearcheable implements _Searcheable { const _PersonSearcheable(this.person); @override toKeywords() => [person.name.toCi()]; @override toResult() => HomeSearchPersonResult(person); final Person person; } class _LocationSearcheable implements _Searcheable { const _LocationSearcheable(this.location); @override toKeywords() => [location.place.toCi()]; @override toResult() => HomeSearchLocationResult(location); final LocationGroup location; }