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

319 lines
9.1 KiB
Dart
Raw Normal View History

2022-08-06 06:21:11 +02:00
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
2023-05-23 18:47:32 +02:00
import 'package:flutter_bloc/flutter_bloc.dart';
2022-08-06 06:21:11 +02:00
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
2023-07-03 19:23:42 +02:00
import 'package:nc_photos/controller/account_pref_controller.dart';
import 'package:nc_photos/controller/collections_controller.dart';
import 'package:nc_photos/controller/server_controller.dart';
2022-08-06 06:21:11 +02:00
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/collection.dart';
2022-08-06 06:21:11 +02:00
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/use_case/collection/list_collection.dart';
2022-08-28 18:07:50 +02:00
import 'package:nc_photos/use_case/list_location_group.dart';
2022-08-06 06:21:11 +02:00
import 'package:nc_photos/use_case/list_tag.dart';
2023-07-03 19:23:42 +02:00
import 'package:nc_photos/use_case/person/list_person.dart';
2022-12-16 16:01:04 +01:00
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
2023-08-25 18:37:17 +02:00
import 'package:np_string/np_string.dart';
2022-12-08 16:39:13 +01:00
import 'package:to_string/to_string.dart';
2022-08-06 06:21:11 +02:00
import 'package:tuple/tuple.dart';
import 'package:woozy_search/woozy_search.dart';
2022-12-08 16:39:13 +01:00
part 'home_search_suggestion.g.dart';
2022-08-06 06:21:11 +02:00
abstract class HomeSearchResult {}
2022-12-08 16:39:13 +01:00
@toString
class HomeSearchCollectionResult implements HomeSearchResult {
const HomeSearchCollectionResult(this.collection);
2022-08-06 06:21:11 +02:00
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-06 06:21:11 +02:00
final Collection collection;
2022-08-06 06:21:11 +02:00
}
2022-12-08 16:39:13 +01:00
@toString
2022-08-06 06:21:11 +02:00
class HomeSearchTagResult implements HomeSearchResult {
const HomeSearchTagResult(this.tag);
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-06 06:21:11 +02:00
final Tag tag;
}
2022-12-08 16:39:13 +01:00
@toString
2022-08-06 06:21:11 +02:00
class HomeSearchPersonResult implements HomeSearchResult {
const HomeSearchPersonResult(this.person);
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-06 06:21:11 +02:00
final Person person;
}
2022-12-08 16:39:13 +01:00
@toString
2022-08-28 18:07:50 +02:00
class HomeSearchLocationResult implements HomeSearchResult {
const HomeSearchLocationResult(this.location);
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-28 18:07:50 +02:00
final LocationGroup location;
}
2022-08-06 06:21:11 +02:00
abstract class HomeSearchSuggestionBlocEvent {
const HomeSearchSuggestionBlocEvent();
}
2022-12-08 16:39:13 +01:00
@toString
2022-08-06 06:21:11 +02:00
class HomeSearchSuggestionBlocPreloadData
extends HomeSearchSuggestionBlocEvent {
const HomeSearchSuggestionBlocPreloadData();
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-06 06:21:11 +02:00
}
2022-12-08 16:39:13 +01:00
@toString
2022-08-06 06:21:11 +02:00
class HomeSearchSuggestionBlocSearch extends HomeSearchSuggestionBlocEvent {
const HomeSearchSuggestionBlocSearch(this.phrase);
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-06 06:21:11 +02:00
final CiString phrase;
}
2022-12-08 16:39:13 +01:00
@toString
2022-08-06 06:21:11 +02:00
abstract class HomeSearchSuggestionBlocState {
const HomeSearchSuggestionBlocState(this.results);
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-06 06:21:11 +02:00
final List<HomeSearchResult> results;
}
class HomeSearchSuggestionBlocInit extends HomeSearchSuggestionBlocState {
const HomeSearchSuggestionBlocInit() : super(const []);
}
class HomeSearchSuggestionBlocLoading extends HomeSearchSuggestionBlocState {
const HomeSearchSuggestionBlocLoading(List<HomeSearchResult> results)
: super(results);
}
class HomeSearchSuggestionBlocSuccess extends HomeSearchSuggestionBlocState {
const HomeSearchSuggestionBlocSuccess(List<HomeSearchResult> results)
: super(results);
}
2022-12-08 16:39:13 +01:00
@toString
2022-08-06 06:21:11 +02:00
class HomeSearchSuggestionBlocFailure extends HomeSearchSuggestionBlocState {
const HomeSearchSuggestionBlocFailure(
List<HomeSearchTagResult> results, this.exception)
: super(results);
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-08-06 06:21:11 +02:00
final Object exception;
}
2022-12-16 16:01:04 +01:00
@npLog
2022-08-06 06:21:11 +02:00
class HomeSearchSuggestionBloc
extends Bloc<HomeSearchSuggestionBlocEvent, HomeSearchSuggestionBlocState> {
HomeSearchSuggestionBloc(
2023-07-03 19:23:42 +02:00
this.account,
this.collectionsController,
this.serverController,
this.accountPrefController,
) : super(const HomeSearchSuggestionBlocInit()) {
2022-08-06 06:21:11 +02:00
final c = KiwiContainer().resolve<DiContainer>();
assert(require(c));
assert(ListTag.require(c));
_c = c.withLocalRepo();
on<HomeSearchSuggestionBlocEvent>(_onEvent);
add(const HomeSearchSuggestionBlocPreloadData());
}
static bool require(DiContainer c) => true;
Future<void> _onEvent(HomeSearchSuggestionBlocEvent event,
Emitter<HomeSearchSuggestionBlocState> emit) async {
_log.info("[_onEvent] $event");
if (event is HomeSearchSuggestionBlocSearch) {
await _onEventSearch(event, emit);
} else if (event is HomeSearchSuggestionBlocPreloadData) {
await _onEventPreloadData(event, emit);
}
}
Future<void> _onEventSearch(HomeSearchSuggestionBlocSearch ev,
Emitter<HomeSearchSuggestionBlocState> 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<void> _onEventPreloadData(HomeSearchSuggestionBlocPreloadData ev,
Emitter<HomeSearchSuggestionBlocState> emit) async {
final product = <_Searcheable>[];
try {
var collections = collectionsController
.peekStream()
.data
.map((e) => e.collection)
2022-08-06 06:21:11 +02:00
.toList();
if (collections.isEmpty) {
collections = await ListCollection(_c,
serverController: serverController)(account)
.last;
}
product.addAll(collections.map(_CollectionSearcheable.new));
_log.info(
"[_onEventPreloadData] Loaded ${collections.length} collections");
2022-08-06 06:21:11 +02:00
} catch (e) {
_log.warning("[_onEventPreloadData] Failed while ListCollection", e);
2022-08-06 06:21:11 +02:00
}
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, accountPrefController.personProviderValue)
.last;
2022-08-06 06:21:11 +02:00
product.addAll(persons.map((t) => _PersonSearcheable(t)));
_log.info("[_onEventPreloadData] Loaded ${persons.length} people");
} catch (e) {
_log.warning("[_onEventPreloadData] Failed while ListPerson", e);
}
2022-08-28 18:07:50 +02:00
try {
final locations = await ListLocationGroup(_c)(account);
// make sure no duplicates
2022-08-28 18:07:50 +02:00
final map = <String, LocationGroup>{};
for (final l in locations.name +
locations.admin1 +
locations.admin2 +
locations.countryCode) {
2022-08-28 18:07:50 +02:00
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);
}
2022-08-06 06:21:11 +02:00
_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;
final CollectionsController collectionsController;
final ServerController serverController;
2023-07-03 19:23:42 +02:00
final AccountPrefController accountPrefController;
2022-08-06 06:21:11 +02:00
late final DiContainer _c;
final _search = Woozy<_Searcheable>(limit: 10);
}
abstract class _Searcheable {
List<CiString> toKeywords();
HomeSearchResult toResult();
}
class _CollectionSearcheable implements _Searcheable {
const _CollectionSearcheable(this.collection);
2022-08-06 06:21:11 +02:00
@override
toKeywords() => [collection.name.toCi()];
2022-08-06 06:21:11 +02:00
@override
toResult() => HomeSearchCollectionResult(collection);
2022-08-06 06:21:11 +02:00
final Collection collection;
2022-08-06 06:21:11 +02:00
}
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;
}
2022-08-28 18:07:50 +02:00
class _LocationSearcheable implements _Searcheable {
const _LocationSearcheable(this.location);
@override
toKeywords() => [location.place.toCi()];
@override
toResult() => HomeSearchLocationResult(location);
final LocationGroup location;
}