Merge branch 'face-rewrite' into dev

This commit is contained in:
Ming Ming 2023-07-23 03:08:44 +08:00
commit 56e3fbd784
122 changed files with 5068 additions and 1411 deletions

View file

@ -1,12 +1,14 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/face.dart';
import 'package:nc_photos/entity/face_recognition_face.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/nc_album.dart';
import 'package:nc_photos/entity/nc_album_item.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/server_status.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
@ -20,15 +22,25 @@ import 'package:np_common/string_extension.dart';
part 'entity_converter.g.dart';
class ApiFaceConverter {
static Face fromApi(api.Face face) {
return Face(
class ApiFaceRecognitionFaceConverter {
static FaceRecognitionFace fromApi(api.FaceRecognitionFace face) {
return FaceRecognitionFace(
id: face.id,
fileId: face.fileId,
);
}
}
class ApiFaceRecognitionPersonConverter {
static FaceRecognitionPerson fromApi(api.FaceRecognitionPerson person) {
return FaceRecognitionPerson(
name: person.name,
thumbFaceId: person.thumbFaceId,
count: person.count,
);
}
}
class ApiFavoriteConverter {
static Favorite fromApi(api.Favorite favorite) {
return Favorite(
@ -121,12 +133,46 @@ class ApiNcAlbumItemConverter {
}
}
class ApiPersonConverter {
static Person fromApi(api.Person person) {
return Person(
name: person.name,
thumbFaceId: person.thumbFaceId,
count: person.count,
class ApiRecognizeFaceConverter {
static RecognizeFace fromApi(api.RecognizeFace item) {
// remote.php/dav/recognize/admin/faces/john
var path = _hrefToPath(item.href);
if (!path.startsWith("remote.php/dav/recognize/")) {
throw ArgumentError("Invalid face path: ${item.href}");
}
// admin/faces/john
path = path.substring(25);
final found = path.indexOf("/");
if (found == -1) {
throw ArgumentError("Invalid face path: ${item.href}");
}
// faces/john
path = path.substring(found + 1);
if (!path.startsWith("faces")) {
throw ArgumentError("Invalid face path: ${item.href}");
}
// john
path = path.slice(6);
return RecognizeFace(label: path);
}
}
class ApiRecognizeFaceItemConverter {
static RecognizeFaceItem fromApi(api.RecognizeFaceItem item) {
return RecognizeFaceItem(
path: _hrefToPath(item.href),
fileId: item.fileId!,
contentLength: item.contentLength,
contentType: item.contentType,
etag: item.etag,
lastModified: item.lastModified,
hasPreview: item.hasPreview,
realPath: item.realPath,
isFavorite: item.favorite,
fileMetadataWidth: item.fileMetadataSize?["width"],
fileMetadataHeight: item.fileMetadataSize?["height"],
faceDetections:
item.faceDetections?.isEmpty == true ? null : item.faceDetections,
);
}
}
@ -210,7 +256,7 @@ class ApiTaggedFileConverter {
}
String _hrefToPath(String href) {
final rawPath = href.trimLeftAny("/");
final rawPath = href.trimAny("/");
final pos = rawPath.indexOf("remote.php");
if (pos == -1) {
// what?

View file

@ -10,8 +10,8 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/data_source.dart';
import 'package:nc_photos/entity/album/data_source2.dart';
import 'package:nc_photos/entity/album/repo2.dart';
import 'package:nc_photos/entity/face.dart';
import 'package:nc_photos/entity/face/data_source.dart';
import 'package:nc_photos/entity/face_recognition_person/data_source.dart';
import 'package:nc_photos/entity/face_recognition_person/repo.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/favorite/data_source.dart';
import 'package:nc_photos/entity/file.dart';
@ -20,11 +20,11 @@ import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/entity/local_file/data_source.dart';
import 'package:nc_photos/entity/nc_album/data_source.dart';
import 'package:nc_photos/entity/nc_album/repo.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/person/data_source.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref/provider/shared_preferences.dart';
import 'package:nc_photos/entity/pref_util.dart' as pref_util;
import 'package:nc_photos/entity/recognize_face/data_source.dart';
import 'package:nc_photos/entity/recognize_face/repo.dart';
import 'package:nc_photos/entity/search.dart';
import 'package:nc_photos/entity/search/data_source.dart';
import 'package:nc_photos/entity/share.dart';
@ -209,13 +209,9 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
const AlbumRemoteDataSource2(), AlbumSqliteDbDataSource2(c.sqliteDb));
c.albumRepo2Remote = const BasicAlbumRepo2(AlbumRemoteDataSource2());
c.albumRepo2Local = BasicAlbumRepo2(AlbumSqliteDbDataSource2(c.sqliteDb));
c.faceRepo = const FaceRepo(FaceRemoteDataSource());
c.fileRepo = FileRepo(FileCachedDataSource(c));
c.fileRepoRemote = const FileRepo(FileWebdavDataSource());
c.fileRepoLocal = FileRepo(FileSqliteDbDataSource(c));
c.personRepo = const PersonRepo(PersonRemoteDataSource());
c.personRepoRemote = const PersonRepo(PersonRemoteDataSource());
c.personRepoLocal = PersonRepo(PersonSqliteDbDataSource(c.sqliteDb));
c.shareRepo = ShareRepo(ShareRemoteDataSource());
c.shareeRepo = ShareeRepo(ShareeRemoteDataSource());
c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource());
@ -228,6 +224,18 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
const NcAlbumRemoteDataSource(), NcAlbumSqliteDbDataSource(c.sqliteDb));
c.ncAlbumRepoRemote = const BasicNcAlbumRepo(NcAlbumRemoteDataSource());
c.ncAlbumRepoLocal = BasicNcAlbumRepo(NcAlbumSqliteDbDataSource(c.sqliteDb));
c.faceRecognitionPersonRepo = const BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonRemoteDataSource());
c.faceRecognitionPersonRepoRemote = const BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonRemoteDataSource());
c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
c.recognizeFaceRepo =
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
c.recognizeFaceRepoRemote =
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
c.recognizeFaceRepoLocal =
BasicRecognizeFaceRepo(RecognizeFaceSqliteDbDataSource(c.sqliteDb));
c.touchManager = TouchManager(c);

View file

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
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';
import 'package:nc_photos/di_container.dart';
@ -13,8 +14,8 @@ import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/use_case/collection/list_collection.dart';
import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:nc_photos/use_case/list_person.dart';
import 'package:nc_photos/use_case/list_tag.dart';
import 'package:nc_photos/use_case/person/list_person.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/ci_string.dart';
import 'package:to_string/to_string.dart';
@ -128,8 +129,11 @@ class HomeSearchSuggestionBlocFailure extends HomeSearchSuggestionBlocState {
class HomeSearchSuggestionBloc
extends Bloc<HomeSearchSuggestionBlocEvent, HomeSearchSuggestionBlocState> {
HomeSearchSuggestionBloc(
this.account, this.collectionsController, this.serverController)
: super(const HomeSearchSuggestionBlocInit()) {
this.account,
this.collectionsController,
this.serverController,
this.accountPrefController,
) : super(const HomeSearchSuggestionBlocInit()) {
final c = KiwiContainer().resolve<DiContainer>();
assert(require(c));
assert(ListTag.require(c));
@ -214,7 +218,8 @@ class HomeSearchSuggestionBloc
_log.warning("[_onEventPreloadData] Failed while ListTag", e);
}
try {
final persons = await ListPerson(_c)(account);
final persons =
await ListPerson(_c)(account, accountPrefController.raw).last;
product.addAll(persons.map((t) => _PersonSearcheable(t)));
_log.info("[_onEventPreloadData] Loaded ${persons.length} people");
} catch (e) {
@ -252,6 +257,7 @@ class HomeSearchSuggestionBloc
final Account account;
final CollectionsController collectionsController;
final ServerController serverController;
final AccountPrefController accountPrefController;
late final DiContainer _c;
final _search = Woozy<_Searcheable>(limit: 10);

View file

@ -1,99 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/use_case/list_person.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
part 'list_person.g.dart';
abstract class ListPersonBlocEvent {
const ListPersonBlocEvent();
}
@toString
class ListPersonBlocQuery extends ListPersonBlocEvent {
const ListPersonBlocQuery(this.account);
@override
String toString() => _$toString();
final Account account;
}
@toString
abstract class ListPersonBlocState {
const ListPersonBlocState(this.account, this.items);
@override
String toString() => _$toString();
final Account? account;
final List<Person> items;
}
class ListPersonBlocInit extends ListPersonBlocState {
ListPersonBlocInit() : super(null, const []);
}
class ListPersonBlocLoading extends ListPersonBlocState {
const ListPersonBlocLoading(Account? account, List<Person> items)
: super(account, items);
}
class ListPersonBlocSuccess extends ListPersonBlocState {
const ListPersonBlocSuccess(Account? account, List<Person> items)
: super(account, items);
}
@toString
class ListPersonBlocFailure extends ListPersonBlocState {
const ListPersonBlocFailure(
Account? account, List<Person> items, this.exception)
: super(account, items);
@override
String toString() => _$toString();
final Object exception;
}
/// List all people recognized in an account
@npLog
class ListPersonBloc extends Bloc<ListPersonBlocEvent, ListPersonBlocState> {
ListPersonBloc(this._c)
: assert(require(_c)),
assert(ListPerson.require(_c)),
super(ListPersonBlocInit()) {
on<ListPersonBlocEvent>(_onEvent);
}
static bool require(DiContainer c) => true;
Future<void> _onEvent(
ListPersonBlocEvent event, Emitter<ListPersonBlocState> emit) async {
_log.info("[_onEvent] $event");
if (event is ListPersonBlocQuery) {
await _onEventQuery(event, emit);
}
}
Future<void> _onEventQuery(
ListPersonBlocQuery ev, Emitter<ListPersonBlocState> emit) async {
try {
emit(ListPersonBlocLoading(ev.account, state.items));
emit(ListPersonBlocSuccess(ev.account, await _query(ev)));
} catch (e, stackTrace) {
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
emit(ListPersonBlocFailure(ev.account, state.items, e));
}
}
Future<List<Person>> _query(ListPersonBlocQuery ev) =>
ListPerson(_c.withLocalRepo())(ev.account);
final DiContainer _c;
}

View file

@ -1,39 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'list_person.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$ListPersonBlocNpLog on ListPersonBloc {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("bloc.list_person.ListPersonBloc");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$ListPersonBlocQueryToString on ListPersonBlocQuery {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "ListPersonBlocQuery {account: $account}";
}
}
extension _$ListPersonBlocStateToString on ListPersonBlocState {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "${objectRuntimeType(this, "ListPersonBlocState")} {account: $account, items: [length: ${items.length}]}";
}
}
extension _$ListPersonBlocFailureToString on ListPersonBlocFailure {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "ListPersonBlocFailure {account: $account, items: [length: ${items.length}], exception: $exception}";
}
}

View file

@ -2,10 +2,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/controller/account_pref_controller.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:nc_photos/use_case/list_person.dart';
import 'package:nc_photos/use_case/person/list_person.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
@ -17,12 +18,13 @@ abstract class SearchLandingBlocEvent {
@toString
class SearchLandingBlocQuery extends SearchLandingBlocEvent {
const SearchLandingBlocQuery(this.account);
const SearchLandingBlocQuery(this.account, this.accountPrefController);
@override
String toString() => _$toString();
final Account account;
final AccountPrefController accountPrefController;
}
@toString
@ -69,15 +71,10 @@ class SearchLandingBlocFailure extends SearchLandingBlocState {
@npLog
class SearchLandingBloc
extends Bloc<SearchLandingBlocEvent, SearchLandingBlocState> {
SearchLandingBloc(this._c)
: assert(require(_c)),
assert(ListPerson.require(_c)),
super(SearchLandingBlocInit()) {
SearchLandingBloc(this._c) : super(SearchLandingBlocInit()) {
on<SearchLandingBlocEvent>(_onEvent);
}
static bool require(DiContainer c) => true;
Future<void> _onEvent(SearchLandingBlocEvent event,
Emitter<SearchLandingBlocState> emit) async {
_log.info("[_onEvent] $event");
@ -117,7 +114,8 @@ class SearchLandingBloc
}
Future<List<Person>> _queryPeople(SearchLandingBlocQuery ev) =>
ListPerson(_c.withLocalRepo())(ev.account);
ListPerson(_c.withLocalRepo())(ev.account, ev.accountPrefController.raw)
.last;
Future<LocationGroupResult> _queryLocations(SearchLandingBlocQuery ev) =>
ListLocationGroup(_c.withLocalRepo())(ev.account);

View file

@ -20,7 +20,7 @@ extension _$SearchLandingBlocNpLog on SearchLandingBloc {
extension _$SearchLandingBlocQueryToString on SearchLandingBlocQuery {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "SearchLandingBlocQuery {account: $account}";
return "SearchLandingBlocQuery {account: $account, accountPrefController: $accountPrefController}";
}
}

View file

@ -2,7 +2,9 @@ import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/controller/account_pref_controller.dart';
import 'package:nc_photos/controller/collections_controller.dart';
import 'package:nc_photos/controller/persons_controller.dart';
import 'package:nc_photos/controller/server_controller.dart';
import 'package:nc_photos/controller/sync_controller.dart';
import 'package:nc_photos/di_container.dart';
class AccountController {
@ -14,6 +16,10 @@ class AccountController {
_serverController = null;
_accountPrefController?.dispose();
_accountPrefController = null;
_personsController?.dispose();
_personsController = null;
_syncController?.dispose();
_syncController = null;
}
Account get account => _account!;
@ -35,8 +41,21 @@ class AccountController {
account: _account!,
);
PersonsController get personsController =>
_personsController ??= PersonsController(
KiwiContainer().resolve<DiContainer>(),
account: _account!,
accountPrefController: accountPrefController,
);
SyncController get syncController => _syncController ??= SyncController(
account: _account!,
);
Account? _account;
CollectionsController? _collectionsController;
ServerController? _serverController;
AccountPrefController? _accountPrefController;
PersonsController? _personsController;
SyncController? _syncController;
}

View file

@ -1,5 +1,6 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:rxdart/rxdart.dart';
@ -15,25 +16,7 @@ class AccountPrefController {
void dispose() {
_shareFolderController.close();
_accountLabelController.close();
}
ValueStream<bool> get isEnableFaceRecognitionApp =>
_enableFaceRecognitionAppController.stream;
Future<void> setEnableFaceRecognitionApp(bool value) async {
final backup = _enableFaceRecognitionAppController.value;
_enableFaceRecognitionAppController.add(value);
try {
if (!await _accountPref.setEnableFaceRecognitionApp(value)) {
throw StateError("Unknown error");
}
} catch (e, stackTrace) {
_log.severe("[setEnableFaceRecognitionApp] Failed setting preference", e,
stackTrace);
_enableFaceRecognitionAppController
..addError(e, stackTrace)
..add(backup);
}
_personProviderController.close();
}
ValueStream<String> get shareFolder => _shareFolderController.stream;
@ -70,13 +53,34 @@ class AccountPrefController {
}
}
ValueStream<PersonProvider> get personProvider =>
_personProviderController.stream;
Future<void> setPersonProvider(PersonProvider value) async {
final backup = _personProviderController.value;
_personProviderController.add(value);
try {
if (!await _accountPref.setPersonProvider(value.index)) {
throw StateError("Unknown error");
}
} catch (e, stackTrace) {
_log.severe(
"[setPersonProvider] Failed setting preference", e, stackTrace);
_personProviderController
..addError(e, stackTrace)
..add(backup);
}
}
AccountPref get raw => _accountPref;
final Account account;
final AccountPref _accountPref;
late final _enableFaceRecognitionAppController =
BehaviorSubject.seeded(_accountPref.isEnableFaceRecognitionAppOr(true));
late final _shareFolderController =
BehaviorSubject.seeded(_accountPref.getShareFolderOr(""));
late final _accountLabelController =
BehaviorSubject.seeded(_accountPref.getAccountLabel());
late final _personProviderController = BehaviorSubject.seeded(
PersonProvider.fromValue(_accountPref.getPersonProviderOr()));
}

View file

@ -0,0 +1,88 @@
import 'dart:async';
import 'package:copy_with/copy_with.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/controller/account_pref_controller.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/use_case/person/list_person.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:rxdart/rxdart.dart';
part 'persons_controller.g.dart';
@genCopyWith
class PersonStreamEvent {
const PersonStreamEvent({
required this.data,
required this.hasNext,
});
final List<Person> data;
/// If true, the results are intermediate values and may not represent the
/// latest state
final bool hasNext;
}
@npLog
class PersonsController {
PersonsController(
this._c, {
required this.account,
required this.accountPrefController,
});
void dispose() {
_personStreamContorller.close();
}
/// Return a stream of [Person]s associated with [account]
///
/// There's no guarantee that the returned list is always sorted in some ways,
/// callers must sort it by themselves if the ordering is important
ValueStream<PersonStreamEvent> get stream {
if (!_isPersonStreamInited) {
_isPersonStreamInited = true;
unawaited(_load());
}
return _personStreamContorller.stream;
}
Future<void> reload() async {
if (_isPersonStreamInited) {
return _load();
} else {
_log.warning("[reload] Not inited, ignore");
}
}
Future<void> _load() async {
var lastData = _personStreamContorller.value.copyWith(hasNext: true);
_personStreamContorller.add(lastData);
final completer = Completer();
ListPerson(_c.withLocalRepo())(account, accountPrefController.raw).listen(
(results) {
lastData = PersonStreamEvent(
data: results,
hasNext: true,
);
_personStreamContorller.add(lastData);
},
onError: _personStreamContorller.addError,
onDone: () => completer.complete(),
);
await completer.future;
_personStreamContorller.add(lastData.copyWith(hasNext: false));
}
final DiContainer _c;
final Account account;
final AccountPrefController accountPrefController;
var _isPersonStreamInited = false;
final _personStreamContorller = BehaviorSubject.seeded(
const PersonStreamEvent(data: [], hasNext: true),
);
}

View file

@ -0,0 +1,48 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'persons_controller.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $PersonStreamEventCopyWithWorker {
PersonStreamEvent call({List<Person>? data, bool? hasNext});
}
class _$PersonStreamEventCopyWithWorkerImpl
implements $PersonStreamEventCopyWithWorker {
_$PersonStreamEventCopyWithWorkerImpl(this.that);
@override
PersonStreamEvent call({dynamic data, dynamic hasNext}) {
return PersonStreamEvent(
data: data as List<Person>? ?? that.data,
hasNext: hasNext as bool? ?? that.hasNext);
}
final PersonStreamEvent that;
}
extension $PersonStreamEventCopyWith on PersonStreamEvent {
$PersonStreamEventCopyWithWorker get copyWith => _$copyWith;
$PersonStreamEventCopyWithWorker get _$copyWith =>
_$PersonStreamEventCopyWithWorkerImpl(this);
}
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$PersonsControllerNpLog on PersonsController {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("controller.persons_controller.PersonsController");
}

View file

@ -0,0 +1,51 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/use_case/startup_sync.dart';
class SyncController {
SyncController({
required this.account,
this.onPeopleUpdated,
});
void dispose() {
_isDisposed = true;
}
Future<void> requestSync(
Account account, PersonProvider personProvider) async {
if (_isDisposed) {
return;
}
if (_syncCompleter == null) {
_syncCompleter = Completer();
final result = await StartupSync.runInIsolate(account, personProvider);
if (!_isDisposed && result.isSyncPersonUpdated) {
onPeopleUpdated?.call();
}
_syncCompleter!.complete();
} else {
return _syncCompleter!.future;
}
}
Future<void> requestResync(
Account account, PersonProvider personProvider) async {
if (_syncCompleter?.isCompleted == true) {
_syncCompleter = null;
return requestSync(account, personProvider);
} else {
// already syncing
return requestSync(account, personProvider);
}
}
final Account account;
final VoidCallback? onPeopleUpdated;
Completer<void>? _syncCompleter;
var _isDisposed = false;
}

View file

@ -1,12 +1,12 @@
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/repo2.dart';
import 'package:nc_photos/entity/face.dart';
import 'package:nc_photos/entity/face_recognition_person/repo.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/entity/nc_album/repo.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/recognize_face/repo.dart';
import 'package:nc_photos/entity/search.dart';
import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart';
@ -23,13 +23,9 @@ enum DiType {
albumRepo2,
albumRepo2Remote,
albumRepo2Local,
faceRepo,
fileRepo,
fileRepoRemote,
fileRepoLocal,
personRepo,
personRepoRemote,
personRepoLocal,
shareRepo,
shareeRepo,
favoriteRepo,
@ -42,6 +38,12 @@ enum DiType {
ncAlbumRepo,
ncAlbumRepoRemote,
ncAlbumRepoLocal,
faceRecognitionPersonRepo,
faceRecognitionPersonRepoRemote,
faceRecognitionPersonRepoLocal,
recognizeFaceRepo,
recognizeFaceRepoRemote,
recognizeFaceRepoLocal,
pref,
sqliteDb,
touchManager,
@ -55,13 +57,9 @@ class DiContainer {
AlbumRepo2? albumRepo2,
AlbumRepo2? albumRepo2Remote,
AlbumRepo2? albumRepo2Local,
FaceRepo? faceRepo,
FileRepo? fileRepo,
FileRepo? fileRepoRemote,
FileRepo? fileRepoLocal,
PersonRepo? personRepo,
PersonRepo? personRepoRemote,
PersonRepo? personRepoLocal,
ShareRepo? shareRepo,
ShareeRepo? shareeRepo,
FavoriteRepo? favoriteRepo,
@ -74,6 +72,12 @@ class DiContainer {
NcAlbumRepo? ncAlbumRepo,
NcAlbumRepo? ncAlbumRepoRemote,
NcAlbumRepo? ncAlbumRepoLocal,
FaceRecognitionPersonRepo? faceRecognitionPersonRepo,
FaceRecognitionPersonRepo? faceRecognitionPersonRepoRemote,
FaceRecognitionPersonRepo? faceRecognitionPersonRepoLocal,
RecognizeFaceRepo? recognizeFaceRepo,
RecognizeFaceRepo? recognizeFaceRepoRemote,
RecognizeFaceRepo? recognizeFaceRepoLocal,
Pref? pref,
sql.SqliteDb? sqliteDb,
TouchManager? touchManager,
@ -83,13 +87,9 @@ class DiContainer {
_albumRepo2 = albumRepo2,
_albumRepo2Remote = albumRepo2Remote,
_albumRepo2Local = albumRepo2Local,
_faceRepo = faceRepo,
_fileRepo = fileRepo,
_fileRepoRemote = fileRepoRemote,
_fileRepoLocal = fileRepoLocal,
_personRepo = personRepo,
_personRepoRemote = personRepoRemote,
_personRepoLocal = personRepoLocal,
_shareRepo = shareRepo,
_shareeRepo = shareeRepo,
_favoriteRepo = favoriteRepo,
@ -102,6 +102,12 @@ class DiContainer {
_ncAlbumRepo = ncAlbumRepo,
_ncAlbumRepoRemote = ncAlbumRepoRemote,
_ncAlbumRepoLocal = ncAlbumRepoLocal,
_faceRecognitionPersonRepo = faceRecognitionPersonRepo,
_faceRecognitionPersonRepoRemote = faceRecognitionPersonRepoRemote,
_faceRecognitionPersonRepoLocal = faceRecognitionPersonRepoLocal,
_recognizeFaceRepo = recognizeFaceRepo,
_recognizeFaceRepoRemote = recognizeFaceRepoRemote,
_recognizeFaceRepoLocal = recognizeFaceRepoLocal,
_pref = pref,
_sqliteDb = sqliteDb,
_touchManager = touchManager;
@ -122,20 +128,12 @@ class DiContainer {
return contianer._albumRepo2Remote != null;
case DiType.albumRepo2Local:
return contianer._albumRepo2Local != null;
case DiType.faceRepo:
return contianer._faceRepo != null;
case DiType.fileRepo:
return contianer._fileRepo != null;
case DiType.fileRepoRemote:
return contianer._fileRepoRemote != null;
case DiType.fileRepoLocal:
return contianer._fileRepoLocal != null;
case DiType.personRepo:
return contianer._personRepo != null;
case DiType.personRepoRemote:
return contianer._personRepoRemote != null;
case DiType.personRepoLocal:
return contianer._personRepoLocal != null;
case DiType.shareRepo:
return contianer._shareRepo != null;
case DiType.shareeRepo:
@ -160,6 +158,18 @@ class DiContainer {
return contianer._ncAlbumRepoRemote != null;
case DiType.ncAlbumRepoLocal:
return contianer._ncAlbumRepoLocal != null;
case DiType.faceRecognitionPersonRepo:
return contianer._faceRecognitionPersonRepo != null;
case DiType.faceRecognitionPersonRepoRemote:
return contianer._faceRecognitionPersonRepoRemote != null;
case DiType.faceRecognitionPersonRepoLocal:
return contianer._faceRecognitionPersonRepoLocal != null;
case DiType.recognizeFaceRepo:
return contianer._recognizeFaceRepo != null;
case DiType.recognizeFaceRepoRemote:
return contianer._recognizeFaceRepoRemote != null;
case DiType.recognizeFaceRepoLocal:
return contianer._recognizeFaceRepoLocal != null;
case DiType.pref:
return contianer._pref != null;
case DiType.sqliteDb:
@ -172,9 +182,7 @@ class DiContainer {
DiContainer copyWith({
OrNull<AlbumRepo>? albumRepo,
OrNull<AlbumRepo2>? albumRepo2,
OrNull<FaceRepo>? faceRepo,
OrNull<FileRepo>? fileRepo,
OrNull<PersonRepo>? personRepo,
OrNull<ShareRepo>? shareRepo,
OrNull<ShareeRepo>? shareeRepo,
OrNull<FavoriteRepo>? favoriteRepo,
@ -183,6 +191,8 @@ class DiContainer {
OrNull<LocalFileRepo>? localFileRepo,
OrNull<SearchRepo>? searchRepo,
OrNull<NcAlbumRepo>? ncAlbumRepo,
OrNull<FaceRecognitionPersonRepo>? faceRecognitionPersonRepo,
OrNull<RecognizeFaceRepo>? recognizeFaceRepo,
OrNull<Pref>? pref,
OrNull<sql.SqliteDb>? sqliteDb,
OrNull<TouchManager>? touchManager,
@ -190,9 +200,7 @@ class DiContainer {
return DiContainer(
albumRepo: albumRepo == null ? _albumRepo : albumRepo.obj,
albumRepo2: albumRepo2 == null ? _albumRepo2 : albumRepo2.obj,
faceRepo: faceRepo == null ? _faceRepo : faceRepo.obj,
fileRepo: fileRepo == null ? _fileRepo : fileRepo.obj,
personRepo: personRepo == null ? _personRepo : personRepo.obj,
shareRepo: shareRepo == null ? _shareRepo : shareRepo.obj,
shareeRepo: shareeRepo == null ? _shareeRepo : shareeRepo.obj,
favoriteRepo: favoriteRepo == null ? _favoriteRepo : favoriteRepo.obj,
@ -202,6 +210,12 @@ class DiContainer {
localFileRepo: localFileRepo == null ? _localFileRepo : localFileRepo.obj,
searchRepo: searchRepo == null ? _searchRepo : searchRepo.obj,
ncAlbumRepo: ncAlbumRepo == null ? _ncAlbumRepo : ncAlbumRepo.obj,
faceRecognitionPersonRepo: faceRecognitionPersonRepo == null
? _faceRecognitionPersonRepo
: faceRecognitionPersonRepo.obj,
recognizeFaceRepo: recognizeFaceRepo == null
? _recognizeFaceRepo
: recognizeFaceRepo.obj,
pref: pref == null ? _pref : pref.obj,
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
touchManager: touchManager == null ? _touchManager : touchManager.obj,
@ -214,13 +228,9 @@ class DiContainer {
AlbumRepo2 get albumRepo2 => _albumRepo2!;
AlbumRepo2 get albumRepo2Remote => _albumRepo2Remote!;
AlbumRepo2 get albumRepo2Local => _albumRepo2Local!;
FaceRepo get faceRepo => _faceRepo!;
FileRepo get fileRepo => _fileRepo!;
FileRepo get fileRepoRemote => _fileRepoRemote!;
FileRepo get fileRepoLocal => _fileRepoLocal!;
PersonRepo get personRepo => _personRepo!;
PersonRepo get personRepoRemote => _personRepoRemote!;
PersonRepo get personRepoLocal => _personRepoLocal!;
ShareRepo get shareRepo => _shareRepo!;
ShareeRepo get shareeRepo => _shareeRepo!;
FavoriteRepo get favoriteRepo => _favoriteRepo!;
@ -233,10 +243,19 @@ class DiContainer {
NcAlbumRepo get ncAlbumRepo => _ncAlbumRepo!;
NcAlbumRepo get ncAlbumRepoRemote => _ncAlbumRepoRemote!;
NcAlbumRepo get ncAlbumRepoLocal => _ncAlbumRepoLocal!;
TouchManager get touchManager => _touchManager!;
FaceRecognitionPersonRepo get faceRecognitionPersonRepo =>
_faceRecognitionPersonRepo!;
FaceRecognitionPersonRepo get faceRecognitionPersonRepoRemote =>
_faceRecognitionPersonRepoRemote!;
FaceRecognitionPersonRepo get faceRecognitionPersonRepoLocal =>
_faceRecognitionPersonRepoLocal!;
RecognizeFaceRepo get recognizeFaceRepo => _recognizeFaceRepo!;
RecognizeFaceRepo get recognizeFaceRepoRemote => _recognizeFaceRepoRemote!;
RecognizeFaceRepo get recognizeFaceRepoLocal => _recognizeFaceRepoLocal!;
sql.SqliteDb get sqliteDb => _sqliteDb!;
Pref get pref => _pref!;
TouchManager get touchManager => _touchManager!;
set albumRepo(AlbumRepo v) {
assert(_albumRepo == null);
@ -268,11 +287,6 @@ class DiContainer {
_albumRepo2Local = v;
}
set faceRepo(FaceRepo v) {
assert(_faceRepo == null);
_faceRepo = v;
}
set fileRepo(FileRepo v) {
assert(_fileRepo == null);
_fileRepo = v;
@ -288,21 +302,6 @@ class DiContainer {
_fileRepoLocal = v;
}
set personRepo(PersonRepo v) {
assert(_personRepo == null);
_personRepo = v;
}
set personRepoRemote(PersonRepo v) {
assert(_personRepoRemote == null);
_personRepoRemote = v;
}
set personRepoLocal(PersonRepo v) {
assert(_personRepoLocal == null);
_personRepoLocal = v;
}
set shareRepo(ShareRepo v) {
assert(_shareRepo == null);
_shareRepo = v;
@ -363,9 +362,34 @@ class DiContainer {
_ncAlbumRepoLocal = v;
}
set touchManager(TouchManager v) {
assert(_touchManager == null);
_touchManager = v;
set faceRecognitionPersonRepo(FaceRecognitionPersonRepo v) {
assert(_faceRecognitionPersonRepo == null);
_faceRecognitionPersonRepo = v;
}
set faceRecognitionPersonRepoRemote(FaceRecognitionPersonRepo v) {
assert(_faceRecognitionPersonRepoRemote == null);
_faceRecognitionPersonRepoRemote = v;
}
set faceRecognitionPersonRepoLocal(FaceRecognitionPersonRepo v) {
assert(_faceRecognitionPersonRepoLocal == null);
_faceRecognitionPersonRepoLocal = v;
}
set recognizeFaceRepo(RecognizeFaceRepo v) {
assert(_recognizeFaceRepo == null);
_recognizeFaceRepo = v;
}
set recognizeFaceRepoRemote(RecognizeFaceRepo v) {
assert(_recognizeFaceRepoRemote == null);
_recognizeFaceRepoRemote = v;
}
set recognizeFaceRepoLocal(RecognizeFaceRepo v) {
assert(_recognizeFaceRepoLocal == null);
_recognizeFaceRepoLocal = v;
}
set sqliteDb(sql.SqliteDb v) {
@ -378,11 +402,15 @@ class DiContainer {
_pref = v;
}
set touchManager(TouchManager v) {
assert(_touchManager == null);
_touchManager = v;
}
AlbumRepo? _albumRepo;
AlbumRepo? _albumRepoRemote;
// Explicitly request a AlbumRepo backed by local source
AlbumRepo? _albumRepoLocal;
FaceRepo? _faceRepo;
AlbumRepo2? _albumRepo2;
AlbumRepo2? _albumRepo2Remote;
AlbumRepo2? _albumRepo2Local;
@ -391,9 +419,6 @@ class DiContainer {
FileRepo? _fileRepoRemote;
// Explicitly request a FileRepo backed by local source
FileRepo? _fileRepoLocal;
PersonRepo? _personRepo;
PersonRepo? _personRepoRemote;
PersonRepo? _personRepoLocal;
ShareRepo? _shareRepo;
ShareeRepo? _shareeRepo;
FavoriteRepo? _favoriteRepo;
@ -406,10 +431,16 @@ class DiContainer {
NcAlbumRepo? _ncAlbumRepo;
NcAlbumRepo? _ncAlbumRepoRemote;
NcAlbumRepo? _ncAlbumRepoLocal;
TouchManager? _touchManager;
FaceRecognitionPersonRepo? _faceRecognitionPersonRepo;
FaceRecognitionPersonRepo? _faceRecognitionPersonRepoRemote;
FaceRecognitionPersonRepo? _faceRecognitionPersonRepoLocal;
RecognizeFaceRepo? _recognizeFaceRepo;
RecognizeFaceRepo? _recognizeFaceRepoRemote;
RecognizeFaceRepo? _recognizeFaceRepoLocal;
sql.SqliteDb? _sqliteDb;
Pref? _pref;
TouchManager? _touchManager;
}
extension DiContainerExtension on DiContainer {
@ -417,24 +448,26 @@ extension DiContainerExtension on DiContainer {
///
/// Notice that not all repo support this
DiContainer withRemoteRepo() => copyWith(
albumRepo: OrNull(albumRepoRemote),
albumRepo2: OrNull(albumRepo2Remote),
fileRepo: OrNull(fileRepoRemote),
personRepo: OrNull(personRepoRemote),
tagRepo: OrNull(tagRepoRemote),
ncAlbumRepo: OrNull(ncAlbumRepoRemote),
albumRepo: OrNull(_albumRepoRemote),
albumRepo2: OrNull(_albumRepo2Remote),
fileRepo: OrNull(_fileRepoRemote),
tagRepo: OrNull(_tagRepoRemote),
ncAlbumRepo: OrNull(_ncAlbumRepoRemote),
faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoRemote),
recognizeFaceRepo: OrNull(_recognizeFaceRepoRemote),
);
/// Uses local repo if available
///
/// Notice that not all repo support this
DiContainer withLocalRepo() => copyWith(
albumRepo: OrNull(albumRepoLocal),
albumRepo2: OrNull(albumRepo2Local),
fileRepo: OrNull(fileRepoLocal),
personRepo: OrNull(personRepoLocal),
tagRepo: OrNull(tagRepoLocal),
ncAlbumRepo: OrNull(ncAlbumRepoLocal),
albumRepo: OrNull(_albumRepoLocal),
albumRepo2: OrNull(_albumRepo2Local),
fileRepo: OrNull(_fileRepoLocal),
tagRepo: OrNull(_tagRepoLocal),
ncAlbumRepo: OrNull(_ncAlbumRepoLocal),
faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoLocal),
recognizeFaceRepo: OrNull(_recognizeFaceRepoLocal),
);
DiContainer withLocalAlbumRepo() =>
@ -442,10 +475,6 @@ extension DiContainerExtension on DiContainer {
DiContainer withRemoteFileRepo() =>
copyWith(fileRepo: OrNull(fileRepoRemote));
DiContainer withLocalFileRepo() => copyWith(fileRepo: OrNull(fileRepoLocal));
DiContainer withRemotePersonRepo() =>
copyWith(personRepo: OrNull(personRepoRemote));
DiContainer withLocalPersonRepo() =>
copyWith(personRepo: OrNull(personRepoLocal));
DiContainer withRemoteTagRepo() => copyWith(tagRepo: OrNull(tagRepoRemote));
DiContainer withLocalTagRepo() => copyWith(tagRepo: OrNull(tagRepoLocal));
}

View file

@ -8,8 +8,7 @@ import 'package:nc_photos/entity/collection_item.dart';
import 'package:nc_photos/entity/collection_item/basic_item.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/use_case/list_face.dart';
import 'package:nc_photos/use_case/populate_person.dart';
import 'package:nc_photos/use_case/person/list_person_face.dart';
class CollectionPersonAdapter
with
@ -18,25 +17,22 @@ class CollectionPersonAdapter
CollectionAdapterUnshareableTag
implements CollectionAdapter {
CollectionPersonAdapter(this._c, this.account, this.collection)
: assert(require(_c)),
_provider = collection.contentProvider as CollectionPersonProvider;
static bool require(DiContainer c) =>
ListFace.require(c) && PopulatePerson.require(c);
: _provider = collection.contentProvider as CollectionPersonProvider;
@override
Stream<List<CollectionItem>> listItem() async* {
final faces = await ListFace(_c)(account, _provider.person);
final files = await PopulatePerson(_c)(account, faces);
Stream<List<CollectionItem>> listItem() {
final rootDirs = account.roots
.map((e) => File(path: file_util.unstripPath(account, e)))
.toList();
yield files
.where((f) =>
file_util.isSupportedFormat(f) &&
rootDirs.any((dir) => file_util.isUnderDir(f, dir)))
.map((f) => BasicCollectionFileItem(f))
.toList();
return ListPersonFace(_c)(account, _provider.person).map((faces) {
return faces
.map((e) => e.file)
.where((f) =>
file_util.isSupportedFormat(f) &&
rootDirs.any((dir) => file_util.isUnderDir(f, dir)))
.map((f) => BasicCollectionFileItem(f))
.toList();
});
}
@override

View file

@ -1,9 +1,6 @@
import 'dart:math' as math;
import 'package:clock/clock.dart';
import 'package:equatable/equatable.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/entity/collection.dart';
import 'package:nc_photos/entity/collection/util.dart';
import 'package:nc_photos/entity/collection_item/util.dart';
@ -21,7 +18,7 @@ class CollectionPersonProvider
String get fourCc => "PERS";
@override
String get id => person.name;
String get id => person.id;
@override
int? get count => person.count;
@ -44,8 +41,8 @@ class CollectionPersonProvider
int height, {
bool? isKeepAspectRatio,
}) {
return api_util.getFacePreviewUrl(account, person.thumbFaceId,
size: math.max(width, height));
return person.getCoverUrl(width, height,
isKeepAspectRatio: isKeepAspectRatio);
}
@override

View file

@ -1,41 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:to_string/to_string.dart';
part 'face.g.dart';
@toString
class Face with EquatableMixin {
const Face({
required this.id,
required this.fileId,
});
@override
String toString() => _$toString();
@override
get props => [
id,
fileId,
];
final int id;
final int fileId;
}
class FaceRepo {
const FaceRepo(this.dataSrc);
/// See [FaceDataSource.list]
Future<List<Face>> list(Account account, Person person) =>
dataSrc.list(account, person);
final FaceDataSource dataSrc;
}
abstract class FaceDataSource {
/// List all faces associated to [person]
Future<List<Face>> list(Account account, Person person);
}

View file

@ -1,37 +0,0 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/entity/face.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
part 'data_source.g.dart';
@npLog
class FaceRemoteDataSource implements FaceDataSource {
const FaceRemoteDataSource();
@override
list(Account account, Person person) async {
_log.info("[list] $person");
final response = await ApiUtil.fromAccount(account)
.ocs()
.facerecognition()
.person(person.name)
.faces()
.get();
if (!response.isGood) {
_log.severe("[list] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
final apiFaces = await api.FaceParser().parse(response.body);
return apiFaces.map(ApiFaceConverter.fromApi).toList();
}
}

View file

@ -0,0 +1,24 @@
import 'package:equatable/equatable.dart';
import 'package:to_string/to_string.dart';
part 'face_recognition_face.g.dart';
@toString
class FaceRecognitionFace with EquatableMixin {
const FaceRecognitionFace({
required this.id,
required this.fileId,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
id,
fileId,
];
final int id;
final int fileId;
}

View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'face_recognition_face.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$FaceRecognitionFaceToString on FaceRecognitionFace {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "FaceRecognitionFace {id: $id, fileId: $fileId}";
}
}

View file

@ -0,0 +1,27 @@
import 'package:equatable/equatable.dart';
import 'package:to_string/to_string.dart';
part 'face_recognition_person.g.dart';
@toString
class FaceRecognitionPerson with EquatableMixin {
const FaceRecognitionPerson({
required this.name,
required this.thumbFaceId,
required this.count,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
name,
thumbFaceId,
count,
];
final String name;
final int thumbFaceId;
final int count;
}

View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'face_recognition_person.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$FaceRecognitionPersonToString on FaceRecognitionPerson {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "FaceRecognitionPerson {name: $name, thumbFaceId: $thumbFaceId, count: $count}";
}
}

View file

@ -0,0 +1,101 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/entity/face_recognition_face.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/face_recognition_person/repo.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
part 'data_source.g.dart';
@npLog
class FaceRecognitionPersonRemoteDataSource
implements FaceRecognitionPersonDataSource {
const FaceRecognitionPersonRemoteDataSource();
@override
Future<List<FaceRecognitionPerson>> getPersons(Account account) async {
_log.info("[getPersons] $account");
final response = await ApiUtil.fromAccount(account)
.ocs()
.facerecognition()
.persons()
.get();
if (!response.isGood) {
_log.severe("[getPersons] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
final apiPersons =
await api.FaceRecognitionPersonParser().parse(response.body);
return apiPersons.map(ApiFaceRecognitionPersonConverter.fromApi).toList();
}
@override
Future<List<FaceRecognitionFace>> getFaces(
Account account, FaceRecognitionPerson person) async {
_log.info("[getFaces] $person");
final response = await ApiUtil.fromAccount(account)
.ocs()
.facerecognition()
.person(person.name)
.faces()
.get();
if (!response.isGood) {
_log.severe("[getFaces] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
final apiFaces = await api.FaceRecognitionFaceParser().parse(response.body);
return apiFaces.map(ApiFaceRecognitionFaceConverter.fromApi).toList();
}
}
@npLog
class FaceRecognitionPersonSqliteDbDataSource
implements FaceRecognitionPersonDataSource {
const FaceRecognitionPersonSqliteDbDataSource(this.sqliteDb);
@override
Future<List<FaceRecognitionPerson>> getPersons(Account account) async {
_log.info("[getPersons] $account");
final dbPersons = await sqliteDb.use((db) async {
return await db.allFaceRecognitionPersons(account: sql.ByAccount.app(account));
});
return dbPersons
.map((p) {
try {
return SqliteFaceRecognitionPersonConverter.fromSql(p);
} catch (e, stackTrace) {
_log.severe(
"[getPersons] Failed while converting DB entry", e, stackTrace);
return null;
}
})
.whereNotNull()
.toList();
}
@override
Future<List<FaceRecognitionFace>> getFaces(
Account account, FaceRecognitionPerson person) async {
_log.info("[getFaces] $person");
// we are not caching faces ATM, to be implemented
return const FaceRecognitionPersonRemoteDataSource()
.getFaces(account, person);
}
final sql.SqliteDb sqliteDb;
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'data_source.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$FaceRecognitionPersonRemoteDataSourceNpLog
on FaceRecognitionPersonRemoteDataSource {
// ignore: unused_element
Logger get _log => log;
static final log = Logger(
"entity.face_recognition_person.data_source.FaceRecognitionPersonRemoteDataSource");
}
extension _$FaceRecognitionPersonSqliteDbDataSourceNpLog
on FaceRecognitionPersonSqliteDbDataSource {
// ignore: unused_element
Logger get _log => log;
static final log = Logger(
"entity.face_recognition_person.data_source.FaceRecognitionPersonSqliteDbDataSource");
}

View file

@ -0,0 +1,52 @@
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/face_recognition_face.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:np_codegen/np_codegen.dart';
part 'repo.g.dart';
abstract class FaceRecognitionPersonRepo {
/// Query all [FaceRecognitionPerson]s belonging to [account]
///
/// Normally the stream should complete with only a single event, but some
/// implementation might want to return multiple set of values, say one set of
/// cached value and later another set of updated value from a remote source.
/// In any case, each event is guaranteed to be one complete set of data
Stream<List<FaceRecognitionPerson>> getPersons(Account account);
/// Query all [FaceRecognitionFace]s belonging to [person]
Stream<List<FaceRecognitionFace>> getFaces(
Account account, FaceRecognitionPerson person);
}
/// A repo that simply relay the call to the backed
/// [FaceRecognitionPersonDataSource]
@npLog
class BasicFaceRecognitionPersonRepo implements FaceRecognitionPersonRepo {
const BasicFaceRecognitionPersonRepo(this.dataSrc);
@override
Stream<List<FaceRecognitionPerson>> getPersons(Account account) async* {
yield await dataSrc.getPersons(account);
}
@override
Stream<List<FaceRecognitionFace>> getFaces(
Account account, FaceRecognitionPerson person) async* {
yield await dataSrc.getFaces(account, person);
}
final FaceRecognitionPersonDataSource dataSrc;
}
abstract class FaceRecognitionPersonDataSource {
/// Query all [FaceRecognitionPerson]s belonging to [account]
Future<List<FaceRecognitionPerson>> getPersons(Account account);
/// Query all faces belonging to [person]
Future<List<FaceRecognitionFace>> getFaces(
Account account, FaceRecognitionPerson person);
}

View file

@ -0,0 +1,16 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'repo.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$BasicFaceRecognitionPersonRepoNpLog
on BasicFaceRecognitionPersonRepo {
// ignore: unused_element
Logger get _log => log;
static final log = Logger(
"entity.face_recognition_person.repo.BasicFaceRecognitionPersonRepo");
}

View file

@ -1,42 +1,94 @@
import 'package:copy_with/copy_with.dart';
import 'package:equatable/equatable.dart';
import 'package:nc_photos/account.dart';
import 'package:flutter/material.dart';
import 'package:to_string/to_string.dart';
part 'person.g.dart';
enum PersonProvider {
none,
faceRecognition,
recognize;
static PersonProvider fromValue(int value) => PersonProvider.values[value];
}
@genCopyWith
@toString
class Person with EquatableMixin {
const Person({
required this.name,
required this.thumbFaceId,
required this.count,
required this.contentProvider,
});
@override
String toString() => _$toString();
bool compareIdentity(Person other) => other.id == id;
int get identityHashCode => id.hashCode;
/// A unique id for each collection. The value is divided into two parts in
/// the format XXXX-YYY...YYY, where XXXX is a four-character code
/// representing the content provider type, and YYY is an implementation
/// detail of each providers
String get id => "${contentProvider.fourCc}-${contentProvider.id}";
/// See [PersonContentProvider.count]
int? get count => contentProvider.count;
/// See [PersonContentProvider.getCoverUrl]
String? getCoverUrl(
int width,
int height, {
bool? isKeepAspectRatio,
}) =>
contentProvider.getCoverUrl(
width,
height,
isKeepAspectRatio: isKeepAspectRatio,
);
/// See [PersonContentProvider.getCoverTransform]
Matrix4? getCoverTransform(int viewportSize, int width, int height) =>
contentProvider.getCoverTransform(viewportSize, width, height);
@override
get props => [
List<Object?> get props => [
name,
thumbFaceId,
count,
contentProvider,
];
final String name;
final int thumbFaceId;
final int count;
final PersonContentProvider contentProvider;
}
class PersonRepo {
const PersonRepo(this.dataSrc);
abstract class PersonContentProvider with EquatableMixin {
const PersonContentProvider();
/// See [PersonDataSource.list]
Future<List<Person>> list(Account account) => dataSrc.list(account);
/// Unique FourCC of this provider type
String get fourCc;
final PersonDataSource dataSrc;
}
abstract class PersonDataSource {
/// List all people for this account
Future<List<Person>> list(Account account);
/// Return the unique id of this person
String get id;
/// Return the number of items in this person, or null if not supported
int? get count;
/// Return the URL of the cover image if available
///
/// The [width] and [height] are provided as a hint only, implementations are
/// free to ignore them if it's not supported
///
/// [isKeepAspectRatio] is only a hint and implementations may ignore it
String? getCoverUrl(
int width,
int height, {
bool? isKeepAspectRatio,
});
/// Return the transformation matrix to focus the face
///
/// Only viewport in square is supported
Matrix4? getCoverTransform(int viewportSize, int width, int height);
}

View file

@ -2,6 +2,39 @@
part of 'person.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $PersonCopyWithWorker {
Person call({String? name, PersonContentProvider? contentProvider});
}
class _$PersonCopyWithWorkerImpl implements $PersonCopyWithWorker {
_$PersonCopyWithWorkerImpl(this.that);
@override
Person call({dynamic name, dynamic contentProvider}) {
return Person(
name: name as String? ?? that.name,
contentProvider:
contentProvider as PersonContentProvider? ?? that.contentProvider);
}
final Person that;
}
extension $PersonCopyWith on Person {
$PersonCopyWithWorker get copyWith => _$copyWith;
$PersonCopyWithWorker get _$copyWith => _$PersonCopyWithWorkerImpl(this);
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
@ -9,6 +42,6 @@ part of 'person.dart';
extension _$PersonToString on Person {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "Person {name: $name, thumbFaceId: $thumbFaceId, count: $count}";
return "Person {name: $name, contentProvider: $contentProvider}";
}
}

View file

@ -0,0 +1,27 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/person/adapter/face_recognition.dart';
import 'package:nc_photos/entity/person/adapter/recognize.dart';
import 'package:nc_photos/entity/person/content_provider/face_recognition.dart';
import 'package:nc_photos/entity/person/content_provider/recognize.dart';
import 'package:nc_photos/entity/person_face.dart';
abstract class PersonAdapter {
const PersonAdapter();
static PersonAdapter of(DiContainer c, Account account, Person person) {
switch (person.contentProvider.runtimeType) {
case PersonFaceRecognitionProvider:
return PersonFaceRecognitionAdapter(c, account, person);
case PersonRecognizeProvider:
return PersonRecognizeAdapter(c, account, person);
default:
throw UnsupportedError(
"Unknown type: ${person.contentProvider.runtimeType}");
}
}
/// List faces of this person
Stream<List<PersonFace>> listFace();
}

View file

@ -0,0 +1,47 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/person/adapter.dart';
import 'package:nc_photos/entity/person/content_provider/face_recognition.dart';
import 'package:nc_photos/entity/person_face.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/use_case/face_recognition_person/list_face_recognition_face.dart';
import 'package:nc_photos/use_case/find_file_descriptor.dart';
import 'package:np_codegen/np_codegen.dart';
part 'face_recognition.g.dart';
@npLog
class PersonFaceRecognitionAdapter implements PersonAdapter {
PersonFaceRecognitionAdapter(this._c, this.account, this.person)
: _provider = person.contentProvider as PersonFaceRecognitionProvider;
@override
Stream<List<PersonFace>> listFace() {
return ListFaceRecognitionFace(_c)(account, _provider.person)
.asyncMap((faces) async {
final found = await FindFileDescriptor(_c)(
account,
faces.map((e) => e.fileId).toList(),
onFileNotFound: (fileId) {
_log.warning("[listFace] File not found: $fileId");
},
);
return faces
.map((i) {
final f = found.firstWhereOrNull((e) => e.fdId == i.fileId);
return f?.run(BasicPersonFace.new);
})
.whereNotNull()
.toList();
});
}
final DiContainer _c;
final Account account;
final Person person;
final PersonFaceRecognitionProvider _provider;
}

View file

@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'face_recognition.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$PersonFaceRecognitionAdapterNpLog on PersonFaceRecognitionAdapter {
// ignore: unused_element
Logger get _log => log;
static final log = Logger(
"entity.person.adapter.face_recognition.PersonFaceRecognitionAdapter");
}

View file

@ -0,0 +1,47 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/person/adapter.dart';
import 'package:nc_photos/entity/person/content_provider/recognize.dart';
import 'package:nc_photos/entity/person_face.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/use_case/find_file_descriptor.dart';
import 'package:nc_photos/use_case/recognize_face/list_recognize_face_item.dart';
import 'package:np_codegen/np_codegen.dart';
part 'recognize.g.dart';
@npLog
class PersonRecognizeAdapter implements PersonAdapter {
PersonRecognizeAdapter(this._c, this.account, this.person)
: _provider = person.contentProvider as PersonRecognizeProvider;
@override
Stream<List<PersonFace>> listFace() {
return ListRecognizeFaceItem(_c)(account, _provider.face)
.asyncMap((faces) async {
final found = await FindFileDescriptor(_c)(
account,
faces.map((e) => e.fileId).toList(),
onFileNotFound: (fileId) {
_log.warning("[listFace] File not found: $fileId");
},
);
return faces
.map((i) {
final f = found.firstWhereOrNull((e) => e.fdId == i.fileId);
return f?.run(BasicPersonFace.new);
})
.whereNotNull()
.toList();
});
}
final DiContainer _c;
final Account account;
final Person person;
final PersonRecognizeProvider _provider;
}

View file

@ -1,14 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'populate_person.dart';
part of 'recognize.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$PopulatePersonNpLog on PopulatePerson {
extension _$PersonRecognizeAdapterNpLog on PersonRecognizeAdapter {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("use_case.populate_person.PopulatePerson");
static final log =
Logger("entity.person.adapter.recognize.PersonRecognizeAdapter");
}

View file

@ -0,0 +1,32 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/person/content_provider/face_recognition.dart';
import 'package:nc_photos/entity/person/content_provider/recognize.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
class PersonBuilder {
static Person byFaceRecognitionPerson(
Account account, FaceRecognitionPerson person) {
return Person(
name: person.name,
contentProvider: PersonFaceRecognitionProvider(
account: account,
person: person,
),
);
}
static Person byRecognizeFace(
Account account, RecognizeFace face, List<RecognizeFaceItem>? items) {
return Person(
name: face.isNamed ? face.label : "",
contentProvider: PersonRecognizeProvider(
account: account,
face: face,
items: items,
),
);
}
}

View file

@ -0,0 +1,55 @@
import 'dart:math' as math;
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:to_string/to_string.dart';
part 'face_recognition.g.dart';
@toString
class PersonFaceRecognitionProvider
with EquatableMixin
implements PersonContentProvider {
const PersonFaceRecognitionProvider({
required this.account,
required this.person,
});
@override
String toString() => _$toString();
@override
String get fourCc => "FACR";
@override
String get id => person.name;
@override
int? get count => person.count;
@override
String? getCoverUrl(
int width,
int height, {
bool? isKeepAspectRatio,
}) {
return api_util.getFacePreviewUrl(
account,
person.thumbFaceId,
size: math.max(width, height),
);
}
@override
Matrix4? getCoverTransform(int viewportSize, int width, int height) => null;
@override
List<Object?> get props => [account, person];
final Account account;
final FaceRecognitionPerson person;
}

View file

@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'face_recognition.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$PersonFaceRecognitionProviderToString
on PersonFaceRecognitionProvider {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "PersonFaceRecognitionProvider {account: $account, person: $person}";
}
}

View file

@ -0,0 +1,108 @@
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:to_string/to_string.dart';
part 'recognize.g.dart';
@toString
class PersonRecognizeProvider
with EquatableMixin
implements PersonContentProvider {
PersonRecognizeProvider({
required this.account,
required this.face,
List<RecognizeFaceItem>? items,
}) : items = items?.sorted((a, b) => b.fileId.compareTo(a.fileId));
@override
String toString() => _$toString();
@override
String get fourCc => "RCNZ";
@override
String get id => face.label;
@override
int? get count => items?.length;
@override
String? getCoverUrl(
int width,
int height, {
bool? isKeepAspectRatio,
}) =>
items?.firstOrNull?.run((i) => api_util.getFilePreviewUrl(
account,
i.toFile(),
width: width,
height: height,
isKeepAspectRatio: isKeepAspectRatio ?? false,
));
@override
Matrix4? getCoverTransform(int viewportSize, int imgW, int imgH) {
final detection = items?.firstOrNull?.faceDetections
?.firstWhereOrNull((e) => e["title"] == face.label);
if (detection == null) {
return null;
}
final faceXNorm = (detection["x"] as Object?).as<double>();
final faceYNorm = (detection["y"] as Object?).as<double>();
final faceHNorm = (detection["height"] as Object?).as<double>();
final faceWNorm = (detection["width"] as Object?).as<double>();
if (faceXNorm == null ||
faceYNorm == null ||
faceHNorm == null ||
faceWNorm == null) {
return null;
}
// move image to the face
double mx = imgW * -faceXNorm;
double my = imgH * -faceYNorm;
// add offset in case image is not a square
if (imgW > imgH) {
mx += (imgW - imgH) / 2;
} else if (imgH > imgW) {
my += (imgH - imgW) / 2;
}
// scale image to focus on the face
final faceW = imgW * faceWNorm;
final faceH = imgH * faceHNorm;
double ms;
if (faceW > faceH) {
ms = viewportSize / faceW;
} else {
ms = viewportSize / faceH;
}
// slightly scale down to include pixels around the face
ms *= .75;
// center the scaled image
final resultFaceW = faceW * ms;
final resultFaceH = faceH * ms;
final cx = (viewportSize - resultFaceW) / 2;
final cy = (viewportSize - resultFaceH) / 2;
return Matrix4.identity()
..translate(cx, cy)
..scale(ms)
..translate(mx, my);
}
@override
List<Object?> get props => [account, face, items];
final Account account;
final RecognizeFace face;
final List<RecognizeFaceItem>? items;
}

View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recognize.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$PersonRecognizeProviderToString on PersonRecognizeProvider {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "PersonRecognizeProvider {account: $account, face: $face, items: ${items == null ? null : "[length: ${items!.length}]"}}";
}
}

View file

@ -1,79 +0,0 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
part 'data_source.g.dart';
@npLog
class PersonRemoteDataSource implements PersonDataSource {
const PersonRemoteDataSource();
@override
list(Account account) async {
_log.info("[list] $account");
final response = await ApiUtil.fromAccount(account)
.ocs()
.facerecognition()
.persons()
.get();
if (!response.isGood) {
_log.severe("[list] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
final apiPersons = await api.PersonParser().parse(response.body);
return apiPersons.map(ApiPersonConverter.fromApi).toList();
}
}
@npLog
class PersonSqliteDbDataSource implements PersonDataSource {
const PersonSqliteDbDataSource(this.sqliteDb);
@override
list(Account account) async {
_log.info("[list] $account");
final dbPersons = await sqliteDb.use((db) async {
return await db.allPersons(appAccount: account);
});
return dbPersons.convertToAppPerson();
}
final sql.SqliteDb sqliteDb;
}
@npLog
class _PersonParser {
List<Person> parseList(List<JsonObj> jsons) {
final product = <Person>[];
for (final j in jsons) {
try {
product.add(parseSingle(j));
} catch (e) {
_log.severe("[parseList] Failed parsing json: ${jsonEncode(j)}", e);
}
}
return product;
}
Person parseSingle(JsonObj json) {
return Person(
name: json["name"],
thumbFaceId: json["thumbFaceId"],
count: json["count"],
);
}
}

View file

@ -1,29 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'data_source.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$PersonRemoteDataSourceNpLog on PersonRemoteDataSource {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("entity.person.data_source.PersonRemoteDataSource");
}
extension _$PersonSqliteDbDataSourceNpLog on PersonSqliteDbDataSource {
// ignore: unused_element
Logger get _log => log;
static final log =
Logger("entity.person.data_source.PersonSqliteDbDataSource");
}
extension _$_PersonParserNpLog on _PersonParser {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("entity.person.data_source._PersonParser");
}

View file

@ -0,0 +1,23 @@
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:to_string/to_string.dart';
part 'person_face.g.dart';
/// A file with the face of a person
abstract class PersonFace {
const PersonFace();
FileDescriptor get file;
}
/// The basic form of [PersonFace]
@toString
class BasicPersonFace implements PersonFace {
const BasicPersonFace(this.file);
@override
String toString() => _$toString();
@override
final FileDescriptor file;
}

View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'person_face.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$BasicPersonFaceToString on BasicPersonFace {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "BasicPersonFace {file: ${file.fdPath}}";
}
}

View file

@ -9,6 +9,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/pref/provider/memory.dart';
import 'package:nc_photos/event/event.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
part 'pref.g.dart';
part 'pref/extension.dart';
@ -65,6 +66,8 @@ class AccountPref {
}
}
Future<JsonObj> toJson() => provider.toJson();
Future<bool> _set<T>(AccountPrefKey key, T value,
Future<bool> Function(AccountPrefKey key, T value) setFn) async {
if (await setFn(key, value)) {
@ -203,19 +206,18 @@ enum PrefKey implements PrefKeyInterface {
}
enum AccountPrefKey implements PrefKeyInterface {
isEnableFaceRecognitionApp,
shareFolder,
hasNewSharedAlbum,
isEnableMemoryAlbum,
touchRootEtag,
accountLabel,
lastNewCollectionType;
lastNewCollectionType,
personProvider,
;
@override
String toStringKey() {
switch (this) {
case AccountPrefKey.isEnableFaceRecognitionApp:
return "isEnableFaceRecognitionApp";
case AccountPrefKey.shareFolder:
return "shareFolder";
case AccountPrefKey.hasNewSharedAlbum:
@ -228,6 +230,8 @@ enum AccountPrefKey implements PrefKeyInterface {
return "accountLabel";
case AccountPrefKey.lastNewCollectionType:
return "lastNewCollectionType";
case AccountPrefKey.personProvider:
return "personProvider";
}
}
}
@ -248,4 +252,6 @@ abstract class PrefProvider {
Future<bool> remove(PrefKeyInterface key);
Future<bool> clear();
Future<JsonObj> toJson();
}

View file

@ -10,5 +10,5 @@ extension _$PrefNpLog on Pref {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("pref.Pref");
static final log = Logger("entity.pref.Pref");
}

View file

@ -270,15 +270,6 @@ extension PrefExtension on Pref {
}
extension AccountPrefExtension on AccountPref {
bool? isEnableFaceRecognitionApp() =>
provider.getBool(AccountPrefKey.isEnableFaceRecognitionApp);
bool isEnableFaceRecognitionAppOr([bool def = true]) =>
isEnableFaceRecognitionApp() ?? def;
Future<bool> setEnableFaceRecognitionApp(bool value) => _set<bool>(
AccountPrefKey.isEnableFaceRecognitionApp,
value,
(key, value) => provider.setBool(key, value));
String? getShareFolder() => provider.getString(AccountPrefKey.shareFolder);
String getShareFolderOr([String def = ""]) => getShareFolder() ?? def;
Future<bool> setShareFolder(String value) => _set<String>(
@ -334,4 +325,11 @@ extension AccountPrefExtension on AccountPref {
(key, value) => provider.setInt(key, value));
}
}
int? getPersonProvider() => provider.getInt(AccountPrefKey.personProvider);
int getPersonProviderOr([int def = 1]) => getPersonProvider() ?? def;
Future<bool> setPersonProvider(int value) => _set<int>(
AccountPrefKey.personProvider,
value,
(key, value) => provider.setInt(key, value));
}

View file

@ -1,4 +1,5 @@
import 'package:nc_photos/entity/pref.dart';
import 'package:np_common/type.dart';
/// [Pref] stored in memory, useful in unit tests
class PrefMemoryProvider extends PrefProvider {
@ -6,6 +7,8 @@ class PrefMemoryProvider extends PrefProvider {
Map<String, dynamic> initialData = const <String, dynamic>{},
]) : _data = Map.of(initialData);
factory PrefMemoryProvider.fromJson(JsonObj json) => PrefMemoryProvider(json);
@override
bool? getBool(PrefKeyInterface key) => _get<bool>(key);
@override
@ -39,6 +42,8 @@ class PrefMemoryProvider extends PrefProvider {
_data.clear();
return true;
}
@override
Future<JsonObj> toJson() async => Map.of(_data);
T? _get<T>(PrefKeyInterface key) => _data[key.toStringKey()];

View file

@ -2,7 +2,9 @@ import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
import 'package:nc_photos/use_case/compat/v34.dart';
import 'package:np_common/type.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
/// [Pref] stored with [SharedPreferences] lib
class PrefSharedPreferencesProvider extends PrefProvider {
@ -54,5 +56,8 @@ class PrefSharedPreferencesProvider extends PrefProvider {
@override
Future<bool> clear() => _pref.clear();
@override
Future<JsonObj> toJson() => SharedPreferencesStorePlatform.instance.getAll();
late SharedPreferences _pref;
}

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
import 'package:np_common/type.dart';
/// [Pref] backed by [UniversalStorage]
class PrefUniversalStorageProvider extends PrefProvider {
@ -52,6 +53,9 @@ class PrefUniversalStorageProvider extends PrefProvider {
return true;
}
@override
Future<JsonObj> toJson() async => Map.of(_data);
T? _get<T>(PrefKeyInterface key) => _data[key.toStringKey()];
Future<bool> _set<T>(PrefKeyInterface key, T value) async {

View file

@ -0,0 +1,26 @@
import 'package:equatable/equatable.dart';
import 'package:to_string/to_string.dart';
part 'recognize_face.g.dart';
/// A person's face recognized by the Recognize app
///
/// Beware that the terminology used in Recognize is different to
/// FaceRecognition, which is also followed by this app. A face in Recognize is
/// a person in FaceRecognition and this app
@toString
class RecognizeFace with EquatableMixin {
const RecognizeFace({
required this.label,
});
bool get isNamed => int.tryParse(label) == null;
@override
String toString() => _$toString();
@override
List<Object?> get props => [label];
final String label;
}

View file

@ -1,14 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'face.dart';
part of 'recognize_face.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$FaceToString on Face {
extension _$RecognizeFaceToString on RecognizeFace {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "Face {id: $id, fileId: $fileId}";
return "RecognizeFace {label: $label}";
}
}

View file

@ -0,0 +1,212 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face/repo.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/table.dart';
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/map_extension.dart';
import 'package:nc_photos/np_api_util.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
part 'data_source.g.dart';
@npLog
class RecognizeFaceRemoteDataSource implements RecognizeFaceDataSource {
const RecognizeFaceRemoteDataSource();
@override
Future<List<RecognizeFace>> getFaces(Account account) async {
_log.info("[getFaces] account: ${account.userId}");
final response = await ApiUtil.fromAccount(account)
.recognize(account.userId.raw)
.faces()
.propfind();
if (!response.isGood) {
_log.severe("[getFaces] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
final apiFaces = await api.RecognizeFaceParser().parse(response.body);
return apiFaces
.map(ApiRecognizeFaceConverter.fromApi)
.where((e) => e.label.isNotEmpty)
.toList();
}
@override
Future<List<RecognizeFaceItem>> getItems(
Account account, RecognizeFace face) async {
_log.info("[getItems] account: ${account.userId}, face: ${face.label}");
final response = await ApiUtil.fromAccount(account)
.recognize(account.userId.raw)
.face(face.label)
.propfind(
getcontentlength: 1,
getcontenttype: 1,
getetag: 1,
getlastmodified: 1,
faceDetections: 1,
fileMetadataSize: 1,
hasPreview: 1,
realpath: 1,
favorite: 1,
fileid: 1,
);
if (!response.isGood) {
_log.severe("[getItems] Failed requesting server: $response");
throw ApiException(
response: response,
message: "Server responed with an error: HTTP ${response.statusCode}",
);
}
final apiItems = await api.RecognizeFaceItemParser().parse(response.body);
return apiItems
.where((f) => f.fileId != null)
.map(ApiRecognizeFaceItemConverter.fromApi)
.toList();
}
@override
Future<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
}) async {
final results = await Future.wait(faces.map((f) async {
try {
return MapEntry(f, await getItems(account, f));
} catch (e, stackTrace) {
_log.severe("[getMultiFaceItems] Failed while querying face: $f", e,
stackTrace);
onError?.call(f, e, stackTrace);
return null;
}
}));
return results.whereNotNull().toMap();
}
@override
Future<Map<RecognizeFace, RecognizeFaceItem>> getMultiFaceLastItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
}) async {
final results = await getMultiFaceItems(account, faces, onError: onError);
return results
.map((key, value) => MapEntry(key, maxBy(value, (e) => e.fileId)!));
}
}
@npLog
class RecognizeFaceSqliteDbDataSource implements RecognizeFaceDataSource {
const RecognizeFaceSqliteDbDataSource(this.sqliteDb);
@override
Future<List<RecognizeFace>> getFaces(Account account) async {
_log.info("[getFaces] $account");
final dbFaces = await sqliteDb.use((db) async {
return await db.allRecognizeFaces(
account: sql.ByAccount.app(account),
);
});
return dbFaces
.map((f) {
try {
return SqliteRecognizeFaceConverter.fromSql(f);
} catch (e, stackTrace) {
_log.severe(
"[getFaces] Failed while converting DB entry", e, stackTrace);
return null;
}
})
.whereNotNull()
.toList();
}
@override
Future<List<RecognizeFaceItem>> getItems(
Account account, RecognizeFace face) async {
_log.info("[getItems] $face");
final results = await getMultiFaceItems(account, [face]);
return results[face]!;
}
@override
Future<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
List<RecognizeFaceItemSort>? orderBy,
int? limit,
}) async {
_log.info("[getMultiFaceItems] ${faces.toReadableString()}");
final dbItems = await sqliteDb.use((db) async {
final results = await Future.wait(faces.map((f) async {
try {
return MapEntry(
f,
await db.recognizeFaceItemsByParentLabel(
account: sql.ByAccount.app(account),
label: f.label,
orderBy: orderBy?.toOrderingItem(db).toList(),
limit: limit,
),
);
} catch (e, stackTrace) {
onError?.call(f, e, stackTrace);
return null;
}
}));
return results.whereNotNull().toMap();
});
return dbItems.entries
.map((entry) {
final face = entry.key;
try {
return MapEntry(
face,
entry.value
.map((i) => SqliteRecognizeFaceItemConverter.fromSql(
account.userId.raw, face.label, i))
.toList(),
);
} catch (e, stackTrace) {
onError?.call(face, e, stackTrace);
return null;
}
})
.whereNotNull()
.toMap();
}
@override
Future<Map<RecognizeFace, RecognizeFaceItem>> getMultiFaceLastItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
}) async {
final results = await getMultiFaceItems(
account,
faces,
onError: onError,
orderBy: [RecognizeFaceItemSort.fileIdDesc],
limit: 1,
);
return (results..removeWhere((key, value) => value.isEmpty))
.map((key, value) => MapEntry(key, value.first));
}
final sql.SqliteDb sqliteDb;
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'data_source.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$RecognizeFaceRemoteDataSourceNpLog
on RecognizeFaceRemoteDataSource {
// ignore: unused_element
Logger get _log => log;
static final log =
Logger("entity.recognize_face.data_source.RecognizeFaceRemoteDataSource");
}
extension _$RecognizeFaceSqliteDbDataSourceNpLog
on RecognizeFaceSqliteDbDataSource {
// ignore: unused_element
Logger get _log => log;
static final log = Logger(
"entity.recognize_face.data_source.RecognizeFaceSqliteDbDataSource");
}

View file

@ -0,0 +1,75 @@
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
part 'repo.g.dart';
abstract class RecognizeFaceRepo {
/// Query all [RecognizeFace]s belonging to [account]
Stream<List<RecognizeFace>> getFaces(Account account);
/// Query all items belonging to [face]
Stream<List<RecognizeFaceItem>> getItems(Account account, RecognizeFace face);
/// Query all items belonging to each face
Stream<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
});
}
/// A repo that simply relay the call to the backed [NcAlbumDataSource]
@npLog
class BasicRecognizeFaceRepo implements RecognizeFaceRepo {
const BasicRecognizeFaceRepo(this.dataSrc);
@override
Stream<List<RecognizeFace>> getFaces(Account account) async* {
yield await dataSrc.getFaces(account);
}
@override
Stream<List<RecognizeFaceItem>> getItems(
Account account, RecognizeFace face) async* {
yield await dataSrc.getItems(account, face);
}
@override
Stream<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
}) async* {
yield await dataSrc.getMultiFaceItems(account, faces, onError: onError);
}
final RecognizeFaceDataSource dataSrc;
}
abstract class RecognizeFaceDataSource {
/// Query all [RecognizeFace]s belonging to [account]
Future<List<RecognizeFace>> getFaces(Account account);
/// Query all items belonging to [face]
Future<List<RecognizeFaceItem>> getItems(Account account, RecognizeFace face);
/// Query all items belonging to each face
Future<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
});
/// Query the last items belonging to each face
Future<Map<RecognizeFace, RecognizeFaceItem>> getMultiFaceLastItems(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
});
}

View file

@ -1,14 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'data_source.dart';
part of 'repo.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$FaceRemoteDataSourceNpLog on FaceRemoteDataSource {
extension _$BasicRecognizeFaceRepoNpLog on BasicRecognizeFaceRepo {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("entity.face.data_source.FaceRemoteDataSource");
static final log =
Logger("entity.recognize_face.repo.BasicRecognizeFaceRepo");
}

View file

@ -0,0 +1,113 @@
import 'package:equatable/equatable.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_common/string_extension.dart';
import 'package:to_string/to_string.dart';
part 'recognize_face_item.g.dart';
@ToString(ignoreNull: true)
class RecognizeFaceItem with EquatableMixin {
const RecognizeFaceItem({
required this.path,
required this.fileId,
this.contentLength,
this.contentType,
this.etag,
this.lastModified,
this.hasPreview,
this.realPath,
this.isFavorite,
this.fileMetadataWidth,
this.fileMetadataHeight,
this.faceDetections,
});
@override
String toString() => _$toString();
@override
List<Object?> get props => [
path,
fileId,
contentLength,
contentType,
etag,
lastModified,
hasPreview,
realPath,
isFavorite,
fileMetadataWidth,
fileMetadataHeight,
faceDetections,
];
final String path;
final int fileId;
final int? contentLength;
final String? contentType;
final String? etag;
final DateTime? lastModified;
final bool? hasPreview;
final String? realPath;
final bool? isFavorite;
final int? fileMetadataWidth;
final int? fileMetadataHeight;
final List<Map<String, dynamic>>? faceDetections;
}
extension RecognizeFaceItemExtension on RecognizeFaceItem {
/// Return the path of this item with the DAV part stripped
///
/// WebDAV file path: remote.php/dav/recognize/{userId}/faces/{face}/{strippedPath}.
/// If this path points to the user's root album path, return "."
String get strippedPath {
if (!path.startsWith("${api.ApiRecognize.path}/")) {
throw ArgumentError("Unsupported path: $path");
}
var begin = "${api.ApiRecognize.path}/".length;
begin = path.indexOf("/", begin);
if (begin == -1) {
throw ArgumentError("Unsupported path: $path");
}
// /faces/{face}/{strippedPath}
if (path.slice(begin, begin + 6) != "/faces") {
throw ArgumentError("Unsupported path: $path");
}
begin += 7;
// {face}/{strippedPath}
begin = path.indexOf("/", begin);
if (begin == -1) {
return ".";
}
return path.slice(begin + 1);
}
bool compareIdentity(RecognizeFaceItem other) => fileId == other.fileId;
int get identityHashCode => fileId.hashCode;
static int identityComparator(RecognizeFaceItem a, RecognizeFaceItem b) =>
a.fileId.compareTo(b.fileId);
File toFile() {
Metadata? metadata;
if (fileMetadataWidth != null && fileMetadataHeight != null) {
metadata = Metadata(
imageWidth: fileMetadataWidth,
imageHeight: fileMetadataHeight,
);
}
return File(
path: realPath ?? path,
fileId: fileId,
contentLength: contentLength,
contentType: contentType,
etag: etag,
lastModified: lastModified,
hasPreview: hasPreview,
isFavorite: isFavorite,
metadata: metadata,
);
}
}

View file

@ -0,0 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recognize_face_item.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$RecognizeFaceItemToString on RecognizeFaceItem {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "RecognizeFaceItem {path: $path, fileId: $fileId, ${contentLength == null ? "" : "contentLength: $contentLength, "}${contentType == null ? "" : "contentType: $contentType, "}${etag == null ? "" : "etag: $etag, "}${lastModified == null ? "" : "lastModified: $lastModified, "}${hasPreview == null ? "" : "hasPreview: $hasPreview, "}${realPath == null ? "" : "realPath: $realPath, "}${isFavorite == null ? "" : "isFavorite: $isFavorite, "}${fileMetadataWidth == null ? "" : "fileMetadataWidth: $fileMetadataWidth, "}${fileMetadataHeight == null ? "" : "fileMetadataHeight: $fileMetadataHeight, "}${faceDetections == null ? "" : "faceDetections: [length: ${faceDetections!.length}]"}}";
}
}

View file

@ -4,6 +4,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/person/builder.dart';
import 'package:nc_photos/entity/search.dart';
import 'package:nc_photos/entity/search_util.dart' as search_util;
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
@ -11,8 +12,9 @@ import 'package:nc_photos/entity/sqlite/files_query_builder.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/list_tagged_file.dart';
import 'package:nc_photos/use_case/populate_person.dart';
import 'package:nc_photos/use_case/person/list_person_face.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/ci_string.dart';
@ -162,7 +164,7 @@ class SearchSqliteDbDataSource implements SearchDataSource {
// "Ada" will return results from "Ada Crook" but NOT "Adabelle"
try {
final dbPersons = await _c.sqliteDb.use((db) async {
return await db.personsByName(
return await db.faceRecognitionPersonsByName(
appAccount: account,
name: criteria.input,
);
@ -170,13 +172,16 @@ class SearchSqliteDbDataSource implements SearchDataSource {
if (dbPersons.isEmpty) {
return [];
}
final persons = await dbPersons.convertToAppPerson();
final persons = (await dbPersons.convertToAppFaceRecognitionPerson())
.map((p) => PersonBuilder.byFaceRecognitionPerson(account, p))
.toList();
_log.info(
"[_listByPerson] Found people: ${persons.map((p) => p.name).toReadableString()}");
final futures = await Future.wait(
persons.map((p) async => await _c.faceRepo.list(account, p)));
persons.map((p) async => ListPersonFace(_c)(account, p).last));
final faces = futures.flatten().toList();
final files = await PopulatePerson(_c)(account, faces);
final files = await InflateFileDescriptor(_c)
.call(account, faces.map((e) => e.file).toList());
return files
.where((f) => criteria.filters.every((c) => c.isSatisfy(f)))
.toList();

View file

@ -18,8 +18,8 @@ import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:np_codegen/np_codegen.dart';
part 'database.g.dart';
part 'database_extension.dart';
part 'database/nc_album_extension.dart';
part 'database_extension.dart';
// remember to also update the truncate method after adding a new table
@npLog
@ -36,9 +36,11 @@ part 'database/nc_album_extension.dart';
Albums,
AlbumShares,
Tags,
Persons,
FaceRecognitionPersons,
NcAlbums,
NcAlbumItems,
RecognizeFaces,
RecognizeFaceItems,
],
)
class SqliteDb extends _$SqliteDb {
@ -47,7 +49,7 @@ class SqliteDb extends _$SqliteDb {
}) : super(executor ?? platform.openSqliteConnection());
@override
get schemaVersion => 5;
get schemaVersion => 6;
@override
get migration => MigrationStrategy(
@ -86,7 +88,7 @@ class SqliteDb extends _$SqliteDb {
await transaction(() async {
if (from < 2) {
await m.createTable(tags);
await m.createTable(persons);
await m.createTable(faceRecognitionPersons);
await _createIndexV2(m);
}
if (from < 3) {
@ -100,6 +102,13 @@ class SqliteDb extends _$SqliteDb {
await m.createTable(ncAlbums);
await m.createTable(ncAlbumItems);
}
if (from < 6) {
if (from >= 2) {
await m.renameTable(faceRecognitionPersons, "persons");
}
await m.createTable(recognizeFaces);
await m.createTable(recognizeFaceItems);
}
});
} catch (e, stackTrace) {
_log.shout("[onUpgrade] Failed upgrading sqlite db", e, stackTrace);
@ -119,8 +128,8 @@ class SqliteDb extends _$SqliteDb {
Future<void> _createIndexV2(Migrator m) async {
await m.createIndex(Index("tags_server_index",
"CREATE INDEX tags_server_index ON tags(server);"));
await m.createIndex(Index("persons_account_index",
"CREATE INDEX persons_account_index ON persons(account);"));
await m.createIndex(Index("face_recognition_persons_account_index",
"CREATE INDEX face_recognition_persons_account_index ON face_recognition_persons(account);"));
}
Future<void> _createIndexV3(Migrator m) async {

File diff suppressed because it is too large Load diff

View file

@ -526,37 +526,37 @@ extension SqliteDbExtension on SqliteDb {
}
}
Future<List<Person>> allPersons({
Account? sqlAccount,
app.Account? appAccount,
Future<List<FaceRecognitionPerson>> allFaceRecognitionPersons({
required ByAccount account,
}) {
assert((sqlAccount != null) != (appAccount != null));
if (sqlAccount != null) {
final query = select(persons)
..where((t) => t.account.equals(sqlAccount.rowId));
assert((account.sqlAccount != null) != (account.appAccount != null));
if (account.sqlAccount != null) {
final query = select(faceRecognitionPersons)
..where((t) => t.account.equals(account.sqlAccount!.rowId));
return query.get();
} else {
final query = select(persons).join([
innerJoin(accounts, accounts.rowId.equalsExp(persons.account),
final query = select(faceRecognitionPersons).join([
innerJoin(
accounts, accounts.rowId.equalsExp(faceRecognitionPersons.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(appAccount!.url))
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(appAccount.userId.toCaseInsensitiveString()));
return query.map((r) => r.readTable(persons)).get();
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
return query.map((r) => r.readTable(faceRecognitionPersons)).get();
}
}
Future<List<Person>> personsByName({
Future<List<FaceRecognitionPerson>> faceRecognitionPersonsByName({
Account? sqlAccount,
app.Account? appAccount,
required String name,
}) {
assert((sqlAccount != null) != (appAccount != null));
if (sqlAccount != null) {
final query = select(persons)
final query = select(faceRecognitionPersons)
..where((t) => t.account.equals(sqlAccount.rowId))
..where((t) =>
t.name.like(name) |
@ -564,8 +564,9 @@ extension SqliteDbExtension on SqliteDb {
t.name.like("$name %"));
return query.get();
} else {
final query = select(persons).join([
innerJoin(accounts, accounts.rowId.equalsExp(persons.account),
final query = select(faceRecognitionPersons).join([
innerJoin(
accounts, accounts.rowId.equalsExp(faceRecognitionPersons.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
@ -573,13 +574,99 @@ extension SqliteDbExtension on SqliteDb {
..where(servers.address.equals(appAccount!.url))
..where(
accounts.userId.equals(appAccount.userId.toCaseInsensitiveString()))
..where(persons.name.like(name) |
persons.name.like("% $name") |
persons.name.like("$name %"));
return query.map((r) => r.readTable(persons)).get();
..where(faceRecognitionPersons.name.like(name) |
faceRecognitionPersons.name.like("% $name") |
faceRecognitionPersons.name.like("$name %"));
return query.map((r) => r.readTable(faceRecognitionPersons)).get();
}
}
Future<List<RecognizeFace>> allRecognizeFaces({
required ByAccount account,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
if (account.sqlAccount != null) {
final query = select(recognizeFaces)
..where((t) => t.account.equals(account.sqlAccount!.rowId));
return query.get();
} else {
final query = select(recognizeFaces).join([
innerJoin(accounts, accounts.rowId.equalsExp(recognizeFaces.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()));
return query.map((r) => r.readTable(recognizeFaces)).get();
}
}
Future<RecognizeFace> recognizeFaceByLabel({
required ByAccount account,
required String label,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
if (account.sqlAccount != null) {
final query = select(recognizeFaces)
..where((t) => t.account.equals(account.sqlAccount!.rowId))
..where((t) => t.label.equals(label));
return query.getSingle();
} else {
final query = select(recognizeFaces).join([
innerJoin(accounts, accounts.rowId.equalsExp(recognizeFaces.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()))
..where(recognizeFaces.label.equals(label));
return query.map((r) => r.readTable(recognizeFaces)).getSingle();
}
}
Future<List<RecognizeFaceItem>> recognizeFaceItemsByParentLabel({
required ByAccount account,
required String label,
List<OrderingTerm>? orderBy,
int? limit,
int? offset,
}) {
assert((account.sqlAccount != null) != (account.appAccount != null));
final query = select(recognizeFaceItems).join([
innerJoin(recognizeFaces,
recognizeFaces.rowId.equalsExp(recognizeFaceItems.parent),
useColumns: false),
]);
if (account.sqlAccount != null) {
query
..where(recognizeFaces.account.equals(account.sqlAccount!.rowId))
..where(recognizeFaces.label.equals(label));
} else {
query
..join([
innerJoin(accounts, accounts.rowId.equalsExp(recognizeFaces.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
])
..where(servers.address.equals(account.appAccount!.url))
..where(accounts.userId
.equals(account.appAccount!.userId.toCaseInsensitiveString()))
..where(recognizeFaces.label.equals(label));
}
if (orderBy != null) {
query.orderBy(orderBy);
if (limit != null) {
query.limit(limit, offset: offset);
}
}
return query.map((r) => r.readTable(recognizeFaceItems)).get();
}
Future<int> countMissingMetadataByFileIds({
Account? sqlAccount,
app.Account? appAccount,
@ -639,9 +726,11 @@ extension SqliteDbExtension on SqliteDb {
await delete(albums).go();
await delete(albumShares).go();
await delete(tags).go();
await delete(persons).go();
await delete(faceRecognitionPersons).go();
await delete(ncAlbums).go();
await delete(ncAlbumItems).go();
await delete(recognizeFaces).go();
await delete(recognizeFaceItems).go();
// reset the auto increment counter
await customStatement("UPDATE sqlite_sequence SET seq=0;");

View file

@ -1,4 +1,8 @@
import 'package:drift/drift.dart';
import 'package:nc_photos/entity/sqlite/database.dart';
import 'package:np_codegen/np_codegen.dart';
part 'table.g.dart';
class Servers extends Table {
IntColumn get rowId => integer().autoIncrement()();
@ -219,7 +223,7 @@ class Tags extends Table {
];
}
class Persons extends Table {
class FaceRecognitionPersons extends Table {
IntColumn get rowId => integer().autoIncrement()();
IntColumn get account =>
integer().references(Accounts, #rowId, onDelete: KeyAction.cascade)();
@ -233,6 +237,43 @@ class Persons extends Table {
];
}
class RecognizeFaces extends Table {
IntColumn get rowId => integer().autoIncrement()();
IntColumn get account =>
integer().references(Accounts, #rowId, onDelete: KeyAction.cascade)();
TextColumn get label => text()();
@override
List<Set<Column>>? get uniqueKeys => [
{account, label},
];
}
@DriftTableSort("SqliteDb")
class RecognizeFaceItems extends Table {
IntColumn get rowId => integer().autoIncrement()();
IntColumn get parent => integer()
.references(RecognizeFaces, #rowId, onDelete: KeyAction.cascade)();
TextColumn get relativePath => text()();
IntColumn get fileId => integer()();
IntColumn get contentLength => integer().nullable()();
TextColumn get contentType => text().nullable()();
TextColumn get etag => text().nullable()();
DateTimeColumn get lastModified =>
dateTime().map(const SqliteDateTimeConverter()).nullable()();
BoolColumn get hasPreview => boolean().nullable()();
TextColumn get realPath => text().nullable()();
BoolColumn get isFavorite => boolean().nullable()();
IntColumn get fileMetadataWidth => integer().nullable()();
IntColumn get fileMetadataHeight => integer().nullable()();
TextColumn get faceDetections => text().nullable()();
@override
List<Set<Column>>? get uniqueKeys => [
{parent, fileId},
];
}
class SqliteDateTimeConverter extends TypeConverter<DateTime, DateTime> {
const SqliteDateTimeConverter();

View file

@ -0,0 +1,104 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'table.dart';
// **************************************************************************
// DriftTableSortGenerator
// **************************************************************************
enum RecognizeFaceItemSort {
rowIdAsc,
rowIdDesc,
parentAsc,
parentDesc,
relativePathAsc,
relativePathDesc,
fileIdAsc,
fileIdDesc,
contentLengthAsc,
contentLengthDesc,
contentTypeAsc,
contentTypeDesc,
etagAsc,
etagDesc,
lastModifiedAsc,
lastModifiedDesc,
hasPreviewAsc,
hasPreviewDesc,
realPathAsc,
realPathDesc,
isFavoriteAsc,
isFavoriteDesc,
fileMetadataWidthAsc,
fileMetadataWidthDesc,
fileMetadataHeightAsc,
fileMetadataHeightDesc,
faceDetectionsAsc,
faceDetectionsDesc,
}
extension RecognizeFaceItemSortIterableExtension
on Iterable<RecognizeFaceItemSort> {
Iterable<OrderingTerm> toOrderingItem(SqliteDb db) {
return map((s) {
switch (s) {
case RecognizeFaceItemSort.rowIdAsc:
return OrderingTerm.asc(db.recognizeFaceItems.rowId);
case RecognizeFaceItemSort.rowIdDesc:
return OrderingTerm.desc(db.recognizeFaceItems.rowId);
case RecognizeFaceItemSort.parentAsc:
return OrderingTerm.asc(db.recognizeFaceItems.parent);
case RecognizeFaceItemSort.parentDesc:
return OrderingTerm.desc(db.recognizeFaceItems.parent);
case RecognizeFaceItemSort.relativePathAsc:
return OrderingTerm.asc(db.recognizeFaceItems.relativePath);
case RecognizeFaceItemSort.relativePathDesc:
return OrderingTerm.desc(db.recognizeFaceItems.relativePath);
case RecognizeFaceItemSort.fileIdAsc:
return OrderingTerm.asc(db.recognizeFaceItems.fileId);
case RecognizeFaceItemSort.fileIdDesc:
return OrderingTerm.desc(db.recognizeFaceItems.fileId);
case RecognizeFaceItemSort.contentLengthAsc:
return OrderingTerm.asc(db.recognizeFaceItems.contentLength);
case RecognizeFaceItemSort.contentLengthDesc:
return OrderingTerm.desc(db.recognizeFaceItems.contentLength);
case RecognizeFaceItemSort.contentTypeAsc:
return OrderingTerm.asc(db.recognizeFaceItems.contentType);
case RecognizeFaceItemSort.contentTypeDesc:
return OrderingTerm.desc(db.recognizeFaceItems.contentType);
case RecognizeFaceItemSort.etagAsc:
return OrderingTerm.asc(db.recognizeFaceItems.etag);
case RecognizeFaceItemSort.etagDesc:
return OrderingTerm.desc(db.recognizeFaceItems.etag);
case RecognizeFaceItemSort.lastModifiedAsc:
return OrderingTerm.asc(db.recognizeFaceItems.lastModified);
case RecognizeFaceItemSort.lastModifiedDesc:
return OrderingTerm.desc(db.recognizeFaceItems.lastModified);
case RecognizeFaceItemSort.hasPreviewAsc:
return OrderingTerm.asc(db.recognizeFaceItems.hasPreview);
case RecognizeFaceItemSort.hasPreviewDesc:
return OrderingTerm.desc(db.recognizeFaceItems.hasPreview);
case RecognizeFaceItemSort.realPathAsc:
return OrderingTerm.asc(db.recognizeFaceItems.realPath);
case RecognizeFaceItemSort.realPathDesc:
return OrderingTerm.desc(db.recognizeFaceItems.realPath);
case RecognizeFaceItemSort.isFavoriteAsc:
return OrderingTerm.asc(db.recognizeFaceItems.isFavorite);
case RecognizeFaceItemSort.isFavoriteDesc:
return OrderingTerm.desc(db.recognizeFaceItems.isFavorite);
case RecognizeFaceItemSort.fileMetadataWidthAsc:
return OrderingTerm.asc(db.recognizeFaceItems.fileMetadataWidth);
case RecognizeFaceItemSort.fileMetadataWidthDesc:
return OrderingTerm.desc(db.recognizeFaceItems.fileMetadataWidth);
case RecognizeFaceItemSort.fileMetadataHeightAsc:
return OrderingTerm.asc(db.recognizeFaceItems.fileMetadataHeight);
case RecognizeFaceItemSort.fileMetadataHeightDesc:
return OrderingTerm.desc(db.recognizeFaceItems.fileMetadataHeight);
case RecognizeFaceItemSort.faceDetectionsAsc:
return OrderingTerm.asc(db.recognizeFaceItems.faceDetections);
case RecognizeFaceItemSort.faceDetectionsDesc:
return OrderingTerm.desc(db.recognizeFaceItems.faceDetections);
}
});
}
}

View file

@ -6,11 +6,13 @@ import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/nc_album.dart';
import 'package:nc_photos/entity/nc_album_item.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/iterable_extension.dart';
@ -18,6 +20,7 @@ import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/or_null.dart';
import 'package:np_api/np_api.dart' as api;
import 'package:np_common/ci_string.dart';
import 'package:np_common/type.dart';
extension SqlTagListExtension on List<sql.Tag> {
Future<List<Tag>> convertToAppTag() {
@ -35,19 +38,36 @@ extension AppTagListExtension on List<Tag> {
}
}
extension SqlPersonListExtension on List<sql.Person> {
Future<List<Person>> convertToAppPerson() {
return computeAll(SqlitePersonConverter.fromSql);
extension SqlFaceRecognitionPersonListExtension
on List<sql.FaceRecognitionPerson> {
Future<List<FaceRecognitionPerson>> convertToAppFaceRecognitionPerson() {
return computeAll(SqliteFaceRecognitionPersonConverter.fromSql);
}
}
extension AppPersonListExtension on List<Person> {
Future<List<sql.PersonsCompanion>> convertToPersonCompanion(
sql.Account? dbAccount) {
extension AppFaceRecognitionPersonListExtension on List<FaceRecognitionPerson> {
Future<List<sql.FaceRecognitionPersonsCompanion>>
convertToFaceRecognitionPersonCompanion(sql.Account? dbAccount) {
return map((p) => {
"account": dbAccount,
"person": p,
}).computeAll(_convertAppPerson);
}).computeAll(_convertAppFaceRecognitionPerson);
}
}
extension SqlRecognizeFaceListExtension on List<sql.RecognizeFace> {
Future<List<RecognizeFace>> convertToAppRecognizeFace() {
return computeAll(SqliteRecognizeFaceConverter.fromSql);
}
}
extension AppRecognizeFaceListExtension on List<RecognizeFace> {
Future<List<sql.RecognizeFacesCompanion>> convertToRecognizeFaceCompanion(
sql.Account? dbAccount) {
return map((f) => {
"account": dbAccount,
"face": f,
}).computeAll(_convertAppRecognizeFace);
}
}
@ -238,15 +258,17 @@ class SqliteTagConverter {
);
}
class SqlitePersonConverter {
static Person fromSql(sql.Person person) => Person(
class SqliteFaceRecognitionPersonConverter {
static FaceRecognitionPerson fromSql(sql.FaceRecognitionPerson person) =>
FaceRecognitionPerson(
name: person.name,
thumbFaceId: person.thumbFaceId,
count: person.count,
);
static sql.PersonsCompanion toSql(sql.Account? dbAccount, Person person) =>
sql.PersonsCompanion(
static sql.FaceRecognitionPersonsCompanion toSql(
sql.Account? dbAccount, FaceRecognitionPerson person) =>
sql.FaceRecognitionPersonsCompanion(
account:
dbAccount == null ? const Value.absent() : Value(dbAccount.rowId),
name: Value(person.name),
@ -323,14 +345,76 @@ class SqliteNcAlbumItemConverter {
);
}
class SqliteRecognizeFaceConverter {
static RecognizeFace fromSql(sql.RecognizeFace face) => RecognizeFace(
label: face.label,
);
static sql.RecognizeFacesCompanion toSql(
sql.Account? dbAccount, RecognizeFace face) =>
sql.RecognizeFacesCompanion(
account:
dbAccount == null ? const Value.absent() : Value(dbAccount.rowId),
label: Value(face.label),
);
}
class SqliteRecognizeFaceItemConverter {
static RecognizeFaceItem fromSql(
String userId, String faceLabel, sql.RecognizeFaceItem item) =>
RecognizeFaceItem(
path:
"${api.ApiRecognize.path}/$userId/faces/$faceLabel/${item.relativePath}",
fileId: item.fileId,
contentLength: item.contentLength,
contentType: item.contentType,
etag: item.etag,
lastModified: item.lastModified,
hasPreview: item.hasPreview,
realPath: item.realPath,
isFavorite: item.isFavorite,
fileMetadataWidth: item.fileMetadataWidth,
fileMetadataHeight: item.fileMetadataHeight,
faceDetections: item.faceDetections
?.run((obj) => (jsonDecode(obj) as List).cast<JsonObj>()),
);
static sql.RecognizeFaceItemsCompanion toSql(
sql.RecognizeFace parent,
RecognizeFaceItem item,
) =>
sql.RecognizeFaceItemsCompanion(
parent: Value(parent.rowId),
relativePath: Value(item.strippedPath),
fileId: Value(item.fileId),
contentLength: Value(item.contentLength),
contentType: Value(item.contentType),
etag: Value(item.etag),
lastModified: Value(item.lastModified),
hasPreview: Value(item.hasPreview),
realPath: Value(item.realPath),
isFavorite: Value(item.isFavorite),
fileMetadataWidth: Value(item.fileMetadataWidth),
fileMetadataHeight: Value(item.fileMetadataHeight),
faceDetections:
Value(item.faceDetections?.run((obj) => jsonEncode(obj))),
);
}
sql.TagsCompanion _convertAppTag(Map map) {
final account = map["account"] as sql.Account?;
final tag = map["tag"] as Tag;
return SqliteTagConverter.toSql(account, tag);
}
sql.PersonsCompanion _convertAppPerson(Map map) {
sql.FaceRecognitionPersonsCompanion _convertAppFaceRecognitionPerson(Map map) {
final account = map["account"] as sql.Account?;
final person = map["person"] as Person;
return SqlitePersonConverter.toSql(account, person);
final person = map["person"] as FaceRecognitionPerson;
return SqliteFaceRecognitionPersonConverter.toSql(account, person);
}
sql.RecognizeFacesCompanion _convertAppRecognizeFace(Map map) {
final account = map["account"] as sql.Account?;
final face = map["face"] as RecognizeFace;
return SqliteRecognizeFaceConverter.toSql(account, face);
}

View file

@ -320,6 +320,10 @@
"@settingsShareFolderPickerDescription": {
"description": "Set the share folder such that it matches the parameter in config.php"
},
"settingsPersonProviderTitle": "Person provider",
"@settingsPersonProviderTitle": {
"description": "Select the server app for face recognition"
},
"settingsServerAppSectionTitle": "Server app support",
"@settingsServerAppSectionTitle": {
"description": "Enable/disable various server apps"
@ -1265,8 +1269,8 @@
"description": "Crop the image"
},
"categoriesLabel": "Categories",
"searchLandingPeopleListEmptyText": "Press help to learn how to setup",
"@searchLandingPeopleListEmptyText": {
"searchLandingPeopleListEmptyText2": "Press settings to switch provider or press help to learn more",
"@searchLandingPeopleListEmptyText2": {
"description": "Shown in the search landing page under the People section when there are no people"
},
"searchLandingCategoryVideosLabel": "Videos",

View file

@ -1,7 +1,9 @@
{
"cs": [
"nameInputInvalidEmpty",
"settingsPersonProviderTitle",
"settingsServerVersionTitle",
"searchLandingPeopleListEmptyText2",
"createCollectionFailureNotification",
"addItemToCollectionTooltip",
"addItemToCollectionFailureNotification",
@ -28,6 +30,7 @@
"settingsShareFolderDialogTitle",
"settingsShareFolderDialogDescription",
"settingsShareFolderPickerDescription",
"settingsPersonProviderTitle",
"settingsServerAppSectionTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
@ -177,7 +180,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -227,6 +230,7 @@
"settingsExifWifiOnlyFalseSubtitle",
"settingsAccountLabelTitle",
"settingsAccountLabelDescription",
"settingsPersonProviderTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
@ -283,7 +287,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -323,10 +327,14 @@
],
"es": [
"settingsPersonProviderTitle",
"searchLandingPeopleListEmptyText2",
"accountSettingsTooltip"
],
"fi": [
"settingsPersonProviderTitle",
"searchLandingPeopleListEmptyText2",
"accountSettingsTooltip"
],
@ -341,6 +349,7 @@
"settingsExifWifiOnlyFalseSubtitle",
"settingsAccountLabelTitle",
"settingsAccountLabelDescription",
"settingsPersonProviderTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
@ -416,7 +425,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -458,6 +467,7 @@
"it": [
"settingsShareFolderDialogDescription",
"settingsShareFolderPickerDescription",
"settingsPersonProviderTitle",
"settingsServerAppSectionTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
@ -717,7 +727,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -805,6 +815,7 @@
"settingsShareFolderDialogTitle",
"settingsShareFolderDialogDescription",
"settingsShareFolderPickerDescription",
"settingsPersonProviderTitle",
"settingsServerAppSectionTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
@ -1064,7 +1075,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -1121,6 +1132,7 @@
"settingsExifWifiOnlyFalseSubtitle",
"settingsAccountLabelTitle",
"settingsAccountLabelDescription",
"settingsPersonProviderTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
@ -1211,7 +1223,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -1252,7 +1264,9 @@
"pt": [
"nameInputInvalidEmpty",
"settingsPersonProviderTitle",
"settingsServerVersionTitle",
"searchLandingPeopleListEmptyText2",
"createCollectionFailureNotification",
"addItemToCollectionTooltip",
"addItemToCollectionFailureNotification",
@ -1275,6 +1289,7 @@
"settingsExifWifiOnlyFalseSubtitle",
"settingsAccountLabelTitle",
"settingsAccountLabelDescription",
"settingsPersonProviderTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
@ -1347,7 +1362,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -1396,6 +1411,7 @@
"settingsExifWifiOnlyFalseSubtitle",
"settingsAccountLabelTitle",
"settingsAccountLabelDescription",
"settingsPersonProviderTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
@ -1468,7 +1484,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",
@ -1517,6 +1533,7 @@
"settingsExifWifiOnlyFalseSubtitle",
"settingsAccountLabelTitle",
"settingsAccountLabelDescription",
"settingsPersonProviderTitle",
"settingsPhotosDescription",
"settingsMemoriesRangeTitle",
"settingsMemoriesRangeValueText",
@ -1589,7 +1606,7 @@
"imageEditTransformOrientationCounterclockwise",
"imageEditTransformCrop",
"categoriesLabel",
"searchLandingPeopleListEmptyText",
"searchLandingPeopleListEmptyText2",
"searchLandingCategoryVideosLabel",
"searchFilterButtonLabel",
"searchFilterDialogTitle",

View file

@ -0,0 +1,15 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/face_recognition_face.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
class ListFaceRecognitionFace {
const ListFaceRecognitionFace(this._c);
/// List all [FaceRecognitionFace]s belonging to [person]
Stream<List<FaceRecognitionFace>> call(
Account account, FaceRecognitionPerson person) =>
_c.faceRecognitionPersonRepo.getFaces(account, person);
final DiContainer _c;
}

View file

@ -0,0 +1,13 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
class ListFaceRecognitionPerson {
const ListFaceRecognitionPerson(this._c);
/// List all [FaceRecognitionPerson]s belonging to [account]
Stream<List<FaceRecognitionPerson>> call(Account account) =>
_c.faceRecognitionPersonRepo.getPersons(account);
final DiContainer _c;
}

View file

@ -3,41 +3,45 @@ import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/list_util.dart' as list_util;
import 'package:nc_photos/use_case/face_recognition_person/list_face_recognition_person.dart';
import 'package:np_codegen/np_codegen.dart';
part 'sync_person.g.dart';
part 'sync_face_recognition_person.g.dart';
@npLog
class SyncPerson {
SyncPerson(this._c) : assert(require(_c));
static bool require(DiContainer c) =>
DiContainer.has(c, DiType.personRepoRemote) &&
DiContainer.has(c, DiType.personRepoLocal);
class SyncFaceRecognitionPerson {
const SyncFaceRecognitionPerson(this._c);
/// Sync people in cache db with remote server
Future<void> call(Account account) async {
///
/// Return if any people were updated
Future<bool> call(Account account) async {
_log.info("[call] Sync people with remote");
late final List<Person> remote;
int personSorter(FaceRecognitionPerson a, FaceRecognitionPerson b) =>
a.name.compareTo(b.name);
late final List<FaceRecognitionPerson> remote;
try {
remote = await _c.personRepoRemote.list(account);
remote = (await ListFaceRecognitionPerson(_c.withRemoteRepo())(account)
.last)
..sort(personSorter);
} catch (e) {
if (e is ApiException && e.response.statusCode == 404) {
// face recognition app probably not installed, ignore
_log.info("[call] Face Recognition app not installed");
return;
return false;
}
rethrow;
}
final cache = await _c.personRepoLocal.list(account);
int personSorter(Person a, Person b) => a.name.compareTo(b.name);
final diff = list_util.diffWith<Person>(cache, remote, personSorter);
final cache = (await ListFaceRecognitionPerson(_c.withLocalRepo())(account)
.last)
..sort(personSorter);
final diff = list_util.diffWith(cache, remote, personSorter);
final inserts = diff.onlyInB;
_log.info("[call] New people: ${inserts.toReadableString()}");
final deletes = diff.onlyInA;
@ -54,29 +58,35 @@ class SyncPerson {
await db.batch((batch) {
for (final d in deletes) {
batch.deleteWhere(
db.persons,
(sql.$PersonsTable p) =>
db.faceRecognitionPersons,
(sql.$FaceRecognitionPersonsTable p) =>
p.account.equals(dbAccount.rowId) & p.name.equals(d.name),
);
}
for (final u in updates) {
batch.update(
db.persons,
sql.PersonsCompanion(
db.faceRecognitionPersons,
sql.FaceRecognitionPersonsCompanion(
name: sql.Value(u.name),
thumbFaceId: sql.Value(u.thumbFaceId),
count: sql.Value(u.count),
),
where: (sql.$PersonsTable p) =>
where: (sql.$FaceRecognitionPersonsTable p) =>
p.account.equals(dbAccount.rowId) & p.name.equals(u.name),
);
}
for (final i in inserts) {
batch.insert(db.persons, SqlitePersonConverter.toSql(dbAccount, i),
mode: sql.InsertMode.insertOrIgnore);
batch.insert(
db.faceRecognitionPersons,
SqliteFaceRecognitionPersonConverter.toSql(dbAccount, i),
mode: sql.InsertMode.insertOrIgnore,
);
}
});
});
return true;
} else {
return false;
}
}

View file

@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sync_face_recognition_person.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$SyncFaceRecognitionPersonNpLog on SyncFaceRecognitionPerson {
// ignore: unused_element
Logger get _log => log;
static final log = Logger(
"use_case.face_recognition_person.sync_face_recognition_person.SyncFaceRecognitionPerson");
}

View file

@ -1,15 +0,0 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/face.dart';
import 'package:nc_photos/entity/person.dart';
class ListFace {
ListFace(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.faceRepo);
Future<List<Face>> call(Account account, Person person) =>
_c.faceRepo.list(account, person);
final DiContainer _c;
}

View file

@ -1,14 +0,0 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
class ListPerson {
ListPerson(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.personRepo);
/// List all persons
Future<List<Person>> call(Account account) => _c.personRepo.list(account);
final DiContainer _c;
}

View file

@ -0,0 +1,81 @@
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/person/builder.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/use_case/face_recognition_person/list_face_recognition_person.dart';
import 'package:nc_photos/use_case/recognize_face/list_recognize_face.dart';
import 'package:nc_photos/use_case/recognize_face/list_recognize_face_item.dart';
import 'package:np_codegen/np_codegen.dart';
part 'list_person.g.dart';
@npLog
class ListPerson {
const ListPerson(this._c);
Stream<List<Person>> call(Account account, AccountPref accountPref) async* {
final provider =
PersonProvider.fromValue(accountPref.getPersonProviderOr());
_log.info("[call] Current provider: $provider");
switch (provider) {
case PersonProvider.none:
return;
case PersonProvider.faceRecognition:
yield* _withFaceRecognition(account);
break;
case PersonProvider.recognize:
yield* _withRecognize(account);
break;
}
}
Stream<List<Person>> _withFaceRecognition(Account account) async* {
try {
await for (final results in ListFaceRecognitionPerson(_c)(account)) {
yield results
.map((e) => PersonBuilder.byFaceRecognitionPerson(account, e))
.toList();
}
} catch (e, stackTrace) {
// not installed?
_log.severe(
"[_withFaceRecognition] Failed while ListFaceRecognitionPerson",
e,
stackTrace,
);
}
}
Stream<List<Person>> _withRecognize(Account account) async* {
try {
await for (final faces in ListRecognizeFace(_c)(account)) {
final itemStream = ListMultipleRecognizeFaceItem(_c)(
account,
faces,
onError: (value, e, stackTrace) {
_log.severe(
"[_withRecognize] Failed while ListRecognizeFace for $value",
e,
stackTrace,
);
},
);
await for (final items in itemStream) {
yield faces
.map((f) => PersonBuilder.byRecognizeFace(account, f, items[f]))
.toList();
}
}
} catch (e, stackTrace) {
// not installed?
_log.severe(
"[_withRecognize] Failed while ListRecognizeFace", e, stackTrace);
}
}
final DiContainer _c;
}

View file

@ -1,14 +1,14 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'face_parser.dart';
part of 'list_person.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$FaceParserNpLog on FaceParser {
extension _$ListPersonNpLog on ListPerson {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("src.entity.face_parser.FaceParser");
static final log = Logger("use_case.person.list_person.ListPerson");
}

View file

@ -0,0 +1,14 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/person/adapter.dart';
import 'package:nc_photos/entity/person_face.dart';
class ListPersonFace {
const ListPersonFace(this._c);
Stream<List<PersonFace>> call(Account account, Person person) =>
PersonAdapter.of(_c, account, person).listFace();
final DiContainer _c;
}

View file

@ -0,0 +1,39 @@
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/use_case/face_recognition_person/sync_face_recognition_person.dart';
import 'package:nc_photos/use_case/recognize_face/sync_recognize_face.dart';
import 'package:np_codegen/np_codegen.dart';
part 'sync_person.g.dart';
@npLog
class SyncPerson {
const SyncPerson(this._c);
/// Sync people in cache db with remote server
///
/// Return if any people were updated
Future<bool> call(Account account, PersonProvider provider) async {
_log.info("[call] Current provider: $provider");
switch (provider) {
case PersonProvider.none:
return false;
case PersonProvider.faceRecognition:
return _withFaceRecognition(account);
case PersonProvider.recognize:
return _withRecognize(account);
}
}
Future<bool> _withFaceRecognition(Account account) =>
SyncFaceRecognitionPerson(_c)(account);
Future<bool> _withRecognize(Account account) =>
SyncRecognizeFace(_c)(account);
final DiContainer _c;
}

View file

@ -10,5 +10,5 @@ extension _$SyncPersonNpLog on SyncPerson {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("use_case.sync_person.SyncPerson");
static final log = Logger("use_case.person.sync_person.SyncPerson");
}

View file

@ -1,40 +0,0 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/face.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:np_codegen/np_codegen.dart';
part 'populate_person.g.dart';
@npLog
class PopulatePerson {
PopulatePerson(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
/// Return a list of files of the faces
Future<List<File>> call(Account account, List<Face> faces) async {
final fileIds = faces.map((f) => f.fileId).toList();
final dbFiles = await _c.sqliteDb.use((db) async {
return await db.completeFilesByFileIds(fileIds, appAccount: account);
});
final files = await dbFiles.convertToAppFile(account);
final fileMap = Map.fromEntries(files.map((f) => MapEntry(f.fileId, f)));
return faces
.map((f) {
final file = fileMap[f.fileId];
if (file == null) {
_log.warning(
"[call] File doesn't exist in DB, removed?: ${f.fileId}");
}
return file;
})
.whereNotNull()
.toList();
}
final DiContainer _c;
}

View file

@ -0,0 +1,13 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/recognize_face.dart';
class ListRecognizeFace {
const ListRecognizeFace(this._c);
/// List all [RecognizeFace]s belonging to [account]
Stream<List<RecognizeFace>> call(Account account) =>
_c.recognizeFaceRepo.getFaces(account);
final DiContainer _c;
}

View file

@ -0,0 +1,29 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:np_common/type.dart';
class ListRecognizeFaceItem {
const ListRecognizeFaceItem(this._c);
/// List all [RecognizeFaceItem]s belonging to [face]
Stream<List<RecognizeFaceItem>> call(Account account, RecognizeFace face) =>
_c.recognizeFaceRepo.getItems(account, face);
final DiContainer _c;
}
class ListMultipleRecognizeFaceItem {
const ListMultipleRecognizeFaceItem(this._c);
/// List all [RecognizeFaceItem]s belonging to each face
Stream<Map<RecognizeFace, List<RecognizeFaceItem>>> call(
Account account,
List<RecognizeFace> faces, {
ErrorWithValueHandler<RecognizeFace>? onError,
}) =>
_c.recognizeFaceRepo.getMultiFaceItems(account, faces, onError: onError);
final DiContainer _c;
}

View file

@ -0,0 +1,288 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' as sql;
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/recognize_face.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/entity/sqlite/type_converter.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/list_util.dart' as list_util;
import 'package:nc_photos/map_extension.dart';
import 'package:nc_photos/use_case/recognize_face/list_recognize_face.dart';
import 'package:nc_photos/use_case/recognize_face/list_recognize_face_item.dart';
import 'package:np_codegen/np_codegen.dart';
part 'sync_recognize_face.g.dart';
@npLog
class SyncRecognizeFace {
const SyncRecognizeFace(this._c);
/// Sync people in cache db with remote server
///
/// Return if any people were updated
Future<bool> call(Account account) async {
_log.info("[call] Sync people with remote");
final faces = await _getFaceResults(account);
if (faces == null) {
return false;
}
var shouldUpdate = faces.inserts.isNotEmpty ||
faces.deletes.isNotEmpty ||
faces.updates.isNotEmpty;
final items =
await _getFaceItemResults(account, faces.results.values.toList());
shouldUpdate = shouldUpdate ||
items.values.any((e) =>
e.inserts.isNotEmpty ||
e.deletes.isNotEmpty ||
e.updates.isNotEmpty);
if (!shouldUpdate) {
return false;
}
await _c.sqliteDb.use((db) async {
final dbAccount = await db.accountOf(account);
await db.batch((batch) {
for (final d in faces.deletes) {
batch.deleteWhere(
db.recognizeFaces,
(sql.$RecognizeFacesTable t) =>
t.account.equals(dbAccount.rowId) &
t.label.equals(faces.results[d]!.label),
);
}
for (final u in faces.updates) {
batch.update(
db.recognizeFaces,
sql.RecognizeFacesCompanion(
label: sql.Value(faces.results[u]!.label),
),
where: (sql.$RecognizeFacesTable t) =>
t.account.equals(dbAccount.rowId) &
t.label.equals(faces.results[u]!.label),
);
}
for (final i in faces.inserts) {
batch.insert(
db.recognizeFaces,
SqliteRecognizeFaceConverter.toSql(dbAccount, faces.results[i]!),
mode: sql.InsertMode.insertOrIgnore,
);
}
});
// update each item
for (final f in faces.results.values) {
try {
await _syncDbForFaceItem(db, dbAccount, f, items[f]!);
} catch (e, stackTrace) {
_log.shout("[call] Failed to update db for face: $f", e, stackTrace);
}
}
});
return true;
}
Future<_FaceResult?> _getFaceResults(Account account) async {
int faceSorter(RecognizeFace a, RecognizeFace b) =>
a.label.compareTo(b.label);
late final List<RecognizeFace> remote;
try {
remote = (await ListRecognizeFace(_c.withRemoteRepo())(account).last)
..sort(faceSorter);
} catch (e) {
if (e is ApiException && e.response.statusCode == 404) {
// recognize app probably not installed, ignore
_log.info("[_getFaceResults] Recognize app not installed");
return null;
}
rethrow;
}
final cache = (await ListRecognizeFace(_c.withLocalRepo())(account).last)
..sort(faceSorter);
final diff = list_util.diffWith(cache, remote, faceSorter);
final inserts = diff.onlyInB;
_log.info("[_getFaceResults] New face: ${inserts.toReadableString()}");
final deletes = diff.onlyInA;
_log.info("[_getFaceResults] Removed face: ${deletes.toReadableString()}");
final updates = remote.where((r) {
final c = cache.firstWhereOrNull((c) => c.label == r.label);
return c != null && c != r;
}).toList();
_log.info("[_getFaceResults] Updated face: ${updates.toReadableString()}");
return _FaceResult(
results: remote.map((e) => MapEntry(e.label, e)).toMap(),
inserts: inserts.map((e) => e.label).toList(),
updates: updates.map((e) => e.label).toList(),
deletes: deletes.map((e) => e.label).toList(),
);
}
Future<Map<RecognizeFace, _FaceItemResult>> _getFaceItemResults(
Account account, List<RecognizeFace> faces) async {
Object? firstError;
StackTrace? firstStackTrace;
final remote = await ListMultipleRecognizeFaceItem(_c.withRemoteRepo())(
account,
faces,
onError: (f, e, stackTrace) {
_log.severe(
"[_getFaceItemResults] Failed while listing remote face: $f",
e,
stackTrace,
);
if (firstError == null) {
firstError = e;
firstStackTrace = stackTrace;
}
},
).last;
if (firstError != null) {
Error.throwWithStackTrace(
firstError!, firstStackTrace ?? StackTrace.current);
}
final cache = await ListMultipleRecognizeFaceItem(_c.withLocalRepo())(
account,
faces,
onError: (f, e, stackTrace) {
_log.severe("[_getFaceItemResults] Failed while listing cache face: $f",
e, stackTrace);
},
).last;
int itemSorter(RecognizeFaceItem a, RecognizeFaceItem b) =>
a.fileId.compareTo(b.fileId);
final results = <RecognizeFace, _FaceItemResult>{};
for (final f in faces) {
final thisCache = (cache[f] ?? [])..sort(itemSorter);
final thisRemote = (remote[f] ?? [])..sort(itemSorter);
final diff = list_util.diffWith<RecognizeFaceItem>(
thisCache, thisRemote, itemSorter);
final inserts = diff.onlyInB;
_log.info(
"[_getFaceItemResults] New item: ${inserts.toReadableString()}");
final deletes = diff.onlyInA;
_log.info(
"[_getFaceItemResults] Removed item: ${deletes.toReadableString()}");
final updates = thisRemote.where((r) {
final c = thisCache.firstWhereOrNull((c) => c.fileId == r.fileId);
return c != null && c != r;
}).toList();
_log.info(
"[_getFaceItemResults] Updated item: ${updates.toReadableString()}");
results[f] = _FaceItemResult(
results: thisRemote.map((e) => MapEntry(e.fileId, e)).toMap(),
inserts: inserts.map((e) => e.fileId).toList(),
updates: updates.map((e) => e.fileId).toList(),
deletes: deletes.map((e) => e.fileId).toList(),
);
}
return results;
}
// Future<_FaceItemResult?> _getFaceItemResults(
// Account account, RecognizeFace face) async {
// late final List<RecognizeFaceItem> remote;
// try {
// remote =
// await ListRecognizeFaceItem(_c.withRemoteRepo())(account, face).last;
// } catch (e) {
// if (e is ApiException && e.response.statusCode == 404) {
// // recognize app probably not installed, ignore
// _log.info("[_getFaceItemResults] Recognize app not installed");
// return null;
// }
// rethrow;
// }
// final cache =
// await ListRecognizeFaceItem(_c.withLocalRepo())(account, face).last;
// int itemSorter(RecognizeFaceItem a, RecognizeFaceItem b) =>
// a.fileId.compareTo(b.fileId);
// final diff = list_util.diffWith(cache, remote, itemSorter);
// final inserts = diff.onlyInB;
// _log.info("[_getFaceItemResults] New face: ${inserts.toReadableString()}");
// final deletes = diff.onlyInA;
// _log.info(
// "[_getFaceItemResults] Removed face: ${deletes.toReadableString()}");
// final updates = remote.where((r) {
// final c = cache.firstWhereOrNull((c) => c.fileId == r.fileId);
// return c != null && c != r;
// }).toList();
// _log.info(
// "[_getFaceItemResults] Updated face: ${updates.toReadableString()}");
// return _FaceItemResult(
// results: remote.map((e) => MapEntry(e.fileId, e)).toMap(),
// inserts: inserts.map((e) => e.fileId).toList(),
// updates: updates.map((e) => e.fileId).toList(),
// deletes: deletes.map((e) => e.fileId).toList(),
// );
// }
Future<void> _syncDbForFaceItem(sql.SqliteDb db, sql.Account dbAccount,
RecognizeFace face, _FaceItemResult item) async {
await db.transaction(() async {
final dbFace = await db.recognizeFaceByLabel(
account: sql.ByAccount.sql(dbAccount),
label: face.label,
);
await db.batch((batch) {
for (final d in item.deletes) {
batch.deleteWhere(
db.recognizeFaceItems,
(sql.$RecognizeFaceItemsTable t) =>
t.parent.equals(dbFace.rowId) & t.fileId.equals(d),
);
}
for (final u in item.updates) {
batch.update(
db.recognizeFaceItems,
SqliteRecognizeFaceItemConverter.toSql(dbFace, item.results[u]!),
where: (sql.$RecognizeFaceItemsTable t) =>
t.parent.equals(dbFace.rowId) & t.fileId.equals(u),
);
}
for (final i in item.inserts) {
batch.insert(
db.recognizeFaceItems,
SqliteRecognizeFaceItemConverter.toSql(dbFace, item.results[i]!),
mode: sql.InsertMode.insertOrIgnore,
);
}
});
});
}
final DiContainer _c;
}
class _FaceResult {
const _FaceResult({
required this.results,
required this.inserts,
required this.updates,
required this.deletes,
});
final Map<String, RecognizeFace> results;
final List<String> inserts;
final List<String> updates;
final List<String> deletes;
}
class _FaceItemResult {
const _FaceItemResult({
required this.results,
required this.inserts,
required this.updates,
required this.deletes,
});
final Map<int, RecognizeFaceItem> results;
final List<int> inserts;
final List<int> updates;
final List<int> deletes;
}

View file

@ -1,14 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'person_parser.dart';
part of 'sync_recognize_face.dart';
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$PersonParserNpLog on PersonParser {
extension _$SyncRecognizeFaceNpLog on SyncRecognizeFace {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("src.entity.person_parser.PersonParser");
static final log =
Logger("use_case.recognize_face.sync_recognize_face.SyncRecognizeFace");
}

View file

@ -4,13 +4,15 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter_isolate/flutter_isolate.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_init.dart' as app_init;
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/use_case/person/sync_person.dart';
import 'package:nc_photos/use_case/sync_favorite.dart';
import 'package:nc_photos/use_case/sync_person.dart';
import 'package:nc_photos/use_case/sync_tag.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
@ -20,36 +22,39 @@ part 'startup_sync.g.dart';
/// Sync various properties with server during startup
@npLog
class StartupSync {
StartupSync(this._c)
: assert(require(_c)),
assert(SyncFavorite.require(_c)),
assert(SyncTag.require(_c));
StartupSync(this._c) : assert(require(_c));
static bool require(DiContainer c) => true;
static bool require(DiContainer c) =>
SyncFavorite.require(c) && SyncTag.require(c);
/// Sync in a background isolate
static Future<SyncResult> runInIsolate(Account account) async {
if (platform_k.isWeb) {
// not supported on web
final c = KiwiContainer().resolve<DiContainer>();
return await StartupSync(c)(account);
} else {
// we can't use regular isolate here because self-signed cert support
// requires native plugins
final resultJson =
await flutterCompute(_isolateMain, _IsolateMessage(account).toJson());
final result = SyncResult.fromJson(resultJson);
// events fired in background isolate won't be noticed by the main isolate,
// so we fire them again here
_broadcastResult(account, result);
return result;
}
static Future<SyncResult> runInIsolate(
Account account, PersonProvider personProvider) async {
return _mutex.protect(() async {
if (platform_k.isWeb) {
// not supported on web
final c = KiwiContainer().resolve<DiContainer>();
return await StartupSync(c)(account, personProvider);
} else {
// we can't use regular isolate here because self-signed cert support
// requires native plugins
final resultJson = await flutterCompute(
_isolateMain, _IsolateMessage(account, personProvider).toJson());
final result = SyncResult.fromJson(resultJson);
// events fired in background isolate won't be noticed by the main isolate,
// so we fire them again here
_broadcastResult(account, result);
return result;
}
});
}
Future<SyncResult> call(Account account) async {
Future<SyncResult> call(
Account account, PersonProvider personProvider) async {
_log.info("[_run] Begin sync");
final stopwatch = Stopwatch()..start();
late final int syncFavoriteCount;
late final bool isSyncPersonUpdated;
try {
syncFavoriteCount = await SyncFavorite(_c)(account);
} catch (e, stackTrace) {
@ -62,12 +67,15 @@ class StartupSync {
_log.shout("[_run] Failed while SyncTag", e, stackTrace);
}
try {
await SyncPerson(_c)(account);
isSyncPersonUpdated = await SyncPerson(_c)(account, personProvider);
} catch (e, stackTrace) {
_log.shout("[_run] Failed while SyncPerson", e, stackTrace);
}
_log.info("[_run] Elapsed time: ${stopwatch.elapsedMilliseconds}ms");
return SyncResult(syncFavoriteCount);
return SyncResult(
syncFavoriteCount: syncFavoriteCount,
isSyncPersonUpdated: isSyncPersonUpdated,
);
}
static void _broadcastResult(Account account, SyncResult result) {
@ -78,37 +86,48 @@ class StartupSync {
}
final DiContainer _c;
static final _mutex = Mutex();
}
class SyncResult {
const SyncResult(this.syncFavoriteCount);
const SyncResult({
required this.syncFavoriteCount,
required this.isSyncPersonUpdated,
});
factory SyncResult.fromJson(JsonObj json) => SyncResult(
json["syncFavoriteCount"],
syncFavoriteCount: json["syncFavoriteCount"],
isSyncPersonUpdated: json["isSyncPersonUpdated"],
);
JsonObj toJson() => {
"syncFavoriteCount": syncFavoriteCount,
"isSyncPersonUpdated": isSyncPersonUpdated,
};
final int syncFavoriteCount;
final bool isSyncPersonUpdated;
}
class _IsolateMessage {
const _IsolateMessage(this.account);
const _IsolateMessage(this.account, this.personProvider);
factory _IsolateMessage.fromJson(JsonObj json) => _IsolateMessage(
Account.fromJson(
json["account"].cast<String, dynamic>(),
upgraderV1: const AccountUpgraderV1(),
)!,
PersonProvider.fromValue(json["personProvider"]),
);
JsonObj toJson() => {
JsonObj toJson() => <String, dynamic>{
"account": account.toJson(),
"personProvider": personProvider.index,
};
final Account account;
final PersonProvider personProvider;
}
@pragma("vm:entry-point")
@ -117,6 +136,6 @@ Future<JsonObj> _isolateMain(JsonObj messageJson) async {
await app_init.init(app_init.InitIsolateType.flutterIsolate);
final c = KiwiContainer().resolve<DiContainer>();
final result = await StartupSync(c)(message.account);
final result = await StartupSync(c)(message.account, message.personProvider);
return result.toJson();
}

View file

@ -388,7 +388,7 @@ class _EditAppBar extends StatelessWidget {
final result = await showDialog<CollectionItemSort>(
context: context,
builder: (context) => FancyOptionPicker(
title: L10n.global().sortOptionDialogTitle,
title: Text(L10n.global().sortOptionDialogTitle),
items: [
_SortDialogParams(
L10n.global().sortOptionTimeDescendingLabel,

View file

@ -1,25 +1,19 @@
import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/network_thumbnail.dart';
class CollectionListSmall extends StatelessWidget {
const CollectionListSmall({
Key? key,
required this.account,
super.key,
required this.label,
required this.coverUrl,
required this.fallbackBuilder,
required this.child,
this.onTap,
}) : super(key: key);
});
@override
build(BuildContext context) {
Widget build(BuildContext context) {
Widget content = Stack(
children: [
SizedBox.expand(
child: _buildCoverImage(context),
),
SizedBox.expand(child: child),
Positioned(
bottom: 0,
left: 0,
@ -82,27 +76,7 @@ class CollectionListSmall extends StatelessWidget {
);
}
Widget _buildCoverImage(BuildContext context) {
Widget buildPlaceholder() => Padding(
padding: const EdgeInsets.all(8),
child: fallbackBuilder(context),
);
try {
return NetworkRectThumbnail(
account: account,
imageUrl: coverUrl,
errorBuilder: (_) => buildPlaceholder(),
);
} catch (_) {
return FittedBox(
child: buildPlaceholder(),
);
}
}
final Account account;
final String label;
final String coverUrl;
final Widget Function(BuildContext context) fallbackBuilder;
final Widget? child;
final VoidCallback? onTap;
}

View file

@ -30,7 +30,7 @@ class FancyOptionPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: title != null ? Text(title!) : null,
title: title,
children: items
.map((e) => SimpleDialogOption(
child: FancyOptionPickerItemView(
@ -46,7 +46,7 @@ class FancyOptionPicker extends StatelessWidget {
);
}
final String? title;
final Widget? title;
final List<FancyOptionPickerItem> items;
}

View file

@ -303,7 +303,7 @@ class _AppBar extends StatelessWidget {
final result = await showDialog<collection_util.CollectionSort>(
context: context,
builder: (context) => FancyOptionPicker(
title: L10n.global().sortOptionDialogTitle,
title: Text(L10n.global().sortOptionDialogTitle),
items: [
FancyOptionPickerItem(
label: L10n.global().sortOptionTimeDescendingLabel,

View file

@ -16,6 +16,7 @@ import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/bloc/progress.dart';
import 'package:nc_photos/bloc/scan_account_dir.dart';
import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/collection.dart';
@ -36,7 +37,6 @@ import 'package:nc_photos/share_handler.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/throttler.dart';
import 'package:nc_photos/use_case/startup_sync.dart';
import 'package:nc_photos/widget/builder/photo_list_item_builder.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/handler/add_selection_to_collection_handler.dart';
@ -556,13 +556,6 @@ class _HomePhotosState extends State<HomePhotos>
}
}
Future<void> _startupSync() async {
if (!_hasResyncedFavorites.value) {
_hasResyncedFavorites.value = true;
unawaited(StartupSync.runInIsolate(widget.account));
}
}
/// Transform a File list to grid items
void _transformItems(
List<FileDescriptor> files, {
@ -603,7 +596,8 @@ class _HomePhotosState extends State<HomePhotos>
if (isPostSuccess) {
_isScrollbarVisible = true;
_startupSync();
context.read<AccountController>().syncController.requestSync(
widget.account, _accountPrefController.personProvider.value);
_tryStartMetadataTask();
}
});
@ -712,23 +706,10 @@ class _HomePhotosState extends State<HomePhotos>
}
}
Primitive<bool> get _hasResyncedFavorites {
final name = bloc_util.getInstNameForRootAwareAccount(
"HomePhotosState._hasResyncedFavorites", widget.account);
try {
_log.fine("[_hasResyncedFavorites] Resolving for '$name'");
return KiwiContainer().resolve<Primitive<bool>>(name);
} catch (_) {
_log.info(
"[_hasResyncedFavorites] New instance for account: ${widget.account}");
final obj = Primitive(false);
KiwiContainer().registerInstance<Primitive<bool>>(obj, name: name);
return obj;
}
}
late final _bloc = ScanAccountDirBloc.of(widget.account);
late final _queryProgressBloc = ProgressBloc();
late final _accountPrefController =
context.read<AccountController>().accountPrefController;
var _backingFiles = <FileDescriptor>[];
var _smartCollections = <Collection>[];

View file

@ -80,6 +80,7 @@ class _HomeSearchSuggestionState extends State<HomeSearchSuggestion>
widget.account,
context.read<AccountController>().collectionsController,
context.read<AccountController>().serverController,
context.read<AccountController>().accountPrefController,
));
if (_bloc.state is! HomeSearchSuggestionBlocInit) {
// process the current state

View file

@ -178,7 +178,7 @@ class _WrappedAppState extends State<_WrappedApp>
),
CollectionPicker.routeName: CollectionPicker.buildRoute,
LanguageSettings.routeName: LanguageSettings.buildRoute,
AccountSettings.routeName: AccountSettings.buildRoute,
PeopleBrowser.routeName: PeopleBrowser.buildRoute,
};
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
@ -206,11 +206,11 @@ class _WrappedAppState extends State<_WrappedApp>
route ??= _handleEnhancementSettingsRoute(settings);
route ??= _handleImageEditorRoute(settings);
route ??= _handleChangelogRoute(settings);
route ??= _handlePeopleBrowserRoute(settings);
route ??= _handlePlacesBrowserRoute(settings);
route ??= _handleResultViewerRoute(settings);
route ??= _handleImageEnhancerRoute(settings);
route ??= _handleCollectionBrowserRoute(settings);
route ??= _handleAccountSettingsRoute(settings);
return route;
}
@ -517,19 +517,6 @@ class _WrappedAppState extends State<_WrappedApp>
return null;
}
Route<dynamic>? _handlePeopleBrowserRoute(RouteSettings settings) {
try {
if (settings.name == PeopleBrowser.routeName &&
settings.arguments != null) {
final args = settings.arguments as PeopleBrowserArguments;
return PeopleBrowser.buildRoute(args);
}
} catch (e) {
_log.severe("[_handlePeopleBrowserRoute] Failed while handling route", e);
}
return null;
}
Route<dynamic>? _handlePlacesBrowserRoute(RouteSettings settings) {
try {
if (settings.name == PlacesBrowser.routeName &&
@ -589,6 +576,19 @@ class _WrappedAppState extends State<_WrappedApp>
return null;
}
Route<dynamic>? _handleAccountSettingsRoute(RouteSettings settings) {
try {
if (settings.name == AccountSettings.routeName) {
final args = settings.arguments as AccountSettingsArguments?;
return AccountSettings.buildRoute(args);
}
} catch (e) {
_log.severe(
"[_handleAccountSettingsRoute] Failed while handling route", e);
}
return null;
}
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final _navigatorKey = GlobalKey<NavigatorState>();

View file

@ -1,12 +1,14 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/np_api_util.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
/// A square thumbnail widget for a file
class NetworkRectThumbnail extends StatelessWidget {
@ -16,6 +18,7 @@ class NetworkRectThumbnail extends StatelessWidget {
required this.imageUrl,
this.dimension,
required this.errorBuilder,
this.onSize,
});
static String imageUrlForFile(Account account, FileDescriptor file) =>
@ -41,10 +44,9 @@ class NetworkRectThumbnail extends StatelessWidget {
final child = FittedBox(
clipBehavior: Clip.hardEdge,
fit: BoxFit.cover,
child: CachedNetworkImage(
child: mod.CachedNetworkImage(
cacheManager: ThumbnailCacheManager.inst,
imageUrl: imageUrl,
// imageUrl: "",
httpHeaders: {
"Authorization": AuthUtil.fromAccount(account).toHeaderValue(),
},
@ -55,6 +57,12 @@ class NetworkRectThumbnail extends StatelessWidget {
dimension: dimension,
child: errorBuilder(context),
),
imageBuilder: (_, child, __) {
return _SizeObserver(
onSize: onSize,
child: child,
);
},
),
);
if (dimension != null) {
@ -74,4 +82,48 @@ class NetworkRectThumbnail extends StatelessWidget {
final String imageUrl;
final double? dimension;
final Widget Function(BuildContext context) errorBuilder;
final ValueChanged<Size>? onSize;
}
class _SizeObserver extends SingleChildRenderObjectWidget {
const _SizeObserver({
super.child,
this.onSize,
});
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderSizeChangedWithCallback(
onLayoutChangedCallback: () {
if (onSize != null) {
final size = context.findRenderObject()?.as<RenderBox>()?.size;
if (size != null) {
onSize?.call(size);
}
}
},
);
}
final ValueChanged<Size>? onSize;
}
class _RenderSizeChangedWithCallback extends RenderProxyBox {
_RenderSizeChangedWithCallback({
RenderBox? child,
required this.onLayoutChangedCallback,
}) : super(child);
@override
void performLayout() {
super.performLayout();
if (size != _oldSize) {
onLayoutChangedCallback();
}
_oldSize = size;
}
final VoidCallback onLayoutChangedCallback;
Size? _oldSize;
}

View file

@ -1,227 +1,205 @@
import 'package:collection/collection.dart';
import 'package:copy_with/copy_with.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/list_person.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/controller/persons_controller.dart';
import 'package:nc_photos/entity/collection/builder.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/collection_list_item.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart';
import 'package:nc_photos/widget/person_thumbnail.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
part 'people_browser.g.dart';
part 'people_browser/bloc.dart';
part 'people_browser/state_event.dart';
part 'people_browser/type.dart';
class PeopleBrowserArguments {
const PeopleBrowserArguments(this.account);
final Account account;
}
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
/// Show a list of all people associated with this account
class PeopleBrowser extends StatefulWidget {
class PeopleBrowser extends StatelessWidget {
static const routeName = "/people-browser";
static Route buildRoute(PeopleBrowserArguments args) => MaterialPageRoute(
builder: (context) => PeopleBrowser.fromArgs(args),
static Route buildRoute() => MaterialPageRoute(
builder: (_) => const PeopleBrowser(),
);
const PeopleBrowser({
Key? key,
required this.account,
}) : super(key: key);
PeopleBrowser.fromArgs(PeopleBrowserArguments args, {Key? key})
: this(
key: key,
account: args.account,
);
const PeopleBrowser({super.key});
@override
createState() => _PeopleBrowserState();
Widget build(BuildContext context) {
final accountController = context.read<AccountController>();
return BlocProvider(
create: (_) => _Bloc(
account: accountController.account,
personsController: accountController.personsController,
),
child: const _WrappedPeopleBrowser(),
);
}
}
final Account account;
class _WrappedPeopleBrowser extends StatefulWidget {
const _WrappedPeopleBrowser();
@override
State<StatefulWidget> createState() => _WrappedPeopleBrowserState();
}
@npLog
class _PeopleBrowserState extends State<PeopleBrowser> {
class _WrappedPeopleBrowserState extends State<_WrappedPeopleBrowser>
with RouteAware, PageVisibilityMixin {
@override
initState() {
void initState() {
super.initState();
_initBloc();
_bloc.add(const _LoadPersons());
}
@override
build(BuildContext context) {
return Scaffold(
body: BlocListener<ListPersonBloc, ListPersonBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ListPersonBloc, ListPersonBlocState>(
bloc: _bloc,
builder: (context, state) => _buildContent(context, state),
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListener(
listenWhen: (previous, current) =>
previous.persons != current.persons,
listener: (context, state) {
_bloc.add(_TransformItems(state.persons));
},
),
_BlocListener(
listenWhen: (previous, current) => previous.error != current.error,
listener: (context, state) {
if (state.error != null && isPageVisible()) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(state.error!.error)),
duration: k.snackBarDurationNormal,
));
}
},
),
],
child: Scaffold(
body: Stack(
children: [
CustomScrollView(
slivers: [
const _AppBar(),
SliverToBoxAdapter(
child: _BlocBuilder(
buildWhen: (previous, current) =>
previous.isLoading != current.isLoading,
builder: (context, state) => state.isLoading
? const LinearProgressIndicator()
: const SizedBox(height: 4),
),
),
_ContentList(
onTap: (_, item) {
Navigator.pushNamed(
context,
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(
CollectionBuilder.byPerson(_bloc.account, item.person),
),
);
},
),
],
),
],
),
),
);
}
void _initBloc() {
if (_bloc.state is ListPersonBlocInit) {
_log.info("[_initBloc] Initialize bloc");
} else {
// process the current state
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_onStateChange(context, _bloc.state);
});
}
});
}
_reqQuery();
}
late final _bloc = context.read<_Bloc>();
}
Widget _buildContent(BuildContext context, ListPersonBlocState state) {
return Stack(
children: [
CustomScrollView(
slivers: [
_buildAppBar(context),
if (state is ListPersonBlocLoading)
const SliverToBoxAdapter(
child: Align(
alignment: Alignment.center,
child: LinearProgressIndicator(),
),
),
SliverStaggeredGrid.extentBuilder(
maxCrossAxisExtent: 160,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
itemCount: _items.length,
itemBuilder: _buildItem,
staggeredTileBuilder: (_) => const StaggeredTile.count(1, 1),
),
],
),
],
);
}
class _AppBar extends StatelessWidget {
const _AppBar();
Widget _buildAppBar(BuildContext context) {
@override
Widget build(BuildContext context) {
return SliverAppBar(
title: Text(L10n.global().collectionPeopleLabel),
floating: true,
);
}
}
Widget _buildItem(BuildContext context, int index) {
final item = _items[index];
return item.buildWidget(context);
}
class _ContentList extends StatelessWidget {
const _ContentList({
this.onTap,
});
void _onStateChange(BuildContext context, ListPersonBlocState state) {
if (state is ListPersonBlocInit) {
_items = [];
} else if (state is ListPersonBlocSuccess ||
state is ListPersonBlocLoading) {
_transformItems(state.items);
} else if (state is ListPersonBlocFailure) {
_transformItems(state.items);
try {
final e = state.exception as ApiException;
if (e.response.statusCode == 404) {
// face recognition app probably not installed, ignore
return;
}
} catch (_) {}
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(state.exception)),
duration: k.snackBarDurationNormal,
));
}
}
void _onItemTap(Person person) {
Navigator.pushNamed(
context,
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(
CollectionBuilder.byPerson(widget.account, person),
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.transformedItems != current.transformedItems,
builder: (context, state) => SliverStaggeredGrid.extentBuilder(
maxCrossAxisExtent: 160,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
itemCount: state.transformedItems.length,
itemBuilder: (context, index) {
final item = state.transformedItems[index];
return _ItemView(
account: context.read<_Bloc>().account,
item: item,
onTap: onTap == null
? null
: () {
onTap!.call(index, item);
},
);
},
staggeredTileBuilder: (_) => const StaggeredTile.count(1, 1),
),
);
}
void _transformItems(List<Person> items) {
_items = items
.sorted((a, b) {
final countCompare = b.count.compareTo(a.count);
if (countCompare == 0) {
return a.name.compareTo(b.name);
} else {
return countCompare;
}
})
.map((e) => _PersonListItem(
account: widget.account,
name: e.name,
faceUrl: api_util.getFacePreviewUrl(widget.account, e.thumbFaceId,
size: k.faceThumbSize),
onTap: () => _onItemTap(e),
))
.toList();
}
void _reqQuery() {
_bloc.add(ListPersonBlocQuery(widget.account));
}
late final _bloc = ListPersonBloc(KiwiContainer().resolve<DiContainer>());
var _items = <_ListItem>[];
final Function(int index, _Item item)? onTap;
}
abstract class _ListItem {
_ListItem({
class _ItemView extends StatelessWidget {
const _ItemView({
required this.account,
required this.item,
this.onTap,
});
Widget buildWidget(BuildContext context);
final VoidCallback? onTap;
}
class _PersonListItem extends _ListItem {
_PersonListItem({
required this.account,
required this.name,
required this.faceUrl,
VoidCallback? onTap,
}) : super(onTap: onTap);
@override
buildWidget(BuildContext context) => CollectionListSmall(
account: account,
label: name,
coverUrl: faceUrl,
fallbackBuilder: (context) => Icon(
Icons.person,
color: Theme.of(context).listPlaceholderForegroundColor,
Widget build(BuildContext context) {
return CollectionListSmall(
label: item.name,
onTap: onTap,
child: LayoutBuilder(
builder: (context, constraints) => PersonThumbnail(
account: account,
coverUrl: item.coverUrl,
person: item.person,
dimension: constraints.maxWidth,
),
onTap: onTap,
);
),
);
}
final Account account;
final String name;
final String faceUrl;
final _Item item;
final VoidCallback? onTap;
}

View file

@ -2,13 +2,95 @@
part of 'people_browser.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call(
{List<Person>? persons,
bool? isLoading,
List<_Item>? transformedItems,
ExceptionEvent? error});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call(
{dynamic persons,
dynamic isLoading,
dynamic transformedItems,
dynamic error = copyWithNull}) {
return _State(
persons: persons as List<Person>? ?? that.persons,
isLoading: isLoading as bool? ?? that.isLoading,
transformedItems:
transformedItems as List<_Item>? ?? that.transformedItems,
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
}
final _State that;
}
extension $_StateCopyWith on _State {
$_StateCopyWithWorker get copyWith => _$copyWith;
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
}
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$_PeopleBrowserStateNpLog on _PeopleBrowserState {
extension _$_WrappedPeopleBrowserStateNpLog on _WrappedPeopleBrowserState {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.people_browser._PeopleBrowserState");
static final log = Logger("widget.people_browser._WrappedPeopleBrowserState");
}
extension _$_BlocNpLog on _Bloc {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.people_browser._Bloc");
}
extension _$_ItemNpLog on _Item {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.people_browser._Item");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {persons: [length: ${persons.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], error: $error}";
}
}
extension _$_LoadPersonsToString on _LoadPersons {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_LoadPersons {}";
}
}
extension _$_TransformItemsToString on _TransformItems {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_TransformItems {persons: [length: ${persons.length}]}";
}
}

View file

@ -0,0 +1,56 @@
part of '../people_browser.dart';
@npLog
class _Bloc extends Bloc<_Event, _State> implements BlocLogger {
_Bloc({
required this.account,
required this.personsController,
}) : super(_State.init()) {
on<_LoadPersons>(_onLoad);
on<_TransformItems>(_onTransformItems);
}
@override
String get tag => _log.fullName;
@override
bool Function(dynamic, dynamic)? get shouldLog => null;
Future<void> _onLoad(_LoadPersons ev, Emitter<_State> emit) {
_log.info(ev);
return emit.forEach<PersonStreamEvent>(
personsController.stream,
onData: (data) => state.copyWith(
persons: data.data,
isLoading: data.hasNext,
),
onError: (e, stackTrace) {
_log.severe("[_onLoad] Uncaught exception", e, stackTrace);
return state.copyWith(
isLoading: false,
error: ExceptionEvent(e, stackTrace),
);
},
);
}
Future<void> _onTransformItems(
_TransformItems ev, Emitter<_State> emit) async {
_log.info("[_onTransformItems] $ev");
final transformed =
ev.persons.sorted(_sorter).map((p) => _Item(p)).toList();
emit(state.copyWith(transformedItems: transformed));
}
final Account account;
final PersonsController personsController;
}
int _sorter(Person a, Person b) {
final countCompare = (b.count ?? 0).compareTo(a.count ?? 0);
if (countCompare == 0) {
return a.name.compareTo(b.name);
} else {
return countCompare;
}
}

View file

@ -0,0 +1,49 @@
part of '../people_browser.dart';
@genCopyWith
@toString
class _State {
const _State({
required this.persons,
required this.isLoading,
required this.transformedItems,
this.error,
});
factory _State.init() => const _State(
persons: [],
isLoading: false,
transformedItems: [],
);
@override
String toString() => _$toString();
final List<Person> persons;
final bool isLoading;
final List<_Item> transformedItems;
final ExceptionEvent? error;
}
abstract class _Event {}
/// Load the list of [Person]s belonging to this account
@toString
class _LoadPersons implements _Event {
const _LoadPersons();
@override
String toString() => _$toString();
}
/// Transform the [Person] list (e.g., filtering, sorting, etc)
@toString
class _TransformItems implements _Event {
const _TransformItems(this.persons);
@override
String toString() => _$toString();
final List<Person> persons;
}

View file

@ -0,0 +1,24 @@
part of '../people_browser.dart';
@npLog
class _Item {
_Item(this.person) {
try {
_coverUrl = person.getCoverUrl(
k.photoLargeSize,
k.photoLargeSize,
isKeepAspectRatio: true,
);
} catch (e, stackTrace) {
_log.warning("[_Item] Failed while getCoverUrl", e, stackTrace);
}
}
String get name => person.name;
String? get coverUrl => _coverUrl;
final Person person;
String? _coverUrl;
}

View file

@ -0,0 +1,97 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/network_thumbnail.dart';
class PersonThumbnail extends StatefulWidget {
const PersonThumbnail({
super.key,
required this.dimension,
required this.account,
required this.coverUrl,
required this.person,
});
@override
State<StatefulWidget> createState() => _PersonThumbnailState();
final double dimension;
final Account account;
final String? coverUrl;
final Person person;
}
class _PersonThumbnailState extends State<PersonThumbnail> {
@override
Widget build(BuildContext context) {
Widget content;
try {
var m = Matrix4.identity();
if (_layoutSize != null) {
final ratio = widget.dimension /
math.min(_layoutSize!.width, _layoutSize!.height);
final mm = widget.person.getCoverTransform(
widget.dimension.toInt(),
(_layoutSize!.width * ratio).toInt(),
(_layoutSize!.height * ratio).toInt(),
);
if (mm != null) {
m = mm;
}
}
content = Transform(
transform: m,
child: NetworkRectThumbnail(
account: widget.account,
imageUrl: widget.coverUrl!,
errorBuilder: (_) => const _Placeholder(),
onSize: (size) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_layoutSize = size;
});
});
},
),
);
if (_layoutSize == null) {
content = Opacity(opacity: 0, child: content);
}
} catch (_) {
content = const FittedBox(
child: _Placeholder(),
);
}
return ClipRRect(
child: SizedBox.square(
dimension: widget.dimension,
child: Container(
color: Theme.of(context).listPlaceholderBackgroundColor,
constraints: const BoxConstraints.expand(),
child: content,
),
),
);
}
Size? _layoutSize;
}
class _Placeholder extends StatelessWidget {
const _Placeholder();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.person,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
}
}

View file

@ -243,14 +243,12 @@ class _PlaceItem {
});
Widget buildWidget(BuildContext context) => CollectionListSmall(
account: account,
label: place,
coverUrl: thumbUrl,
fallbackBuilder: (context) => Icon(
Icons.location_on,
color: Theme.of(context).listPlaceholderForegroundColor,
),
onTap: onTap,
child: _PlaceThumbnail(
account: account,
coverUrl: thumbUrl,
),
);
final Account account;
@ -337,3 +335,43 @@ class _CountryItemView extends StatelessWidget {
final String text;
final VoidCallback? onTap;
}
class _PlaceThumbnail extends StatelessWidget {
const _PlaceThumbnail({
required this.account,
required this.coverUrl,
});
@override
Widget build(BuildContext context) {
try {
return NetworkRectThumbnail(
account: account,
imageUrl: coverUrl!,
errorBuilder: (_) => const _Placeholder(),
);
} catch (_) {
return const FittedBox(
child: _Placeholder(),
);
}
}
final Account account;
final String? coverUrl;
}
class _Placeholder extends StatelessWidget {
const _Placeholder();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.location_on,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
}
}

View file

@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/search_landing.dart';
import 'package:nc_photos/controller/account_controller.dart';
@ -22,10 +21,14 @@ import 'package:nc_photos/use_case/list_location_group.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/network_thumbnail.dart';
import 'package:nc_photos/widget/people_browser.dart';
import 'package:nc_photos/widget/person_thumbnail.dart';
import 'package:nc_photos/widget/places_browser.dart';
import 'package:nc_photos/widget/settings/account_settings.dart';
import 'package:np_codegen/np_codegen.dart';
part 'search_landing.g.dart';
part 'search_landing/type.dart';
part 'search_landing/view.dart';
class SearchLanding extends StatefulWidget {
const SearchLanding({
@ -83,10 +86,11 @@ class _SearchLandingState extends State<SearchLanding> {
return Column(
children: [
if (context
.read<AccountController>()
.accountPrefController
.isEnableFaceRecognitionApp
.value)
.read<AccountController>()
.accountPrefController
.personProvider
.value !=
PersonProvider.none)
..._buildPeopleSection(context, state),
..._buildLocationSection(context, state),
ListTile(
@ -123,17 +127,33 @@ class _SearchLandingState extends State<SearchLanding> {
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(L10n.global().collectionPeopleLabel),
trailing: isNoResult
? IconButton(
onPressed: () {
launch(help_util.peopleUrl);
},
tooltip: L10n.global().helpTooltip,
icon: const Icon(Icons.help_outline),
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
Navigator.of(context).pushNamed(
AccountSettings.routeName,
arguments: const AccountSettingsArguments(
highlight: AccountSettingsOption.personProvider,
),
);
},
tooltip: L10n.global().accountSettingsTooltip,
icon: const Icon(Icons.settings_outlined),
),
IconButton(
onPressed: () {
launch(help_util.peopleUrl);
},
tooltip: L10n.global().helpTooltip,
icon: const Icon(Icons.help_outline),
),
],
)
: TextButton(
onPressed: () {
Navigator.of(context).pushNamed(PeopleBrowser.routeName,
arguments: PeopleBrowserArguments(widget.account));
Navigator.of(context).pushNamed(PeopleBrowser.routeName);
},
child: Text(L10n.global().showAllButtonLabel),
),
@ -142,7 +162,7 @@ class _SearchLandingState extends State<SearchLanding> {
SizedBox(
height: 48,
child: Center(
child: Text(L10n.global().searchLandingPeopleListEmptyText),
child: Text(L10n.global().searchLandingPeopleListEmptyText2),
),
)
else
@ -255,7 +275,7 @@ class _SearchLandingState extends State<SearchLanding> {
void _transformPersons(List<Person> persons) {
_personItems = persons
.sorted((a, b) {
final countCompare = b.count.compareTo(a.count);
final countCompare = (b.count ?? 0).compareTo(a.count ?? 0);
if (countCompare == 0) {
return a.name.compareTo(b.name);
} else {
@ -265,9 +285,7 @@ class _SearchLandingState extends State<SearchLanding> {
.take(10)
.map((e) => _LandingPersonItem(
account: widget.account,
name: e.name,
faceUrl: api_util.getFacePreviewUrl(widget.account, e.thumbFaceId,
size: k.faceThumbSize),
person: e,
onTap: () => _onPersonItemTap(e),
))
.toList();
@ -295,101 +313,46 @@ class _SearchLandingState extends State<SearchLanding> {
}
void _reqQuery() {
_bloc.add(SearchLandingBlocQuery(widget.account));
_bloc.add(SearchLandingBlocQuery(widget.account, _accountPrefController));
}
late final _bloc = SearchLandingBloc(KiwiContainer().resolve<DiContainer>());
late final _accountPrefController =
context.read<AccountController>().accountPrefController;
var _personItems = <_LandingPersonItem>[];
var _locationItems = <_LandingLocationItem>[];
}
class _LandingPersonItem {
_LandingPersonItem({
required this.account,
required this.name,
required this.faceUrl,
this.onTap,
});
Widget buildWidget(BuildContext context) => _LandingItemWidget(
account: account,
label: name,
coverUrl: faceUrl,
onTap: onTap,
fallbackBuilder: (_) => Icon(
Icons.person,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
final Account account;
final String name;
final String faceUrl;
final VoidCallback? onTap;
}
class _LandingLocationItem {
const _LandingLocationItem({
required this.account,
required this.name,
required this.thumbUrl,
this.onTap,
});
Widget buildWidget(BuildContext context) => _LandingItemWidget(
account: account,
label: name,
coverUrl: thumbUrl,
onTap: onTap,
fallbackBuilder: (_) => Icon(
Icons.location_on,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
final Account account;
final String name;
final String thumbUrl;
final VoidCallback? onTap;
}
class _LandingItemWidget extends StatelessWidget {
const _LandingItemWidget({
Key? key,
class _LandingPersonWidget extends StatelessWidget {
const _LandingPersonWidget({
required this.account,
required this.person,
required this.label,
required this.coverUrl,
required this.fallbackBuilder,
this.onTap,
}) : super(key: key);
});
@override
build(BuildContext context) {
Widget build(BuildContext context) {
final content = Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Center(
child: AspectRatio(
aspectRatio: 1,
child: _buildCoverImage(context),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(72 / 2),
child: PersonThumbnail(
dimension: 72,
account: account,
person: person,
coverUrl: coverUrl,
),
),
),
const SizedBox(height: 8),
SizedBox(
width: 88,
child: Text(
label + "\n",
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Expanded(child: _Label(label: label)),
],
),
);
@ -403,37 +366,74 @@ class _LandingItemWidget extends StatelessWidget {
}
}
Widget _buildCoverImage(BuildContext context) {
Widget cover;
Widget buildPlaceholder() => Padding(
padding: const EdgeInsets.all(8),
child: fallbackBuilder(context),
);
try {
cover = NetworkRectThumbnail(
account: account,
imageUrl: coverUrl,
errorBuilder: (_) => buildPlaceholder(),
);
} catch (_) {
cover = FittedBox(
child: buildPlaceholder(),
);
}
final Account account;
final Person person;
final String label;
final String? coverUrl;
final VoidCallback? onTap;
}
return ClipRRect(
borderRadius: BorderRadius.circular(128),
child: Container(
color: Theme.of(context).listPlaceholderBackgroundColor,
constraints: const BoxConstraints.expand(),
child: cover,
class _LandingLocationWidget extends StatelessWidget {
const _LandingLocationWidget({
required this.account,
required this.label,
required this.coverUrl,
this.onTap,
});
@override
Widget build(BuildContext context) {
final content = Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: _LocationCoverImage(
dimension: 72,
account: account,
coverUrl: coverUrl,
),
),
const SizedBox(height: 8),
Expanded(child: _Label(label: label)),
],
),
);
if (onTap != null) {
return InkWell(
onTap: onTap,
child: content,
);
} else {
return content;
}
}
final Account account;
final String label;
final String coverUrl;
final Widget Function(BuildContext context) fallbackBuilder;
final String? coverUrl;
final VoidCallback? onTap;
}
class _Label extends StatelessWidget {
const _Label({
required this.label,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 88,
child: Text(
label + "\n",
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}
final String label;
}

View file

@ -0,0 +1,49 @@
part of '../search_landing.dart';
class _LandingPersonItem {
_LandingPersonItem({
required this.account,
required this.person,
this.onTap,
}) : name = person.name,
faceUrl = person.getCoverUrl(
k.photoLargeSize,
k.photoLargeSize,
isKeepAspectRatio: true,
);
Widget buildWidget(BuildContext context) => _LandingPersonWidget(
account: account,
person: person,
label: name,
coverUrl: faceUrl,
onTap: onTap,
);
final Account account;
final Person person;
final String name;
final String? faceUrl;
final VoidCallback? onTap;
}
class _LandingLocationItem {
const _LandingLocationItem({
required this.account,
required this.name,
required this.thumbUrl,
this.onTap,
});
Widget buildWidget(BuildContext context) => _LandingLocationWidget(
account: account,
label: name,
coverUrl: thumbUrl,
onTap: onTap,
);
final Account account;
final String name;
final String thumbUrl;
final VoidCallback? onTap;
}

View file

@ -0,0 +1,56 @@
part of '../search_landing.dart';
class _LocationCoverPlaceholder extends StatelessWidget {
const _LocationCoverPlaceholder();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.location_on,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
}
}
class _LocationCoverImage extends StatelessWidget {
const _LocationCoverImage({
required this.dimension,
required this.account,
required this.coverUrl,
});
@override
Widget build(BuildContext context) {
Widget cover;
try {
cover = NetworkRectThumbnail(
account: account,
imageUrl: coverUrl!,
errorBuilder: (_) => const _LocationCoverPlaceholder(),
);
} catch (_) {
cover = const FittedBox(
child: _LocationCoverPlaceholder(),
);
}
return SizedBox.square(
dimension: dimension,
child: ClipRRect(
borderRadius: BorderRadius.circular(dimension / 2),
child: Container(
color: Theme.of(context).listPlaceholderBackgroundColor,
constraints: const BoxConstraints.expand(),
child: cover,
),
),
);
}
final double dimension;
final Account account;
final String? coverUrl;
}

View file

@ -6,13 +6,13 @@ class _Bloc extends Bloc<_Event, _State> {
required DiContainer container,
required Account account,
required this.accountPrefController,
this.highlight,
}) : _c = container,
super(_State.init(
account: account,
label: accountPrefController.accountLabel.value,
shareFolder: accountPrefController.shareFolder.value,
isEnableFaceRecognitionApp:
accountPrefController.isEnableFaceRecognitionApp.value,
personProvider: accountPrefController.personProvider.value,
)) {
on<_SetLabel>(_onSetLabel);
on<_OnUpdateLabel>(_onOnUpdateLabel);
@ -39,11 +39,11 @@ class _Bloc extends Bloc<_Event, _State> {
},
));
on<_SetEnableFaceRecognitionApp>(_onSetEnableFaceRecognitionApp);
on<_OnUpdateEnableFaceRecognitionApp>(_onOnUpdateEnableFaceRecognitionApp);
_subscriptions.add(accountPrefController.isEnableFaceRecognitionApp.listen(
on<_SetPersonProvider>(_onSetPersonProvider);
on<_OnUpdatePersonProvider>(_onOnUpdatePersonProvider);
_subscriptions.add(accountPrefController.personProvider.listen(
(event) {
add(_OnUpdateEnableFaceRecognitionApp(event));
add(_OnUpdatePersonProvider(event));
},
onError: (e, stackTrace) {
add(_SetError(_WritePrefError(e, stackTrace)));
@ -131,18 +131,18 @@ class _Bloc extends Bloc<_Event, _State> {
emit(state.copyWith(shareFolder: ev.shareFolder));
}
void _onSetEnableFaceRecognitionApp(
_SetEnableFaceRecognitionApp ev, Emitter<_State> emit) {
void _onSetPersonProvider(_SetPersonProvider ev, Emitter<_State> emit) {
_log.info(ev);
accountPrefController
.setEnableFaceRecognitionApp(ev.isEnableFaceRecognitionApp);
accountPrefController.setPersonProvider(ev.personProvider);
}
void _onOnUpdateEnableFaceRecognitionApp(
_OnUpdateEnableFaceRecognitionApp ev, Emitter<_State> emit) {
void _onOnUpdatePersonProvider(
_OnUpdatePersonProvider ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(
isEnableFaceRecognitionApp: ev.isEnableFaceRecognitionApp));
personProvider: ev.personProvider,
shouldResync: true,
));
}
void _onSetError(_SetError ev, Emitter<_State> emit) {
@ -152,6 +152,7 @@ class _Bloc extends Bloc<_Event, _State> {
final DiContainer _c;
final AccountPrefController accountPrefController;
final AccountSettingsOption? highlight;
final _subscriptions = <StreamSubscription>[];
var _isHandlingError = false;

Some files were not shown because too many files have changed in this diff Show more