diff --git a/lib/bloc/album_search_suggestion.dart b/lib/bloc/album_search_suggestion.dart deleted file mode 100644 index 67b02f5e..00000000 --- a/lib/bloc/album_search_suggestion.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; -import 'package:nc_photos/entity/album.dart'; -import 'package:nc_photos/iterable_extension.dart'; -import 'package:tuple/tuple.dart'; -import 'package:woozy_search/woozy_search.dart'; - -abstract class AlbumSearchSuggestionBlocEvent { - const AlbumSearchSuggestionBlocEvent(); -} - -class AlbumSearchSuggestionBlocUpdateItemsEvent - extends AlbumSearchSuggestionBlocEvent { - const AlbumSearchSuggestionBlocUpdateItemsEvent(this.albums); - - @override - toString() { - return "$runtimeType {" - "albums: List {legth: ${albums.length}}, " - "}"; - } - - final List albums; -} - -class AlbumSearchSuggestionBlocSearchEvent - extends AlbumSearchSuggestionBlocEvent { - const AlbumSearchSuggestionBlocSearchEvent(this.phrase); - - @override - toString() { - return "$runtimeType {" - "phrase: '$phrase', " - "}"; - } - - final String phrase; -} - -abstract class AlbumSearchSuggestionBlocState { - const AlbumSearchSuggestionBlocState(this.results); - - @override - toString() { - return "$runtimeType {" - "results: List {legth: ${results.length}}, " - "}"; - } - - final List results; -} - -class AlbumSearchSuggestionBlocInit extends AlbumSearchSuggestionBlocState { - const AlbumSearchSuggestionBlocInit() : super(const []); -} - -class AlbumSearchSuggestionBlocSuccess extends AlbumSearchSuggestionBlocState { - const AlbumSearchSuggestionBlocSuccess(List results) : super(results); -} - -class AlbumSearchSuggestionBloc extends Bloc { - AlbumSearchSuggestionBloc() : super(const AlbumSearchSuggestionBlocInit()); - - @override - mapEventToState(AlbumSearchSuggestionBlocEvent event) async* { - _log.info("[mapEventToState] $event"); - if (event is AlbumSearchSuggestionBlocSearchEvent) { - yield* _onEventSearch(event); - } else if (event is AlbumSearchSuggestionBlocUpdateItemsEvent) { - yield* _onEventUpdateItems(event); - } - } - - Stream _onEventSearch( - AlbumSearchSuggestionBlocSearchEvent ev) async* { - // doesn't work with upper case - final results = _search.search(ev.phrase.toLowerCase()); - if (kDebugMode) { - final str = results.map((e) => "${e.score}: ${e.text}").join("\n"); - _log.info("[_onEventSearch] Search '${ev.phrase}':\n$str"); - } - final matches = results - .where((element) => element.score > 0) - .map((e) { - if ((e.value as Album) - .name - .toLowerCase() - .startsWith(ev.phrase.toLowerCase())) { - // prefer names that start exactly with the search phrase - return Tuple2(e.score + 1, e.value as Album); - } else { - return Tuple2(e.score, e.value as Album); - } - }) - .sorted((a, b) { - return a.item1.compareTo(b.item1); - }) - .reversed - .map((e) => e.item2) - .toList(); - yield AlbumSearchSuggestionBlocSuccess(matches); - _lastSearch = ev; - } - - Stream _onEventUpdateItems( - AlbumSearchSuggestionBlocUpdateItemsEvent ev) async* { - _search.setEntries([]); - for (final a in ev.albums) { - _search.addEntry(a.name, value: a); - } - if (_lastSearch != null) { - // search again - yield* _onEventSearch(_lastSearch!); - } - } - - final _search = Woozy(limit: 5); - AlbumSearchSuggestionBlocSearchEvent? _lastSearch; - - static final _log = - Logger("bloc.album_search_suggestion.AlbumSearchSuggestionBloc"); -} diff --git a/lib/bloc/search_suggestion.dart b/lib/bloc/search_suggestion.dart new file mode 100644 index 00000000..e0ac20a0 --- /dev/null +++ b/lib/bloc/search_suggestion.dart @@ -0,0 +1,134 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/ci_string.dart'; +import 'package:nc_photos/iterable_extension.dart'; +import 'package:tuple/tuple.dart'; +import 'package:woozy_search/woozy_search.dart'; + +abstract class SearchSuggestionBlocEvent { + const SearchSuggestionBlocEvent(); +} + +class SearchSuggestionBlocUpdateItemsEvent + extends SearchSuggestionBlocEvent { + const SearchSuggestionBlocUpdateItemsEvent(this.items); + + @override + toString() { + return "$runtimeType {" + "items: List {legth: ${items.length}}, " + "}"; + } + + final List items; +} + +class SearchSuggestionBlocSearchEvent extends SearchSuggestionBlocEvent { + const SearchSuggestionBlocSearchEvent(this.phrase); + + @override + toString() { + return "$runtimeType {" + "phrase: '$phrase', " + "}"; + } + + final CiString phrase; +} + +abstract class SearchSuggestionBlocState { + const SearchSuggestionBlocState(this.results); + + @override + toString() { + return "$runtimeType {" + "results: List {legth: ${results.length}}, " + "}"; + } + + final List results; +} + +class SearchSuggestionBlocInit extends SearchSuggestionBlocState { + const SearchSuggestionBlocInit() : super(const []); +} + +class SearchSuggestionBlocLoading extends SearchSuggestionBlocState { + const SearchSuggestionBlocLoading(List results) : super(results); +} + +class SearchSuggestionBlocSuccess extends SearchSuggestionBlocState { + const SearchSuggestionBlocSuccess(List results) : super(results); +} + +class SearchSuggestionBloc + extends Bloc> { + SearchSuggestionBloc({ + required this.itemToKeywords, + }) : super(SearchSuggestionBlocInit()); + + @override + mapEventToState(SearchSuggestionBlocEvent event) async* { + _log.info("[mapEventToState] $event"); + if (event is SearchSuggestionBlocSearchEvent) { + yield* _onEventSearch(event); + } else if (event is SearchSuggestionBlocUpdateItemsEvent) { + yield* _onEventUpdateItems(event); + } + } + + Stream> _onEventSearch( + SearchSuggestionBlocSearchEvent ev) async* { + yield SearchSuggestionBlocLoading(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.info("[_onEventSearch] Search '${ev.phrase}':\n$str"); + } + final matches = results + .where((element) => element.score > 0) + .map((e) { + if (itemToKeywords(e.value as T) + .any((k) => k.startsWith(ev.phrase))) { + // prefer names that start exactly with the search phrase + return Tuple2(e.score + 1, e.value as T); + } else { + return Tuple2(e.score, e.value as T); + } + }) + .sorted((a, b) => a.item1.compareTo(b.item1)) + .reversed + .distinctIf( + (a, b) => identical(a.item2, b.item2), + (a) => a.item2.hashCode, + ) + .map((e) => e.item2) + .toList(); + yield SearchSuggestionBlocSuccess(matches); + _lastSearch = ev; + } + + Stream> _onEventUpdateItems( + SearchSuggestionBlocUpdateItemsEvent ev) async* { + _search.setEntries([]); + for (final a in ev.items) { + for (final k in itemToKeywords(a)) { + _search.addEntry(k.toCaseInsensitiveString(), value: a); + } + } + if (_lastSearch != null) { + // search again + yield* _onEventSearch(_lastSearch!); + } + } + + final List Function(T item) itemToKeywords; + + final _search = Woozy(limit: 5); + SearchSuggestionBlocSearchEvent? _lastSearch; + + static final _log = + Logger("bloc.album_search_suggestion.SearchSuggestionBloc"); +} diff --git a/lib/ci_string.dart b/lib/ci_string.dart index cf59b119..8e63ab06 100644 --- a/lib/ci_string.dart +++ b/lib/ci_string.dart @@ -79,6 +79,8 @@ class CiString implements Comparable { @override toString() => raw; + String toCaseInsensitiveString() => _lower; + final String raw; final String _lower; } diff --git a/lib/widget/album_search_delegate.dart b/lib/widget/album_search_delegate.dart index 8934ac01..f9f8df68 100644 --- a/lib/widget/album_search_delegate.dart +++ b/lib/widget/album_search_delegate.dart @@ -5,7 +5,8 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_db.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc/album_search.dart'; -import 'package:nc_photos/bloc/album_search_suggestion.dart'; +import 'package:nc_photos/bloc/search_suggestion.dart'; +import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file/data_source.dart'; @@ -26,7 +27,7 @@ class AlbumSearchDelegate extends SearchDelegate { ListAlbum(fileRepo, albumRepo)(account).toList().then((value) { final albums = value.whereType().toList(); _searchBloc.add(AlbumSearchBlocUpdateItemsEvent(albums)); - _suggestionBloc.add(AlbumSearchSuggestionBlocUpdateItemsEvent(albums)); + _suggestionBloc.add(SearchSuggestionBlocUpdateItemsEvent(albums)); }); } @@ -67,9 +68,9 @@ class AlbumSearchDelegate extends SearchDelegate { @override buildSuggestions(BuildContext context) { - _suggestionBloc.add(AlbumSearchSuggestionBlocSearchEvent(query)); - return BlocBuilder( + _suggestionBloc.add(SearchSuggestionBlocSearchEvent(query.toCi())); + return BlocBuilder, + SearchSuggestionBlocState>( bloc: _suggestionBloc, builder: _buildSuggestionContent, ); @@ -116,7 +117,7 @@ class AlbumSearchDelegate extends SearchDelegate { } Widget _buildSuggestionContent( - BuildContext context, AlbumSearchSuggestionBlocState state) { + BuildContext context, SearchSuggestionBlocState state) { return SingleChildScrollView( child: Column( children: state.results @@ -135,5 +136,7 @@ class AlbumSearchDelegate extends SearchDelegate { final Account account; final _searchBloc = AlbumSearchBloc(); - final _suggestionBloc = AlbumSearchSuggestionBloc(); + final _suggestionBloc = SearchSuggestionBloc( + itemToKeywords: (item) => [item.name.toCi()], + ); }