mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-13 18:58:53 +01:00
Merge branch 'face-rewrite' into dev
This commit is contained in:
commit
56e3fbd784
122 changed files with 5068 additions and 1411 deletions
|
@ -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?
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
88
app/lib/controller/persons_controller.dart
Normal file
88
app/lib/controller/persons_controller.dart
Normal 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),
|
||||
);
|
||||
}
|
48
app/lib/controller/persons_controller.g.dart
Normal file
48
app/lib/controller/persons_controller.g.dart
Normal 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");
|
||||
}
|
51
app/lib/controller/sync_controller.dart
Normal file
51
app/lib/controller/sync_controller.dart
Normal 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;
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
24
app/lib/entity/face_recognition_face.dart
Normal file
24
app/lib/entity/face_recognition_face.dart
Normal 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;
|
||||
}
|
14
app/lib/entity/face_recognition_face.g.dart
Normal file
14
app/lib/entity/face_recognition_face.g.dart
Normal 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}";
|
||||
}
|
||||
}
|
27
app/lib/entity/face_recognition_person.dart
Normal file
27
app/lib/entity/face_recognition_person.dart
Normal 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;
|
||||
}
|
14
app/lib/entity/face_recognition_person.g.dart
Normal file
14
app/lib/entity/face_recognition_person.g.dart
Normal 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}";
|
||||
}
|
||||
}
|
101
app/lib/entity/face_recognition_person/data_source.dart
Normal file
101
app/lib/entity/face_recognition_person/data_source.dart
Normal 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;
|
||||
}
|
25
app/lib/entity/face_recognition_person/data_source.g.dart
Normal file
25
app/lib/entity/face_recognition_person/data_source.g.dart
Normal 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");
|
||||
}
|
52
app/lib/entity/face_recognition_person/repo.dart
Normal file
52
app/lib/entity/face_recognition_person/repo.dart
Normal 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);
|
||||
}
|
16
app/lib/entity/face_recognition_person/repo.g.dart
Normal file
16
app/lib/entity/face_recognition_person/repo.g.dart
Normal 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");
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
27
app/lib/entity/person/adapter.dart
Normal file
27
app/lib/entity/person/adapter.dart
Normal 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();
|
||||
}
|
47
app/lib/entity/person/adapter/face_recognition.dart
Normal file
47
app/lib/entity/person/adapter/face_recognition.dart
Normal 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;
|
||||
}
|
15
app/lib/entity/person/adapter/face_recognition.g.dart
Normal file
15
app/lib/entity/person/adapter/face_recognition.g.dart
Normal 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");
|
||||
}
|
47
app/lib/entity/person/adapter/recognize.dart
Normal file
47
app/lib/entity/person/adapter/recognize.dart
Normal 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;
|
||||
}
|
|
@ -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");
|
||||
}
|
32
app/lib/entity/person/builder.dart
Normal file
32
app/lib/entity/person/builder.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
55
app/lib/entity/person/content_provider/face_recognition.dart
Normal file
55
app/lib/entity/person/content_provider/face_recognition.dart
Normal 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;
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
108
app/lib/entity/person/content_provider/recognize.dart
Normal file
108
app/lib/entity/person/content_provider/recognize.dart
Normal 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;
|
||||
}
|
14
app/lib/entity/person/content_provider/recognize.g.dart
Normal file
14
app/lib/entity/person/content_provider/recognize.g.dart
Normal 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}]"}}";
|
||||
}
|
||||
}
|
|
@ -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"],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
23
app/lib/entity/person_face.dart
Normal file
23
app/lib/entity/person_face.dart
Normal 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;
|
||||
}
|
14
app/lib/entity/person_face.g.dart
Normal file
14
app/lib/entity/person_face.g.dart
Normal 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}}";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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()];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
26
app/lib/entity/recognize_face.dart
Normal file
26
app/lib/entity/recognize_face.dart
Normal 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;
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
212
app/lib/entity/recognize_face/data_source.dart
Normal file
212
app/lib/entity/recognize_face/data_source.dart
Normal 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;
|
||||
}
|
25
app/lib/entity/recognize_face/data_source.g.dart
Normal file
25
app/lib/entity/recognize_face/data_source.g.dart
Normal 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");
|
||||
}
|
75
app/lib/entity/recognize_face/repo.dart
Normal file
75
app/lib/entity/recognize_face/repo.dart
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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");
|
||||
}
|
113
app/lib/entity/recognize_face_item.dart
Normal file
113
app/lib/entity/recognize_face_item.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
14
app/lib/entity/recognize_face_item.g.dart
Normal file
14
app/lib/entity/recognize_face_item.g.dart
Normal 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}]"}}";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
@ -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;");
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
104
app/lib/entity/sqlite/table.g.dart
Normal file
104
app/lib/entity/sqlite/table.g.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
81
app/lib/use_case/person/list_person.dart
Normal file
81
app/lib/use_case/person/list_person.dart
Normal 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;
|
||||
}
|
|
@ -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");
|
||||
}
|
14
app/lib/use_case/person/list_person_face.dart
Normal file
14
app/lib/use_case/person/list_person_face.dart
Normal 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;
|
||||
}
|
39
app/lib/use_case/person/sync_person.dart
Normal file
39
app/lib/use_case/person/sync_person.dart
Normal 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;
|
||||
}
|
|
@ -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");
|
||||
}
|
|
@ -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;
|
||||
}
|
13
app/lib/use_case/recognize_face/list_recognize_face.dart
Normal file
13
app/lib/use_case/recognize_face/list_recognize_face.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
288
app/lib/use_case/recognize_face/sync_recognize_face.dart
Normal file
288
app/lib/use_case/recognize_face/sync_recognize_face.dart
Normal 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;
|
||||
}
|
|
@ -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");
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>[];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}]}";
|
||||
}
|
||||
}
|
||||
|
|
56
app/lib/widget/people_browser/bloc.dart
Normal file
56
app/lib/widget/people_browser/bloc.dart
Normal 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;
|
||||
}
|
||||
}
|
49
app/lib/widget/people_browser/state_event.dart
Normal file
49
app/lib/widget/people_browser/state_event.dart
Normal 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;
|
||||
}
|
24
app/lib/widget/people_browser/type.dart
Normal file
24
app/lib/widget/people_browser/type.dart
Normal 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;
|
||||
}
|
97
app/lib/widget/person_thumbnail.dart
Normal file
97
app/lib/widget/person_thumbnail.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
49
app/lib/widget/search_landing/type.dart
Normal file
49
app/lib/widget/search_landing/type.dart
Normal 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;
|
||||
}
|
56
app/lib/widget/search_landing/view.dart
Normal file
56
app/lib/widget/search_landing/view.dart
Normal 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;
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue