2021-11-19 18:42:55 +01:00
|
|
|
import 'package:bloc/bloc.dart';
|
2022-07-25 07:51:52 +02:00
|
|
|
import 'package:collection/collection.dart';
|
2021-11-19 18:42:55 +01:00
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:nc_photos/ci_string.dart';
|
|
|
|
import 'package:nc_photos/iterable_extension.dart';
|
2022-12-16 16:01:04 +01:00
|
|
|
import 'package:np_codegen/np_codegen.dart';
|
2022-12-08 16:39:13 +01:00
|
|
|
import 'package:to_string/to_string.dart';
|
2021-11-19 18:42:55 +01:00
|
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
import 'package:woozy_search/woozy_search.dart';
|
|
|
|
|
2022-12-08 16:39:13 +01:00
|
|
|
part 'search_suggestion.g.dart';
|
|
|
|
|
2021-11-19 18:42:55 +01:00
|
|
|
abstract class SearchSuggestionBlocEvent<T> {
|
|
|
|
const SearchSuggestionBlocEvent();
|
|
|
|
}
|
|
|
|
|
2022-12-08 16:39:13 +01:00
|
|
|
@toString
|
2021-11-19 18:42:55 +01:00
|
|
|
class SearchSuggestionBlocUpdateItemsEvent<T>
|
|
|
|
extends SearchSuggestionBlocEvent<T> {
|
|
|
|
const SearchSuggestionBlocUpdateItemsEvent(this.items);
|
|
|
|
|
|
|
|
@override
|
2022-12-08 16:39:13 +01:00
|
|
|
String toString() => _$toString();
|
2021-11-19 18:42:55 +01:00
|
|
|
|
|
|
|
final List<T> items;
|
|
|
|
}
|
|
|
|
|
2022-12-08 16:39:13 +01:00
|
|
|
@toString
|
2021-11-19 18:42:55 +01:00
|
|
|
class SearchSuggestionBlocSearchEvent<T> extends SearchSuggestionBlocEvent<T> {
|
|
|
|
const SearchSuggestionBlocSearchEvent(this.phrase);
|
|
|
|
|
|
|
|
@override
|
2022-12-08 16:39:13 +01:00
|
|
|
String toString() => _$toString();
|
2021-11-19 18:42:55 +01:00
|
|
|
|
|
|
|
final CiString phrase;
|
|
|
|
}
|
|
|
|
|
2022-12-08 16:39:13 +01:00
|
|
|
@toString
|
2021-11-19 18:42:55 +01:00
|
|
|
abstract class SearchSuggestionBlocState<T> {
|
|
|
|
const SearchSuggestionBlocState(this.results);
|
|
|
|
|
|
|
|
@override
|
2022-12-08 16:39:13 +01:00
|
|
|
String toString() => _$toString();
|
2021-11-19 18:42:55 +01:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-12-16 16:01:04 +01:00
|
|
|
@npLog
|
2021-11-19 18:42:55 +01:00
|
|
|
class SearchSuggestionBloc<T>
|
|
|
|
extends Bloc<SearchSuggestionBlocEvent, SearchSuggestionBlocState<T>> {
|
|
|
|
SearchSuggestionBloc({
|
|
|
|
required this.itemToKeywords,
|
2022-07-09 07:59:09 +02:00
|
|
|
}) : super(SearchSuggestionBlocInit<T>()) {
|
|
|
|
on<SearchSuggestionBlocEvent>(_onEvent);
|
|
|
|
}
|
2021-11-19 18:42:55 +01:00
|
|
|
|
2022-07-09 07:59:09 +02:00
|
|
|
Future<void> _onEvent(SearchSuggestionBlocEvent event,
|
|
|
|
Emitter<SearchSuggestionBlocState<T>> emit) async {
|
|
|
|
_log.info("[_onEvent] $event");
|
2021-11-19 18:42:55 +01:00
|
|
|
if (event is SearchSuggestionBlocSearchEvent) {
|
2022-07-09 07:59:09 +02:00
|
|
|
await _onEventSearch(event, emit);
|
2021-11-19 18:42:55 +01:00
|
|
|
} else if (event is SearchSuggestionBlocUpdateItemsEvent<T>) {
|
2022-07-09 07:59:09 +02:00
|
|
|
await _onEventUpdateItems(event, emit);
|
2021-11-19 18:42:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-09 07:59:09 +02:00
|
|
|
Future<void> _onEventSearch(SearchSuggestionBlocSearchEvent ev,
|
|
|
|
Emitter<SearchSuggestionBlocState<T>> emit) async {
|
|
|
|
emit(SearchSuggestionBlocLoading(state.results));
|
2021-11-19 18:42:55 +01:00
|
|
|
// 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();
|
2022-07-09 07:59:09 +02:00
|
|
|
emit(SearchSuggestionBlocSuccess(matches));
|
2021-11-19 18:42:55 +01:00
|
|
|
_lastSearch = ev;
|
|
|
|
}
|
|
|
|
|
2022-07-09 07:59:09 +02:00
|
|
|
Future<void> _onEventUpdateItems(SearchSuggestionBlocUpdateItemsEvent<T> ev,
|
|
|
|
Emitter<SearchSuggestionBlocState<T>> emit) async {
|
2021-11-19 18:42:55 +01:00
|
|
|
_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
|
2022-07-09 07:59:09 +02:00
|
|
|
await _onEventSearch(_lastSearch!, emit);
|
2021-11-19 18:42:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final List<CiString> Function(T item) itemToKeywords;
|
|
|
|
|
|
|
|
final _search = Woozy(limit: 5);
|
|
|
|
SearchSuggestionBlocSearchEvent? _lastSearch;
|
|
|
|
}
|