mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Refactor: make search suggestion bloc generic
This commit is contained in:
parent
5d63a5bcb3
commit
ebcfc03815
4 changed files with 146 additions and 131 deletions
|
@ -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<Album> 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<Album> results;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumSearchSuggestionBlocInit extends AlbumSearchSuggestionBlocState {
|
|
||||||
const AlbumSearchSuggestionBlocInit() : super(const []);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumSearchSuggestionBlocSuccess extends AlbumSearchSuggestionBlocState {
|
|
||||||
const AlbumSearchSuggestionBlocSuccess(List<Album> results) : super(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumSearchSuggestionBloc extends Bloc<AlbumSearchSuggestionBlocEvent,
|
|
||||||
AlbumSearchSuggestionBlocState> {
|
|
||||||
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<AlbumSearchSuggestionBlocState> _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<AlbumSearchSuggestionBlocState> _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");
|
|
||||||
}
|
|
134
lib/bloc/search_suggestion.dart
Normal file
134
lib/bloc/search_suggestion.dart
Normal file
|
@ -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<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");
|
||||||
|
}
|
|
@ -79,6 +79,8 @@ class CiString implements Comparable<Object> {
|
||||||
@override
|
@override
|
||||||
toString() => raw;
|
toString() => raw;
|
||||||
|
|
||||||
|
String toCaseInsensitiveString() => _lower;
|
||||||
|
|
||||||
final String raw;
|
final String raw;
|
||||||
final String _lower;
|
final String _lower;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/app_db.dart';
|
import 'package:nc_photos/app_db.dart';
|
||||||
import 'package:nc_photos/app_localizations.dart';
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
import 'package:nc_photos/bloc/album_search.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/album.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file/data_source.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) {
|
ListAlbum(fileRepo, albumRepo)(account).toList().then((value) {
|
||||||
final albums = value.whereType<Album>().toList();
|
final albums = value.whereType<Album>().toList();
|
||||||
_searchBloc.add(AlbumSearchBlocUpdateItemsEvent(albums));
|
_searchBloc.add(AlbumSearchBlocUpdateItemsEvent(albums));
|
||||||
_suggestionBloc.add(AlbumSearchSuggestionBlocUpdateItemsEvent(albums));
|
_suggestionBloc.add(SearchSuggestionBlocUpdateItemsEvent<Album>(albums));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,9 +68,9 @@ class AlbumSearchDelegate extends SearchDelegate {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
buildSuggestions(BuildContext context) {
|
buildSuggestions(BuildContext context) {
|
||||||
_suggestionBloc.add(AlbumSearchSuggestionBlocSearchEvent(query));
|
_suggestionBloc.add(SearchSuggestionBlocSearchEvent<Album>(query.toCi()));
|
||||||
return BlocBuilder<AlbumSearchSuggestionBloc,
|
return BlocBuilder<SearchSuggestionBloc<Album>,
|
||||||
AlbumSearchSuggestionBlocState>(
|
SearchSuggestionBlocState<Album>>(
|
||||||
bloc: _suggestionBloc,
|
bloc: _suggestionBloc,
|
||||||
builder: _buildSuggestionContent,
|
builder: _buildSuggestionContent,
|
||||||
);
|
);
|
||||||
|
@ -116,7 +117,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSuggestionContent(
|
Widget _buildSuggestionContent(
|
||||||
BuildContext context, AlbumSearchSuggestionBlocState state) {
|
BuildContext context, SearchSuggestionBlocState<Album> state) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: state.results
|
children: state.results
|
||||||
|
@ -135,5 +136,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
||||||
final Account account;
|
final Account account;
|
||||||
|
|
||||||
final _searchBloc = AlbumSearchBloc();
|
final _searchBloc = AlbumSearchBloc();
|
||||||
final _suggestionBloc = AlbumSearchSuggestionBloc();
|
final _suggestionBloc = SearchSuggestionBloc<Album>(
|
||||||
|
itemToKeywords: (item) => [item.name.toCi()],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue