nc-photos/app/lib/bloc/search_suggestion.dart

135 lines
3.9 KiB
Dart
Raw Normal View History

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<T> {
const SearchSuggestionBlocEvent();
}
class SearchSuggestionBlocUpdateItemsEvent<T>
extends SearchSuggestionBlocEvent<T> {
const SearchSuggestionBlocUpdateItemsEvent(this.items);
@override
toString() {
return "$runtimeType {"
"items: List {legth: ${items.length}}, "
"}";
}
final List<T> items;
}
class SearchSuggestionBlocSearchEvent<T> extends SearchSuggestionBlocEvent<T> {
const SearchSuggestionBlocSearchEvent(this.phrase);
@override
toString() {
return "$runtimeType {"
"phrase: '$phrase', "
"}";
}
final CiString phrase;
}
abstract class SearchSuggestionBlocState<T> {
const SearchSuggestionBlocState(this.results);
@override
toString() {
return "$runtimeType {"
"results: List {legth: ${results.length}}, "
"}";
}
final List<T> results;
}
class SearchSuggestionBlocInit<T> extends SearchSuggestionBlocState<T> {
const SearchSuggestionBlocInit() : super(const []);
}
class SearchSuggestionBlocLoading<T> extends SearchSuggestionBlocState<T> {
const SearchSuggestionBlocLoading(List<T> results) : super(results);
}
class SearchSuggestionBlocSuccess<T> extends SearchSuggestionBlocState<T> {
const SearchSuggestionBlocSuccess(List<T> results) : super(results);
}
class SearchSuggestionBloc<T>
extends Bloc<SearchSuggestionBlocEvent, SearchSuggestionBlocState<T>> {
SearchSuggestionBloc({
required this.itemToKeywords,
}) : super(SearchSuggestionBlocInit<T>());
@override
mapEventToState(SearchSuggestionBlocEvent event) async* {
_log.info("[mapEventToState] $event");
if (event is SearchSuggestionBlocSearchEvent) {
yield* _onEventSearch(event);
} else if (event is SearchSuggestionBlocUpdateItemsEvent<T>) {
yield* _onEventUpdateItems(event);
}
}
Stream<SearchSuggestionBlocState<T>> _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<SearchSuggestionBlocState<T>> _onEventUpdateItems(
SearchSuggestionBlocUpdateItemsEvent<T> 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<CiString> Function(T item) itemToKeywords;
final _search = Woozy(limit: 5);
SearchSuggestionBlocSearchEvent? _lastSearch;
static final _log =
Logger("bloc.album_search_suggestion.SearchSuggestionBloc");
}